Weapon abilities: grant on equip, remove on unequip

Commits 7629966, 02ab227 + work in progress · GrantHeroWeaponAbilities, RemoveGrantedHeroWeaponAbilities, AWarriorHeroWeapon::GrantedAbilitySpecHandles · 9 min

In one breath

When you equip the axe, its attack abilities are granted (each with its input tag), and the handles GAS gives back are stored on the weapon. When you unequip, those exact handles are used to remove them. The axe's attacks only exist while the axe is in your hand. Handles are the receipts that let you undo precisely what you did.

The round trip

  1. Equip (GA_Hero_EquipAxe): read HeroWeaponData.DefaultWeaponAbilities from the axe → GrantHeroWeaponAbilities → get back a list of handles → AssignGrantedAbilitySpecHandles stores them on the axe.
  2. While equipped, pressing an attack button finds those abilities by input tag (ability input).
  3. Unequip (GA_Hero_UnequipAxe): GetGrantAbilitySpecHandles from the axe → RemoveGrantedHeroWeaponAbilities clears each one and empties the list.

Granting, and getting handles back

UFUNCTION(BlueprintCallable, Category="Warrior|Ability", meta = (ApplyLevel = 1))
void GrantHeroWeaponAbilities(const TArray<FWarriorHeroAbilitySet>& InDefaultWeaponAbilities,
                              int32 ApplyLevel,
                              TArray<FGameplayAbilitySpecHandle>& OutgrantedAbilitySpecHandles);

// body: same spec-building loop as the startup data, plus:
OutgrantedAbilitySpecHandles.AddUnique(GiveAbility(AbilitySpec));

Out parameters and UPARAM(ref): how references become Blueprint pins

This is the references page in action. A UFUNCTION's reference parameters turn into different pins depending on const:

C++ parameterBlueprint pinUsed here for
const TArray<…>& In…Input (read-only)The abilities to grant
TArray<…>& Out… (non-const)Output pin, on the right sideThe handles you get back
UPARAM(ref) TArray<…>& In…Input, passed by reference, so changes reach the caller's variableThe handles to remove, then emptied

Without UPARAM(ref), Blueprint would treat the remove function's array as an output, and you couldn't plug the weapon's handles in. With it, InSpecHandlesToRemove.Empty() empties the array you actually passed in. Unreal's naming convention helps you read these: In prefix for inputs, Out for outputs.

Removing

void UWarriorAbilitySystemComponent::RemoveGrantedHeroWeaponAbilities(
        UPARAM(ref) TArray<FGameplayAbilitySpecHandle>& InSpecHandlesToRemove)
{
    if (InSpecHandlesToRemove.IsEmpty()) return;
    for (const FGameplayAbilitySpecHandle& SpecHandle : InSpecHandlesToRemove)
        if (SpecHandle.IsValid()) ClearAbility(SpecHandle);
    InSpecHandlesToRemove.Empty();
}

Does this hit the "list changes while looping" issue from the input lesson?

On the ability input page I flagged that OnAbilityInputPressed loops over the ability list while activating, and granting abilities changes that list. In your case it's fine: the equip ability grants the weapon abilities after the montage's gameplay event, a later frame, long after the input loop has finished. If a future ability granted something immediately on activation, that's when to collect handles first.

The T-pose fix (Blueprint, from your commit message)

Unequipping unlinks the axe anim layer. The main anim BP then falls back to its own default implementation of each layer. If that default is empty, there's no pose at all, and an empty pose is the skeleton's reference pose: T-pose. The fix: give ABP_HeroCharacter's default ArmedLocomotionState layer an unarmed pose so there's always something to fall back to. General rule: every layer slot needs a sensible default, because "no layer linked" is a real state.

Small things in your code

Quiz yourself

Why store the handles from GiveAbility on the weapon?

To remove exactly those grants on unequip. The handle identifies one specific grant.

Why remove by handle rather than by ability class?

The same class could be granted by more than one source. A handle is unambiguous.

In a UFUNCTION, what pin does a non-const TArray<…>& parameter become?

An output pin.

What does UPARAM(ref) do?

Makes a non-const reference parameter an input pin passed by reference, so the function can modify the caller's variable.

What does meta = (ApplyLevel = 1) do?

Gives the Blueprint pin for the ApplyLevel parameter a default value of 1.

Why Empty() the array after the loop instead of removing elements during it?

Modifying an array while looping over it is unsafe. Read everything first, then clear.

Why did unequipping cause a T-pose?

Unlinking the axe layer fell back to the main anim BP's default layer, which was empty, so no pose meant the reference (T) pose. The fix is a real default pose in that layer.

DynamicAbilityTags vs GetDynamicSpecSourceTags()?

Same purpose. DynamicAbilityTags is deprecated in UE 5.5+; use the getter.