Ability input: press a button, activate by tag
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
- You press E. Enhanced Input fires
IA_EquipAxe, Started. - The binding calls
Input_AbilityInputPressed(InputTag.EquipAxe)on the character, with the tag passed along. - The character forwards it:
WarriorAbilitySystemComponent->OnAbilityInputPressed(Tag). - The ASC loops over its granted abilities, finds the one stamped
InputTag.EquipAxe, and callsTryActivateAbility. GA_Hero_EquipAxeruns (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);
}
}
- The extra argument at the end (
Config.InputTag) is a payload. Enhanced Input stores it with the binding and passes it to your function every time it fires. That's whyInput_AbilityInputPressedtakes anFGameplayTag: one function handles every ability button, and the tag says which one. Started= pressed,Completed= released. Released does nothing yet; it's there for "hold to charge" or "hold to block" abilities later.- One
CallbackFunctype for both works because pressed and released have identical signatures. If they differed, you'd need two template parameters. Config.IsValid()(the struct function from your last question) would do the same as the hand-written check in theif.
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);
}
}
- This is the
virtualoverride the startup data page predicted: the hero version callsSuper::for the shared lists, then adds the hero-only, input-bound abilities. - Dynamic spec source tags are tags on this particular grant of the ability, not on the ability class. The same
ability class could be granted twice with two different input tags. (Older tutorials call it
DynamicAbilityTags; UE 5.5+ renamed it.) - The struct moved into
WarriorStructTypes.hin7a3d5fc, a shared home for small data types that several classes use, so no class header has to include another just for a struct.
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);
}
}
HasTagExact, notHasTag:InputTag.EquipAxemust not also match a parent likeInputTag(tags page).const FGameplayTag InInputTagby value: a gameplay tag is tiny (one name), so copying is fine.conston a value parameter only stops the function from changing its own copy. It's not a promise to the caller.TryActivateAbility, "try": activation can fail (on cooldown, blocked by a tag, can't afford the cost), and GAS checks all that for you.
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.