Gameplay Tags in GAS¶
If GAS has a lingua franca, it's Gameplay Tags. They're how abilities know whether they can activate, how effects decide whether to apply, how cues know what to show, and how you query an actor's current state. Understanding tags is understanding the language that every GAS component speaks.
What Tags Are¶
A Gameplay Tag is a hierarchical, dot-separated label. That's it — just a name.
Under the hood, each tag is registered in a global tag dictionary and compared by hash, which makes tag operations fast — comparable to integer comparisons, not string comparisons. You can use tags liberally without worrying about performance.
Tags are not booleans. They're not enums. They're a structured namespace that GAS can query hierarchically.
Tag Hierarchy and Parent Matching¶
This is the feature that makes tags powerful: checking for a parent tag matches all of its children.
Given these tags on an actor:
These checks all return true:
| Query | Result | Why |
|---|---|---|
CrowdControl.Hard.Stun |
Match | Exact match |
CrowdControl.Hard |
Match | Parent of CrowdControl.Hard.Stun |
CrowdControl |
Match | Parent of both tags |
CrowdControl.Soft.Slow |
Match | Exact match |
CrowdControl.Hard.Freeze |
No match | Not present |
Damage.Type.Fire |
No match | Not present |
This is enormously useful. An ability that should be blocked by any hard crowd control just checks for CrowdControl.Hard — it automatically blocks on Stun, Freeze, Fear, or any future hard CC you add. No code changes needed.
MatchesTag vs MatchesTagExact¶
GAS provides two matching functions, and choosing the right one matters:
// MatchesTag — hierarchical matching (parent matches children)
Tag.MatchesTag(OtherTag);
// MatchesTagExact — exact match only (no hierarchy)
Tag.MatchesTagExact(OtherTag);
Use MatchesTag (the default) when you want hierarchical matching — which is most of the time. Checking for CrowdControl.Hard should match CrowdControl.Hard.Stun.
Use MatchesTagExact when you specifically need to distinguish between a parent and its children. For example, if you have logic that should only fire for CrowdControl.Hard.Stun and not for CrowdControl.Hard.Freeze, use exact matching.
Direction matters
A.MatchesTag(B) checks if A is the same as or a child of B. So CrowdControl.Hard.Stun.MatchesTag(CrowdControl.Hard) is true, but CrowdControl.Hard.MatchesTag(CrowdControl.Hard.Stun) is false. The tag on the left is the one being tested; the tag on the right is the "filter."
Tag Containers¶
In practice, actors don't hold a single tag — they hold a set of tags in an FGameplayTagContainer:
FGameplayTagContainer TagContainer;
TagContainer.AddTag(FGameplayTag::RequestGameplayTag(FName("State.InCombat")));
TagContainer.AddTag(FGameplayTag::RequestGameplayTag(FName("CrowdControl.Soft.Slow")));
You can query containers with:
// Does the container have ANY tag matching this one?
bool bHasCC = TagContainer.HasTag(CrowdControlTag); // Hierarchical
// Does the container have ALL of these tags?
bool bHasAll = TagContainer.HasAll(RequiredTags);
// Does the container have ANY of these tags?
bool bHasAny = TagContainer.HasAny(BlockedTags);
GAS uses these container queries extensively — ability activation checks, effect application requirements, and tag-based filtering all use HasTag, HasAll, and HasAny on the ASC's current tag container.
Tag Counts¶
Here's something that surprises people: GAS tracks tags by count, not by presence/absence.
When an effect grants the tag CrowdControl.Hard.Stun, the ASC increments the count for that tag. When the effect ends, it decrements the count. The tag is considered "present" as long as the count is greater than zero.
Why does this matter? Because multiple sources can grant the same tag:
Stun effect A applies → Stun count = 1 (tag is present)
Stun effect B applies → Stun count = 2 (tag is still present)
Stun effect A expires → Stun count = 1 (tag is STILL present!)
Stun effect B expires → Stun count = 0 (tag is gone)
If tags were simple booleans, removing one stun would un-stun the character even though another stun is still active. Tag counts prevent this.
Loose Tags¶
You can also add tags manually (outside of effects) using "loose" tags:
// Add a loose tag (increments count)
ASC->AddLooseGameplayTag(MyTag);
// Remove a loose tag (decrements count)
ASC->RemoveLooseGameplayTag(MyTag);
// Check current count
int32 Count = ASC->GetTagCount(MyTag);
Loose tags follow the same counting rules. If an effect granted a tag and you also added it as a loose tag, the count is 2 — both need to be removed for the tag to disappear.
Balance your adds and removes
Every AddLooseGameplayTag must have a corresponding RemoveLooseGameplayTag. If you add a tag in BeginPlay but forget to remove it, it will persist forever. Effect-granted tags handle this automatically — loose tags are your responsibility.
Responding to Tag Changes¶
Often you need to react when a tag is added or removed. GAS provides two mechanisms:
Tag Change Delegates¶
Register a delegate on the ASC to be notified when a specific tag's count changes:
ASC->RegisterGameplayTagEvent(
StunTag,
EGameplayTagEventType::NewOrRemoved // Only fires when count goes 0→1 or 1→0
).AddUObject(this, &AMyCharacter::OnStunTagChanged);
void AMyCharacter::OnStunTagChanged(const FGameplayTag Tag, int32 NewCount)
{
if (NewCount > 0)
{
// Stun started — disable input, play stun animation
}
else
{
// Stun ended — re-enable input
}
}
The EGameplayTagEventType controls when the delegate fires:
NewOrRemoved— fires only on transitions: tag goes from absent to present (0 to 1+) or present to absent (1+ to 0). This is usually what you want.AnyCountChange— fires on every increment/decrement. Use this if you need to track stacks.
How Tags Are Registered¶
Tags must be registered before they can be used. There are several ways:
Project Settings (Editor Tag Manager)¶
The most common method. Open Project Settings > GameplayTags and add tags directly. These are stored in your project's DefaultGameplayTags.ini.
DataTables¶
Create a DataTable with row type GameplayTagTableRow and populate it with tags. Reference the DataTable in Project Settings under GameplayTags > Gameplay Tag Table List.
Native Code¶
Declare tags in C++ using UE_DECLARE_GAMEPLAY_TAG_EXTERN / UE_DEFINE_GAMEPLAY_TAG:
// Header
UE_DECLARE_GAMEPLAY_TAG_EXTERN(TAG_State_Dead);
// Source
UE_DEFINE_GAMEPLAY_TAG(TAG_State_Dead, "State.Dead");
Native tags are available immediately at startup with zero lookup cost. Use this for tags you reference frequently in C++ — it avoids the RequestGameplayTag(FName("...")) pattern that can fail silently if you typo the string.
Plugin .ini Files¶
Plugins can register their own tags by including a Config/Tags/ directory with .ini files in the standard GameplayTags format:
[/Script/GameplayTags.GameplayTagsSettings]
+GameplayTagList=(Tag="MyPlugin.Status.Burning",DevComment="Applied when the actor is on fire")
+GameplayTagList=(Tag="MyPlugin.Status.Frozen",DevComment="Applied when the actor is frozen")
These are loaded automatically when the plugin loads. This is how you ship reusable GAS functionality in plugins without requiring users to manually add your tags.
Tag Design¶
Designing a good tag hierarchy is one of the most impactful decisions in a GAS project. A well-designed hierarchy lets you write broad queries (CrowdControl.Hard matches any hard CC) and keeps your tag namespace manageable as the project grows.
The key principles:
- Design for queries, not just labels — think about what you'll check for, not just what you'll grant
- Use hierarchy for "is-a" relationships —
Damage.Type.Fireis aDamage.Type - Keep it shallow enough to be useful — 3-4 levels is usually the sweet spot
- Separate concerns —
Ability.*for ability identity,State.*for actor state,CrowdControl.*for CC types
For the complete tag design guide with naming conventions, common hierarchies, and anti-patterns, see Tag Architecture.
Design Anti-Patterns¶
Watch out for "broken hierarchies" where specific and general are in the wrong order:
| Pattern | Problem | Better |
|---|---|---|
Item.Apple.Heal |
Can't query "all healing items" without enumerating every item | Item.Heal.Apple |
Ability.Fireball.Damage |
Can't query "all damage abilities" broadly | Ability.Damage.Fireball |
Status.Poison.Nature |
Mixes the effect (poison) with the element (nature) | Status.DoT.Poison + Damage.Type.Nature |
The rule: put the thing you'll query for closest to the root. If you'll often check "is this a healing item?", then Heal should be higher in the hierarchy than Apple.
GameplayTagResponseTable¶
UGameplayTagReponseTable (yes, the typo is in the engine — "Reponse" not "Response") is a data-driven table that automatically applies or removes Gameplay Effects in response to tag count changes. Instead of writing code that says "when stunned, apply the stun visual effect," you define that relationship in a data asset. The table handles the rest.
This is one of GAS's most underused features. It lets you decouple "what happens when a status is applied" from the code that applies the status. Every stun ability doesn't need to know about the slow effect, the visual effect, and the input blocking effect — the response table handles all of that centrally.
How It Works¶
The table is a UDataAsset with an array of FGameplayTagResponseTableEntry entries. Each entry has two sides:
- Positive — a tag and associated Gameplay Effects. When the net count is positive, these effects are applied.
- Negative — a tag and associated Gameplay Effects. When the net count is negative, these effects are applied.
The "positive" and "negative" naming is a bit misleading. Think of it as a tug-of-war: positive tags pull the count up, negative tags pull it down. The table applies the appropriate effects based on which side is "winning."
USTRUCT()
struct FGameplayTagResponseTableEntry
{
/** Tags that count as "positive" toward the final response count.
* If the overall count is positive, these ResponseGameplayEffects are applied. */
FGameplayTagReponsePair Positive;
/** Tags that count as "negative" toward the final response count.
* If the overall count is negative, these ResponseGameplayEffects are applied. */
FGameplayTagReponsePair Negative;
};
Each side (FGameplayTagReponsePair) contains:
| Field | Type | Purpose |
|---|---|---|
Tag |
FGameplayTag |
The tag to monitor on the ASC |
ResponseGameplayEffects |
TArray<TSubclassOf<UGameplayEffect>> |
Effects to apply when this side's count "wins" |
SoftCountCap |
int32 |
Maximum effective count for this side (0 = no cap) |
When the tag count changes, the table recalculates the net count and applies or removes effects accordingly. If multiple stacks of the same response tag exist, the table applies multiple levels of the response effect (using AddOrUpdate internally).
Setting It Up¶
1. Create the data asset:
In the Content Browser, right-click and create a new Data Asset of type GameplayTagReponseTable.
2. Configure entries:
Each entry maps tag changes to effect responses. For the common "one-sided" use case (tag added = apply effect, tag removed = remove effect), you only fill in the Positive side:
| Positive Tag | Response Effects | What Happens |
|---|---|---|
CrowdControl.Hard.Stun |
GE_StunVisual, GE_StunMovementBlock |
When stunned: particles + movement lock. When stun ends: both removed. |
State.InCombat |
GE_CombatRegenBlock |
Entering combat stops health regen. Leaving combat restores it. |
Status.DamageOverTime.Burn |
GE_BurnVisual |
Burning plays fire particles. When burn ends, particles stop. |
For the "opposing forces" pattern, fill in both sides:
| Positive Tag | Positive Effects | Negative Tag | Negative Effects |
|---|---|---|---|
Status.Haste |
GE_SpeedBuff |
Status.Slow |
GE_SpeedDebuff |
In this setup, if an actor has 2 stacks of Haste and 1 stack of Slow, the net count is +1 and GE_SpeedBuff is applied at level 1. If Slow overtakes Haste, GE_SpeedDebuff kicks in instead. This is great for "buff vs debuff" mechanics.
3. Register it with the ASC:
Call RegisterResponseForEvents on the ASC, typically during initialization:
void AMyCharacter::BeginPlay()
{
Super::BeginPlay();
if (UAbilitySystemComponent* ASC = GetAbilitySystemComponent())
{
if (TagResponseTable)
{
TagResponseTable->RegisterResponseForEvents(ASC);
}
}
}
This binds the table to tag change events on that ASC. The table internally calls RegisterGameplayTagEvent for each tag it cares about and handles the apply/remove logic in its TagResponseEvent callback.
Register on every ASC
RegisterResponseForEvents must be called on each ASC that should use the table. If you want all characters to respond, call it in your character base class. The table stores weak references to registered ASCs and cleans up stale entries periodically.
Practical Examples¶
Status effect visuals without coupling:
Instead of every stun ability manually applying GE_StunVisual:
// Response Table entry:
Positive.Tag = CrowdControl.Hard.Stun
Positive.ResponseGameplayEffects = [GE_StunVisual, GE_StunInputBlock]
Now any effect or ability that grants the CrowdControl.Hard.Stun tag automatically triggers the visual and input block. New stun abilities don't need to know about the visual system at all.
Combat state management:
// Response Table entry:
Positive.Tag = State.InCombat
Positive.ResponseGameplayEffects = [GE_BlockPassiveRegen, GE_CombatMusicTrigger]
Entering combat automatically stops passive regen and triggers combat music. Any system that adds State.InCombat (proximity detection, taking damage, using an offensive ability) gets this behavior for free.
Opposing buff/debuff:
// Response Table entry:
Positive.Tag = Status.Haste
Positive.ResponseGameplayEffects = [GE_HasteSpeedBuff]
Negative.Tag = Status.Slow
Negative.ResponseGameplayEffects = [GE_SlowSpeedDebuff]
The table automatically resolves which side is dominant based on stack counts.
The SoftCountCap¶
SoftCountCap limits how high the effective count can go. If you set it to 3, then even if the tag has 10 stacks, the response table treats the count as 3 for purposes of calculating how many levels of the response effect to apply. This prevents runaway stacking — you can have 10 sources of Haste but cap the speed buff at 3 levels.
Set to 0 (the default) for no cap.
Limitations¶
- Tag-based only — the table reacts to tag count changes, not to specific effects. You can't say "when GE_PoisonDagger applies, do X." You say "when the Poison tag changes, do X."
- No fine-grained control — you can't differentiate why a tag was added. Whether the stun came from a boss ability or a player ability, the response is the same.
- Effect responses are applied as new effects — they go through the normal application pipeline, including tag requirements and immunity checks. This is usually what you want, but be careful of circular dependencies (response effect grants a tag that triggers another response).
- Weak reference cleanup — the table periodically cleans up stale ASC references, but during that window, the internal map may hold references to destroyed components. This is handled safely via
TWeakObjectPtrbut worth knowing about.
Under the Hood: FNames and Performance¶
Source
This section draws from GameplayTags and FNames In-Depth by itsBaffled -- an excellent deep dive into the internals.
FGameplayTag is a thin wrapper around a single FName that stores the full dot-separated string (e.g., "Weapon.AR.AK47"). Understanding FNames helps you understand tag performance.
FName Comparison is Extremely Fast¶
An FName is 8 bytes -- two uint32 values (ComparisonIndex + Number). Equality comparison copies those 8 bytes into a uint64 and does a single integer comparison. This is effectively free.
Tags inherit this performance. Comparing two FGameplayTag values is an FName comparison -- two integers, not a string compare.
Construction from a string is more expensive (involves hashing into the global FNamePool), but you typically construct tags once at startup and compare them at runtime. Copy construction (passing tags around) is trivial -- it's just copying 8 bytes.
FGameplayTagContainer Internals¶
A container holds two arrays:
GameplayTags-- the actual tags you addedParentTags-- automatically derived from the hierarchy
For tag Weapon.AR.AK47, ParentTags automatically includes Weapon and Weapon.AR. This is why HasTag("Weapon") can match Weapon.AR.AK47 without traversing the hierarchy at query time -- the parent tags are pre-computed when tags are added to the container.
Key Limitations¶
- 65,535 tags maximum per project
- FName indices are NOT stable across engine launches -- never serialize or network raw
ComparisonIndexvalues. Unreal sends FNames as full strings over the network. - GameplayTags do NOT require GAS -- they're a standalone framework usable in any UE project
Tag Replication Optimization¶
When FastReplication is enabled in DefaultGameplayTags.ini, tags replicate via a global index instead of full strings. All clients and server must agree on the tag dictionary.
Key settings for tuning replication bandwidth:
| Setting | What It Controls | Default |
|---|---|---|
CommonlyReplicatedTags |
Tags in this array get lower indices, costing fewer bits on the wire | Empty |
NetIndexFirstBitSegment |
Minimum bits always transmitted. Common tags cost this + 1 flag bit, uncommon tags cost more. | 16 |
NumBitsForContainerSize |
Max tags per container = 2^n - 1 | 6 (= 63 tags max) |
Example: If you have 255 tags (8 bits needed) but 95% of network traffic uses the lower 32 (5 bits), set NetIndexFirstBitSegment to 5. Common tags now cost 6 bits (5 + 1 flag) instead of 8.
Use the console command GameplayTags.PrintReplicationFrequencyReport to generate statistics and ready-to-paste INI entries for your most commonly replicated tags.
Editor Tips¶
Filtering Tag Dropdowns¶
Use the Categories meta specifier on UPROPERTY to filter the tag picker dropdown in the editor:
// Only shows tags under Weapon.AR and Weapon.SMG
UPROPERTY(EditDefaultsOnly, meta=(Categories="Weapon.AR,Weapon.SMG"))
FGameplayTag WeaponTag;
This is invaluable on large projects where the full tag list is overwhelming. Designers only see the tags relevant to their property.
Further Reading¶
- Tag Architecture -- the complete tag design guide with the 21-namespace starter preset
- GameplayTags and FNames In-Depth -- deep dive into internals and replication
- Replication Modes -- how tag replication interacts with ASC replication modes