8/15/2021 - Behind the Curtains of Combat Counters


Another Sunday, another potential blog post. As I teased last week, I'll spend today talking a bit about a few additional enemy combat mechanics that I think will be fun to integrate in future and current enemy types - parrying, countering, blocking, and air-burstering/countering. 

WARNING - IF UR NOT SUPER FAMILIAR W/ AINODES and AITREES, THIS MIGHT BE REALLY CONFUSING! YOU HAVE BEEN WARNED.

Parrying/Countering

In the demo, there aren't any enemies that counter-attack blocked moves per se. There are some enemies that deal with aggressive melee player via hitstun resistance/super-armor, but nothing (currently) that behaves like this:

Here, we see the 3rd-hit of a player's combo get countered and punished by what im currently referring to as an "Elite Goblin". This enemy is currently not present in the demo - the Elite Goblin was create to test a number of combat mechanics that i wanted to try out, including countering!
The implementation of a counter is pretty straight forward, at least at a high level:


In this boilerplate function, a counter is made-up of 5 or 6-ish AINode (which you can think of as "Actions") that need to complete before finishing.

The first 2 AINodes - BlockedAnAttack and HurtboxWillBeIsolated[int i] check to see if (surprised) we blocked an attack w/ the appropriate hurtbox ("blocking" in Warp Soldier is implement by changing to a hurtbox w/ high resistance).

Next we check to see if the AI designer wanted to do any additional actions before attempting to repel the attacker, and if so, we ad that to the sequence. Then, we actually repel the attacker in seq.Add(RepelAttacker.Instance).

The next line seems kinda odd tho - is it possible for a monster to be in hitstun while blocking? Given the way Warp Soldier's blocking is implemented, this is definitely possible, and actually necessary in our implementation of Air-Bursting (stopping a player's air-juggle combo in mid-air). However in that gif, forcing a monster into recover from hitstun when it's not currently in hitstun is idempotent - i.e. it won't incur any weird side-effects. But in case if it is already in hitstun, we need to make sure the defender gets complete agency back.

Next, if check the maintainRiposteHurtbox variable specific by the caller. In some enemy designs, we actually want to make sure the enemy is still blocking or invincible during the parry actions that follow. If that variable is set to false, it just does the postRiposteSequence specified by the caller. However, if it is set to true, it'll keep that hurtbox enabled every frame of the postRiposteSequence via IsolateSingleHurtboxThisFrame[] + postRiposteSequence And()'d together.

The final line, Seq(seq) then bundles-up all we specified into an IndexSequence. Without going too much into my AITree implementation, you can view an And(....) as a set of actions we simultaneous poke every frame (if they all succeed), whereas a Seq(...) is a sequence of actions we step through across multiple frames, only moving onto the next one if the previous one succeeds.

(Note to self - consider a blog post on AITrees 101).

In the meantime, here's what the call site typically looks like.


The r_seq describe what you're actually seeing in the gif: a riposte animation (the backswing), the poke-dash attack w/ the trident/dident, followed by a brief idle animation. The result: an elegant-and-scalable function for easily building counterattacks! At least for things which already know how to block...

This is likely leaving the reader w/ many-question, esp if they have no experience w/ AI-trees, but one question you might have is this - how did it know to counter only the 3rd hit? Is it random? Is there a per-enemy variable that specifies this? Or can a designer manually specify this somehow?

The truth is - most of the code above is actually an old implementation that assumes a fixed number hits on-block per-character-type. The current code looks like this:

TODO: delete undocumented already-implemented TODOs or lone "TODO" comments like this one

At the bottom is an Or(...) AINode, which will try all of its sub-branches until it finds something that returns "Running" or "Success".  It'll first see if its been combo'd enough via MetConstantComboLimit[hitsTillCounter]. If it hasn't (which will usually be the case if it hasn't blocked any attacks), it'll return "Failure", the whole parent Sequence will return Failure, and that Sequence's parent Or will move onto its next sub-ainode - the Block Sequence. If the Block Condition succeeds, it'll start blocking and increment the combo counter w/ those blocked hits (hence the Debug.LogError line enforced above - the combo counter normally only goes-up if the attacks do damage, so we need to make sure the user's block-config actually has that flag turned on. Otherwise, the Counter Sequence's first AINode will never trigger).

Eventually, the first Sequence's MetConstantComboLimit condition gets met, and it'll do stuff as the original BuildVanillaParrySequence except A. it also takes care of block behavior (which would have to be specified separately previously), and B. doesn't rely on some constant variable defined per-monster.

Here's what this function's callsite looks like:

This can basically be read as "If the target's next attack is probably gonna hit me, block with this, and on the 3rd hit, do a riposteAnimation, then a poke-dash, then do a brief idle animation".

Air-Bursting

An air-burst, or Air-Counter, is really just the same thing as a Parry, but it only triggers if the user is airborne!



At the very bottom, you'll see that this uses the BuildVanillaParrySequence used by the old counter. It also just checks for if the user is in the air.

Looking at the gif, you may be wondering - how can he counter if he isn't blocking?

The truth is: he is "blocking". Its just not telegraphed:


Every enemy has an invisible combo counter which an AI designer can leverage. If the Goblin designer (me) wants to do something like - prevent an air-combo from going for too long, earlier in the Goblin's AITree constructor, I can tell the Goblin to ready an insta-block hurtbox if they're about to hit that counter-limit (via WillOrHaveHitComboLimit). So if an Elite Goblin has a combo limit counter of, say, 9, on the 8th hit, it'll ready its defenses. THEN, on the 9th hit, the parry defense conditions (specified later in the AI-Tree constructor) used by the AerialRiposteSequence will kick-in and repel the attacker/player. But instead of performing a counterattack for the post-riposte sequence, i have it do an air-roll, landing animation, and brief idle animation.

Conclusion

Well, that was hot sweaty and confusing. Or maybe that's just how im feeling right now without proper air-conditioning @_@.

So why bother with any of this?

Right now, the demo mostly consists of two types of combat encounters - small dudes who you can combo to-infinity, or dudes that you poke a few times, step away from to avoid a counter-attack, then poke again. All the tools mentioned above let me spice-up the encounters and combat-flow-charts a bit. I can do stuff like randomly force a counter-on-block to add a bit of unpredictability and force the player's hand. The air-burst/roll is particularly interesting because it acts as a bit of a "situation-reset" (going from combo-game to neutral-baiting-and-poking-game again). I might even use some of these tools to give the player a potential parry-move. Point is - I now have some more tools to prevent high-HP encounters from feeling too easy or too tedious. Is it the best-or-easiest way of improving combat in general? Only one way to find out...

In the next week or two I might make the Elite Goblin an optional boss in the demo. OR i might wait till September to do a more substantial play/feel/demo content update. We'll see - depends on how busy I am with full-game content in the near future and whether or not it can synergize with this demo build.

In any case, you'll hear back from me here on itch probably next-week-ish.

Get Warp Soldier - Nov 2021 Demo

Download NowName your own price

Leave a comment

Log in with itch.io to leave a comment.