Log 24: Airing Out Attacks
- Kassandra McCormack
- Dec 23, 2024
- 8 min read
For this stretch of my game development adventure I decided to work on combat, specifically on in-air attacks. Specifically what I mean: What happens when the player presses one of the attack buttons while still in the air, either from jumping or falling off a ledge. In-air attacks are interesting because not every Warriors game has them, especially if they don't have a dedicated jump button. But, because of the fact that I intend to have a level of verticality in my level design, I need to have a dedicated jump button. It will make AI pathfinding and navigation a pain, but in the end I think it will be worth it.
If one is going to make an action game with a jump button, there had better be a way to attack in-air. How to implement that mechanic can also vary: do I just do a single attack per button and let gravity function as normal? Do I add a bit of upward impulse on each attack and give the character a proper attack string? Do I just leave gravity as a mere suggestion and turn it off while the player is in a combo like some fighting games?
It is important to figure out where on the axis of action speed your game sits: is it slower and heavier? More like an Elden Ring? Or faster and lighter like a Devil May Cry? This is always important to consider, but especially for how a game handles aerial combat. Personally I like lighter, punchy-er (pun intended) combat in my Sentai properties and action games that take gravity as more of a suggestion than a rule feel better for that. But, I also don't want characters to stay in the air forever because that's just flight, and while the line between the two may be subtle it IS there. So, I my intent was to make it so the player could only do 1 combo string while in the air, but while doing that string gravity would take a back seat and allow the player to finish their string.
I'm aiming for something in between but definitely leaning more towards DMC.
Videos made by CurtisTheCactus (Can You Beat Elden Ring ONLY using JUMP ATTACKS?: https://youtu.be/uVqIwUFgpAU?si=uxuNKUD1uKrRn-Ho) & 時影 Tokikage (DMC5 Dante Air Combo and Weapon Switching practice: https://youtu.be/4gqqp7XsyZ8?si=LpSjm2iYBqtlz3FI).
The first thing I would need to do is add some way for the Attack Controller Component (ACC) to check the grounded state of the attached character. Fortunately the Character Movement Component (CMC) already has a check for that under the function "Is Moving on Ground" (which does not require actual movement to register as being on the ground), so I just had to add a reference to the CMC in the Attack Controller's "Initialization" function. The grounding check comes into play mainly in two functions the "Have Attack at Number" function (which checks if a valid index is found in a given attack array given a proposed attack number) and the "Get Current Attack Info" function (which I refactored into a series of nested Select function calls to get the appropriate attack info).
While I was working on that I decided to put all the attacking stuff into the ACC so the actual character only needs to call a single function and not a whole mess on the Event Graph. I figured I could get away with this since all the other components necessary for attacking (CMC, Faction Controller, etc) were being passed into Attack Controller's "Initialize" function anyway, and it looks nicer. The only extra bit I had to do was move the Basic Character's "Custom Play Montage" into an interface event so it could be called from a component without having to do any casts.
Speaking of playing animations, I discovered an interesting bug: it turns out I had never actually implemented not being able to move while attacking. The only reason it had appeared to be working was that some of the attack animations had root motion applied and some (the heavy attacks, for some reason) did not. I decided to just apply root motion to the attacks that didn't have it and call it a day on that problem instead of making it more complicated than it needed to be. I want the game to be a power fantasy, but not too easy and forcing players to commit to an attack until it is done is one of the basic ways to non-numerically increase difficulty (versus numerically such as enemy health and damage).
Once I had all that squared away, disabling gravity became as simple as setting the "Gravity Scale" on the CMC to 0 while attacking, and then on the "Attack Combo Complete Event" simply resetting it and allowing the character to fall. This left me with the conundrum of how to limit it to a single combo in the air? Otherwise, the player wouldn't be able to move anywhere due to the aforementioned root motion application.
Only doing a single combo within a jump just required a simple bool check, setting it to true once the "Attack Combo Complete Event" fires. But in order to set the bool to false, I had to make a dispatcher on the Basic Character that fires every tick with whether the character is grounded or not. I decided to use the Tick event on the character and not the Attack Controller because I was already using it for previous mechanics, so I didn't want to create an additional Tick call. Basically once the Dispatcher sends "True" it updates the "Attacked This Jump" bool to false and it can be used again on another jump/fall.

One thing I decided I didn't want to include in the game was in-air dodging, which it turns out I accidentally made by combining my mechanics. Of course, as I'm writing this I'm having second thoughts about it. So, at some point I'll have to fully develop an air dodge mechanic and test if it is something I want to include. For now I've disabled the ability to dodge in mid-air by doing a grounded check before executing a dodge animation.
During my testing, I noticed that sometimes when hitting a character they would get knocked back at incorrect times. Turns out there was an unnecessary branch in my "Hit Character" function that was knocking characters back early and not only when a combo was completed. All I had to do to fix the problem was detach the branch. I love it when a problem has an easy solution.
For in-air attacks just having the character float there and punch or slash is all well and good, for light attacks, but I wanted something bigger and flashier for the heavy attack. For the test character, a big drop kick that drags enemies into the ground and does big damage on landing is what I wanted. In order to do this, the heavy attack needs to be broken up into 3 stages: the immediate response to the button press where the character starts the attack, the drop of variable length where the character falls back to the ground, and the landing once they touch down. Three stages, requiring 3 separate animations. Fortunately one of the animation packs I had bought included a drop kick perfectly divided into those parts so this is obviously a common requirement for games. Good to know I'm on the right track.
The problem is that because the player can push the button at any point in a fall, I needed some way to automatically transition to the second and third stages. For the second, variable length, stage where the character is falling I made a modified "Do Heavy Attack" function called "Auto-Do Heavy Attack". I subscribed this function to the "Can Attack" dispatcher so when it returns true (received a specific AnimNotify in the 1st animation) it does the functionality of "Do Heavy Attack". This auto-function plays a looping montage of the character falling, but with their foot extended in a drop kick. Funnily enough, I learned that while there are two checkboxes in the montage edit widow labeled "Loop", none of them actually loop the montage and instead you have to change how the current section links to the next. I had to do a bit of digging to find that out. Finally, I made a "Landed" dispatcher that gets called when "Grounded" goes from false to true and once this dispatcher fires it plays the landing montage and deals damage and knock back.
I learned the hard way that "Auto-Do Heavy Attack" and "On Landing Heavy Attack" couldn't just call regular "Do Heavy Attack" with some added bells and whistles. Doing that was causing it to play the regular grounded heavy kick animation if the character landed after a short enough fall, so I wound up pulling out some of the core functionality of "Do Heavy Attack" and copying it into the other functions so I could fix that issue.
Because of the way the attack animations and code are dependent on specific AnimNotifies going off, if the player landed before the AnimNotify "AN_AttackStage(CanCombo)" either from a short jump or waiting until almost touching the ground, it would cause the character to snap to their idle pose without playing the landing animation. Because the AnimNotify isn't setting "Can Attack" back to true yet, my code wasn't recognizing that it had a valid montage (a sort of safety thing in place so players can't cancel animations into other attacks early), so I had to add a special bypass for that bool check that only gets used in the "On Landing Heavy Attack" call.
An interesting interplay between mechanics in my game was that unlike on-ground attacks where different numbers of light attacks resulted in a different heavy attack, no matter how many light attacks the player does in-air (max unmorphed being 3), they do the same heavy attack every time. I chose to do this because of the simple limitation that I only had access to so many air attack animations and needed to simplify how in-air attacking worked because of it. In order to do this, I had to put a special branch in the "Do Heavy Attack" function that checks if the player is in the air, if they are it resets the "Attack Number" variable back to -1 so I can use the same array logic to store and retrieve the different stages of the drop attack.
An interesting bug occurred while testing: if the player pressed the heavy attack button immediately after pressing the light attack button, it would occasionally skip the falling animation and cancel into the regular non-attacking falling animation, only to play the attack landing animation when it was over. In order to fix this I wound up having to put an extra check of the "Can Attack" bool at the beginnings of the "Do Light Attack" and "Do Heavy Attack" functions so they couldn't skip stages with rapid button presses.
The final functionality I had to create for the dropping heavy attack is that when the player starts it, it should drag all enemies it hits down with the player character, that way I can guarantee players are doing the damage the player is expecting and not letting enemies slip through the cracks and either falling too fast, or not fast enough, or even getting knocked back early. I started off by looking at the code in Attack Controller's hit detection and damage function and realized I needed the exact same thing except for the damage application. So after a bit of refactoring I split the hit detection into its own function that returns a parsed map of actors with the locations they were hit at. I can then use that function in both the damage application on normal hit detection, and on gathering them all up and bind them to the player character through physics constraints.

Comments