Weapon abilities: grant on equip, remove on unequip
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
- Equip (
GA_Hero_EquipAxe): readHeroWeaponData.DefaultWeaponAbilitiesfrom the axe →GrantHeroWeaponAbilities→ get back a list of handles →AssignGrantedAbilitySpecHandlesstores them on the axe. - While equipped, pressing an attack button finds those abilities by input tag (ability input).
- Unequip (
GA_Hero_UnequipAxe):GetGrantAbilitySpecHandlesfrom the axe →RemoveGrantedHeroWeaponAbilitiesclears 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));
GiveAbilityreturns a handle, the ID of that one grant. Startup abilities ignored it because they're never removed. Weapon abilities must be removed later, so this time you keep it.- Why handles, not ability classes? "Remove the light attack class" is ambiguous if two things granted it. A handle identifies this exact grant.
meta = (ApplyLevel = 1): a default value for the Blueprint pin. Blueprint reads metadata named after a parameter as that pin's default. (A C++ default,int32 ApplyLevel = 1, also works and helps C++ callers too.)
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++ parameter | Blueprint pin | Used here for |
|---|---|---|
const TArray<…>& In… | Input (read-only) | The abilities to grant |
TArray<…>& Out… (non-const) | Output pin, on the right side | The handles you get back |
UPARAM(ref) TArray<…>& In… | Input, passed by reference, so changes reach the caller's variable | The 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();
}
ClearAbility(Handle): the same call theOnGivenpolicy uses to remove one-shot abilities (activation policy).- Empty after the loop, not during. The loop only reads the array, so it's safe; changing an array while looping over it is the TArray trap.
- Emptying matters: stale handles left on the weapon would be "removed" again on the next unequip. That's harmless
here thanks to the
IsValidcheck, but untidy.
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.
- Two spellings of the same thing: this function uses
AbilitySpec.DynamicAbilityTags.AddTag(...), while the hero startup data usesGetDynamicSpecSourceTags().AddTag(...). In UE 5.5+DynamicAbilityTagsis deprecated (look for a deprecation warning in your build output). Use the getter in both places so they can't drift apart. - The spec-building loop now exists twice (startup data and here). A small shared helper, e.g. one function that
builds and gives a spec from an
FWarriorHeroAbilitySet, would keep them identical. The deprecation above is exactly the kind of drift that duplication causes. GetGrantAbilitySpecHandlesis missing a"ed"(Granted), and has a space before().AddUniqueis redundant (every new grant has a new handle), soAddis fine. Harmless.
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.