Equip and unequip: montages, gameplay events, anim layers

Commits 8d6e783, 7a3d5fc + work in progress · GA_Hero_EquipAxe, AN_SendGameplayEvent, ALI_Hero, AnimLayer_Hero_Axe, FWarriorWeaponData · 10 min

In one breath

Pressing E runs the equip ability. It plays a montage (reach for the axe). At the exact frame the hand closes, an anim notify sends a gameplay event; the ability is waiting for that event, and attaches the axe to the hand. Then the character's locomotion switches to axe animations by linking a different anim layer. Unequip is the same in reverse.

Note on sources

Most of this lesson lives in Blueprint assets, which I can't read as text. The flow below is the standard GAS pattern that your asset and tag names match (AN_SendGameplayEvent, Player.Event.Equip.Axe, ALI_Hero, AnimLayer_Hero_Axe). Open GA_Hero_EquipAxe next to this page and check each step against your actual graph. That's a good study exercise in itself.

The flow inside GA_Hero_EquipAxe

  1. Play Montage and Wait → AM_Hero_Axe_Equip. An ability task: starts the montage and lets the ability keep going while it plays.
  2. At the same time, Wait Gameplay Event for Player.Event.Equip.Axe.
  3. In the montage, at the frame the hand grabs the axe, AN_SendGameplayEvent fires: "send Player.Event.Equip.Axe to my owner".
  4. The wait completes. The ability gets the axe (combat component, by tag Player.Weapon.Axe), attaches it to the hand socket, sets CurrentEquippedWeaponTag, and links the axe anim layer.
  5. Montage ends → End Ability.

Why an event from the animation, not a timer?

"Attach after 0.4 seconds" breaks the moment someone changes the animation's speed or length, or the montage plays at a different rate. A notify placed on the frame where the hand closes is always in sync, because the animation itself says when. It's the same "one source of truth" idea as the Niagara drift question.

Why a gameplay event rather than an ordinary anim notify event? Gameplay events go through the ASC with a tag, so the notify doesn't need to know about the ability, and the ability doesn't need to know about the montage. One generic AN_SendGameplayEvent with a tag field serves every ability.

Anim layers: swapping whole sets of animations

AssetRole
ALI_HeroAnimation Layer Interface: declares the layer slots, e.g. "Locomotion", without any animation in them
ABP_HeroCharacterThe main anim BP. Its graph calls the layers but doesn't define them
MasterAnimLayer_HeroShared parent for layer Blueprints (based on UWarriorHeroLinkedAnimLayer)
AnimLayer_Hero_AxeFills the slots with axe animations (BS_DefaultLocomotion_Axe)

Equipping calls LinkAnimClassLayers(AnimLayer_Hero_Axe) on the mesh: the main graph stays the same, but its "Locomotion" slot now plays the axe version. Unequipping calls UnlinkAnimClassLayers, and it falls back to unarmed. This is the interface pattern again, applied to animation: the main graph depends on the interface, not on any particular weapon's animations.

The C++ bit: reaching the main anim instance from a layer

UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe))
UWarriorHeroAnimInstance* GetHeroAnimInstance() const;

UWarriorHeroAnimInstance* UWarriorHeroLinkedAnimLayer::GetHeroAnimInstance() const
{
    return Cast<UWarriorHeroAnimInstance>(GetOwningComponent()->GetAnimInstance());
}

Weapon data: each weapon brings its own setup

USTRUCT(BlueprintType)
struct FWarriorWeaponData
{
    UPROPERTY(EditDefaultsOnly) TSubclassOf<UWarriorHeroLinkedAnimLayer> WeaponAnimLayerToLink;
    UPROPERTY(EditDefaultsOnly) UInputMappingContext* WeaponInputMappingContext;           // work in progress
    UPROPERTY(EditDefaultsOnly, meta=(TitleProperty = "InputTag"))
    TArray<FWarriorHeroAbilitySet> DefaultWeaponAbilities;                                // work in progress
};
// AWarriorHeroWeapon
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly) FWarriorWeaponData HeroWeaponData;

The equip ability reads everything it needs from the weapon: which anim layer to link, which input mapping context to add (axe controls), and which abilities to grant while it's held (light/heavy attacks, each with an input tag, using the same FWarriorHeroAbilitySet as last lesson). Add a second weapon later and the equip ability doesn't change. Only the data does.

Small things in your code

Quiz yourself

Why attach the axe on a gameplay event from the montage, instead of after a delay?

The notify sits on the exact frame the hand closes, so it stays in sync if the animation's length or play rate changes. A delay doesn't.

Why send a gameplay event (with a tag) rather than a plain anim notify event?

It goes through the ASC by tag, so the notify and the ability don't need to know about each other. One generic notify works for every ability.

Which ability tasks run the equip ability?

Play Montage and Wait (plays the equip montage) and Wait Gameplay Event (for Player.Event.Equip.Axe).

What's an Animation Layer Interface, and what does LinkAnimClassLayers do?

The interface declares layer slots (like Locomotion) that the main graph calls. Linking a layer class fills those slots with its animations (e.g. the axe set) without changing the main graph.

Why does UWarriorHeroLinkedAnimLayer need GetHeroAnimInstance()?

A linked layer is a separate anim instance with none of the main instance's variables. The getter fetches the mesh's main instance so the layer can read ground speed and the like.

What does meta = (BlueprintThreadSafe) promise?

That the function is safe to call from the anim graph's thread-safe (worker thread) update.

Why store the anim layer, input context and abilities in FWarriorWeaponData on the weapon?

The equip ability reads them from whichever weapon it's equipping, so new weapons need only new data, not new ability code.

Why initialise pointer members in a struct with = nullptr?

A struct created in plain C++ doesn't zero its members, so the pointer would hold a garbage address. Reflection-created ones happen to be zeroed, but don't rely on it.