How does combat work in the ActionRPG sample?

I am using the ActionRPG sample to learn how to create my own action RPG game, but it’s not obvious how a character swinging their weapon and colliding with an enemy translates into calls into the ability system that invokes URPGDamageExecution. How does the calculation get its target? How are the different attack types handled, and why does the GA_PlayerAxeMelee gameplay ability have a map of tags to effects? What is the ConsumeWeapon event in the WeaponActor blueprint used for?

How does this all work?

1 Like

Answering my own questions:

When a player presses the attack button (left mouse button), the PlayerController Blueprint invokes the Do Melee Attack function on the character, as shown below:

The Do Melee Attack function does a few different things, as shown in the image below. First, it confirms that there is some kind of ability/attack that the character can do. Next, it checks to see if the character is already in the middle of performing some other melee attack, to allow the player to perform a “combo attack” – giving the player the ability to alternate between different variations of an attack by rapidly pressing the attack key. For our purposes here, ignore the Jump Section for Combo branch and focus on the lower branch – Activate Abilities with Item Slot.

That activates the Gameplay Ability for the attack type of the equipped weapon (aka. the “attack GA”). The base class for melee attack GAs looks like the image below. The most important part is the call to Play Montage and Wait for Event. This is an “ability task” – something that a GA activates and allows to run until it returns a result. In this case, the task plays whatever montage (i.e. character animation) the attack GA has specified.

Something that tripped me up is that: 1) the GA, 2) the animation that is played for the character during the attack (the montage), and 3) the model of the equipped weapon (the WeaponActor) are actually not tightly coupled to one another – they’re distinct pieces. The weapon actor/model is bound to the GA for the type of attack via a RPGWeaponItem data blueprint, a sub-type of RPGItem, which the Action RPG project uses as the base class for all items that a character can equip. The montage, meanwhile, is connected to the weapon via the GA – as mentioned above, the attack GA indicates what montage to play for its attack type.

As a side note: RPGItem itself is a sub-type of PrimaryDataAsset which – without getting lost in the weeds – provides a way for a large game to dynamically discover and automatically load information about items that can appear in many different maps/areas of a game. For example, imagine that a game provides merchants that sell items – the game needs a way to load information about all the items available for purchase without anything in the map or the player’s inventory prompting those items to become loaded. I believe a simpler approach may have been to combine RPGItems with the GAs for those items to reduce how many objects are needed, but that may have increased the memory footprint when providing a listing of items (e.g., consider a store with 1,000 items in a giant list) and it would be wasteful/redundant if multiple items behaved the same way with a few different variations of attack variables/montages. Anyways, there’s a bit more about this and the asset manager system in the documentation for the Action RPG sample.

Now, back to how the actual attack mechanics work when the montage is fired by the ability task in the attack GA. When a character swings their weapon, it initially has no collision enabled to ensure that it does not accidentally register overlap (i.e., a hit) on the character who is swinging the weapon or any other nearby geometry. Collision is only enabled for the weapon during the appropriate frames of the animation, when the weapon is fully extended to strike something. This is handled via “animation notifications” (“anim notifies” for short), as shown below. Each notify event has an associated event tag, allowing the animation (the type of swing, for example) to drive what type of damage is being done.

352366-anim-notifies.gif

The event graphs for the notifies look like the image below. The top graph covers the notify event that fires at the start of the swing and the bottom graph covers the event that fires at the end of the swing.

At the start of the swing, the notify at the top of the image obtains the player’s current WeaponActor, sets its status to “attacking” (side note: this is also done in the weapon’s event handler, so I am not sure why it’s also being done here), and then fires an Event Begin Weapon Attack event on that weapon to allow it to react to the start of the swing. The tag that was provided by the specific animation sequence – along with some other timing parameters that I have not yet figured out – also get passed in, so that the weapon knows exactly what type of attack it is participating in.

At the end of the swing, the notify at the bottom of the image fires an Event End Weapon Attack event on the current weapon so that it knows that the weapon is no longer eligible to participate in an attack.

The physical model/actor for each weapon extends from a WeaponActor base blueprint, which has an event graph that contains the two events I mentioned above, as shown in the image below:

All the Event Begin Weapon Attack event does is: 1) update the state of the weapon with the parameters provided by the anim. notify, 2) sets the “is attacking” flag on the weapon, and 3) enables capsule collision on the weapon model. This allows the weapon to start registering overlap events whenever it touches anything in the world, including enemies and the player’s pawn.

Meanwhile, the Event End Weapon Attack event does the reverse – it clears the “Is attacking” flag and turns collision off, so that the weapon no longer registers overlap events.

The Begin Overlap and End Overlap events are really where the interesting parts happen:

This event handler first ensures that the collision that is being registered is valid, in the sense that it is not collision between the player and their weapon, and is only happening when the weapon is in attack mode (just a safety against a race condition during the animation, right before collision is completely turned off). Assuming that the collision is legit, the handler uses a Do Once to ensure that the same collision doesn’t keep registering overlap events that result in damage until the weapon physically stops touching the target (e.g., if the weapon is slicing through an enemy, it only does a certain amount of damage per slice rather than doing more damage throughout the swing).

You’ll notice that the event handler gets the “instigator” multiple times. In this context, the instigator is the player character who caused the damage to happen (i.e. the pawn actor under whose control the attack is being done). This is important, because the source of the attack (“self”) is the weapon at this point, and the damage the character does to the target needs to be based on the ability system component attributes of the player not the weapon (the weapon does not have an ASC).

All this winds up in the Send Gameplay Event to Actor call, which is responsible for returning control to the attack GA. This fires the “Event Received” pin on the “Play Montage and Wait for Event” node of the attack GA, as shown in the image below. At first glance, it is not immediately obvious how this works, since the C++ code for the “Send Gameplay Event to Actor” node has been configured to wait for a tag of Event.Montage, while the tag that gets passed-in to Send Gameplay Event To Actor provides a different tag – a tag matching the “attack type” tag from the montage. But, the key to understanding this is that all the attack type tags are sub-tags of Event.Montage (e.g., Event.Montage.Shared.WeaponHit) and the code navigates up the tag hierarchy until it finds a GA that is waiting for an event matching an ancestory of the tag (e.g., Event.Montage is a grandparent in the hierarchy of Event.Montage.Shared.WeaponHit).

Once control has returned to the attack GA, it uses a custom “Apply Effect Container” node to convert the type of attack event into the appropriate type of target data and gameplay effect (see below). This is where the map on each attack GA comes into use (also shown below).


Based on the nature of the attack being invoked (as identified by the tag sent in from the montage), the appropriate Gameplay Effect and target data are produced. Each of those GEs is what actually invokes the damage calculation.

Now, that’s great if the character actually hit a target with their attack, but what happens when they don’t? In that case, control still returns to the GA but this time via the “On Blend Out” pin. Put simply, UE4 plays the montage and if it doesn’t happen to register any hits during the animation, it “blends out” and back to whatever idle animation the character would be playing if they weren’t attacking. Back in the MeleeBase event graph, that just results in the attack GA ending (i.e., the attack finishing).

7 Likes

You’re amazing! Really appreciate the writeup, answered a few questions I had about GAS.

2 Likes