Equip and unequip: montages, gameplay events, anim layers
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.
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
- Play Montage and Wait →
AM_Hero_Axe_Equip. An ability task: starts the montage and lets the ability keep going while it plays. - At the same time, Wait Gameplay Event for
Player.Event.Equip.Axe. - In the montage, at the frame the hand grabs the axe,
AN_SendGameplayEventfires: "sendPlayer.Event.Equip.Axeto my owner". - The wait completes. The ability gets the axe (combat component, by tag
Player.Weapon.Axe), attaches it to the hand socket, setsCurrentEquippedWeaponTag, and links the axe anim layer. - 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
| Asset | Role |
|---|---|
ALI_Hero | Animation Layer Interface: declares the layer slots, e.g. "Locomotion", without any animation in them |
ABP_HeroCharacter | The main anim BP. Its graph calls the layers but doesn't define them |
MasterAnimLayer_Hero | Shared parent for layer Blueprints (based on UWarriorHeroLinkedAnimLayer) |
AnimLayer_Hero_Axe | Fills 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());
}
- A linked layer is its own anim instance. It doesn't have
GroundSpeedand friends. This getter fetches the main instance (the mesh's) so the layer's blend space can read its variables. BlueprintThreadSafe: promises this function is safe to call from the anim graph's worker-thread update (thread-safe update). Without it, the anim graph warns when you call it from a thread-safe function.
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.
UInputMappingContext* WeaponInputMappingContext;has no= nullptr. YourFWarriorInputActionConfigdoes initialise its pointer. Do the same here: a struct created in plain C++ (not through reflection) would otherwise hold a garbage address.- In
WarriorGameplayTags.cpp, the twoPlayer_Eventdefinitions end with;and the others don't. Harmless (an empty statement), but inconsistent. - Your uncommitted changes fix the include order and the
Unquiptypo. Commit them.
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.