Ability input: press a button, activate by tag

Commit 403f1fc (+ 7a3d5fc) · BindAbilityInputAction, FWarriorHeroAbilitySet, OnAbilityInputPressed · 10 min

In one breath

Every ability button gets an input tag. When abilities are granted, each one is stamped with its input tag. When a button is pressed, the ASC looks for abilities stamped with that tag and activates them. The character never needs to know which abilities exist. Press E, and whatever ability carries InputTag.EquipAxe runs.

The whole path, one press

  1. You press E. Enhanced Input fires IA_EquipAxe, Started.
  2. The binding calls Input_AbilityInputPressed(InputTag.EquipAxe) on the character, with the tag passed along.
  3. The character forwards it: WarriorAbilitySystemComponent->OnAbilityInputPressed(Tag).
  4. The ASC loops over its granted abilities, finds the one stamped InputTag.EquipAxe, and calls TryActivateAbility.
  5. GA_Hero_EquipAxe runs (next lesson).

1. Config: a second list of input actions

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, meta = (TitleProperty = "InputTag"))
TArray<FWarriorInputActionConfig> AbilityInputActions;   // alongside NativeInputActions

Native actions (move, look) are bound one by one to specific C++ functions. Ability actions are all bound the same way, to "activate whatever has this tag". That's why they're in a separate list.

2. Binding with a payload

template <class UserObject, typename CallbackFunc>
void BindAbilityInputAction(const UDataAsset_InputConfig* InInputConfig, UserObject* ContextObject,
                            CallbackFunc InputPressedFunc, CallbackFunc InputReleasedFunc)
{
    for (const FWarriorInputActionConfig& Config : InInputConfig->AbilityInputActions)
    {
        if (!Config.InputAction || !Config.InputTag.IsValid()) continue;
        BindAction(Config.InputAction, ETriggerEvent::Started,   ContextObject, InputPressedFunc,  Config.InputTag);
        BindAction(Config.InputAction, ETriggerEvent::Completed, ContextObject, InputReleasedFunc, Config.InputTag);
    }
}

3. Granting: stamping the tag onto the ability

USTRUCT(BlueprintType)
struct FWarriorHeroAbilitySet
{
    UPROPERTY(EditDefaultsOnly, meta=(categories = "InputTag")) FGameplayTag InputTag;
    UPROPERTY(EditDefaultsOnly) TSubclassOf<UWarriorGameplayAbility> AbilityToGrant;
    bool IsValid() const;
};

void UDataAsset_HeroStartupData::GiveToAbilitySystemComponent(UWarriorAbilitySystemComponent* InASCToGive, int32 ApplyLevel)
{
    Super::GiveToAbilitySystemComponent(InASCToGive, ApplyLevel);   // the shared lists first
    for (const FWarriorHeroAbilitySet& AbilitySet : HeroStartupAbilitySets)
    {
        if (!AbilitySet.IsValid()) continue;
        FGameplayAbilitySpec AbilitySpec(AbilitySet.AbilityToGrant);
        AbilitySpec.SourceObject = InASCToGive->GetAvatarActor();
        AbilitySpec.Level = ApplyLevel;
        AbilitySpec.GetDynamicSpecSourceTags().AddTag(AbilitySet.InputTag);   // the stamp
        InASCToGive->GiveAbility(AbilitySpec);
    }
}

4. The ASC finds and activates

void UWarriorAbilitySystemComponent::OnAbilityInputPressed(const FGameplayTag InInputTag)
{
    if (!InInputTag.IsValid()) return;
    for (const FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
    {
        if (!AbilitySpec.GetDynamicSpecSourceTags().HasTagExact(InInputTag)) continue;
        TryActivateAbility(AbilitySpec.Handle);
    }
}
Worth knowing: changing a list while looping over it

The loop walks GetActivatableAbilities() and activates abilities inside it. If an activated ability ever grants or removes abilities (equipping a weapon that grants its attacks is the obvious one), that's the list you're looping over being changed under you, the same family of problem as the TArray reference bug. GAS has internal locking to defer some of this, but Epic's Lyra plays it safe: first collect the matching handles into a local array, then activate them in a second loop. A good pattern to recognise.

Quiz yourself

Trace one press of E from the keyboard to the ability running.

IA_EquipAxe Started → bound callback Input_AbilityInputPressed(InputTag.EquipAxe) → ASC->OnAbilityInputPressed(tag) → loop finds the spec stamped with that tag → TryActivateAbility → GA_Hero_EquipAxe runs.

What is the payload argument in BindAction(..., InputPressedFunc, Config.InputTag)?

An extra value Enhanced Input stores with the binding and passes to the callback each time. It's how one function handles every ability button and still knows which one fired.

Started vs Completed?

Started fires once when the input begins (pressed). Completed fires when it ends (released).

What are dynamic spec source tags, and why put the input tag there instead of on the ability class?

Tags on one specific grant of an ability. The same ability class could be granted with different input tags.

Why HasTagExact rather than HasTag?

HasTag also matches parent tags, so a broad tag could activate the wrong ability. Exact matching avoids that.

Why TryActivateAbility and not "ActivateAbility"?

Activation can fail (cooldown, cost, blocking tags). GAS checks the rules and only activates if they pass.

What's risky about activating abilities inside a loop over GetActivatableAbilities(), and the safe pattern?

An activated ability could grant or remove abilities, changing the list mid-loop. Safer: collect the matching handles first, then activate in a second loop (what Lyra does).

What does const mean on a by-value parameter like const FGameplayTag InInputTag?

Only that the function won't modify its own local copy. The caller's tag was copied, so it's never affected either way.