[UE5] Gameplay Tags: Taming an Ever-Growing Enemy Enum with Hierarchical Tags

Created: 2026-02-07Last updated: 2026-07-20

Why Enum-based classification breaks down, and how hierarchical tags like Enemy.Undead.Skeleton fix it, illustrated with a Blueprint-first approach. Covers adding tags in Project Settings, choosing between Has Tag / Has Any / Has All, and building holy water that never needs editing when you add enemies.

You managed enemy types with an Enum, and now every new enemy means opening that Enum again. Then you try to build holy water that only affects undead, and your Switch on Enum grows a pin for Skeleton, Zombie, and Ghoul, so every new enemy sends you back into the holy water Blueprint too.

For a small prototype that works fine. But the more types you add, the more "add one enemy" turns into "edit things all over the project." The tool that unwinds this is Gameplay Tags. This article covers classifying enemies with hierarchical labels like Enemy.Undead.Skeleton so that adding types never forces you to rewrite the matching logic, using a Blueprint-first approach.

A row of hierarchical tag labels with a subset of them marked together

What You'll Learn

  • A Gameplay Tag is a label with a hierarchy built from dots
  • The decisive difference from Enums and Names is matching a whole group by its parent tag
  • Adding tags in Project Settings, and managing them in DefaultGameplayTags.ini
  • When to use Has Tag / Has Any Tags / Has All Tags / Matches Tag
  • Hands-on: anti-undead holy water that needs no edits when you add enemies

Sponsored

Where Enums Break Down

Enums are a good fit for "a value from a fixed set of options." While you only have three enemy types, there is nothing wrong with them.

The problems arrive when the categories grow, and when you want to treat a group as one.

Left: lines from an Enum list converging and tangling into each piece of logic. Right: a single line running from one parent node of a tag hierarchy into the logic

Add Skeleton to E_EnemyType and every Blueprint using Switch on Enum grows a new pin. Holy water, holy magic, the necromancer's resurrection: you open all of them and rewire. Adding one enemy means touching assets that have nothing to do with that enemy.

Enums are also flat. There is no way to express an intermediate grouping like "undead in general," so you are stuck enumerating "Skeleton or Zombie or Ghoul" forever.

Managing them with Name (strings) makes additions free, but now typos go unnoticed until runtime. You will not remember whether you wrote "Undead" or "undead".

Gameplay Tags close both gaps. Additions are free, tags are picked from a dictionary so spelling cannot go wrong, and the hierarchy lets you handle groups at once.

A Gameplay Tag Is a Hierarchical Label

A Gameplay Tag is a label whose hierarchy is separated by . (dots).

A tree with Enemy at the top branching into Undead and Beast, with Skeleton, Zombie, and Ghoul beneath Undead. The entire Undead branch is circled
Enemy
├─ Enemy.Undead
│   ├─ Enemy.Undead.Skeleton
│   ├─ Enemy.Undead.Zombie
│   └─ Enemy.Undead.Ghoul
└─ Enemy.Beast
    ├─ Enemy.Beast.Wolf
    └─ Enemy.Beast.Bear

The decisive property is that a child tag also matches its parent tag. Query an enemy holding Enemy.Undead.Skeleton with Enemy.Undead and you get true. Query it with Enemy and you also get true.

That one-way relationship is what pays off. Logic that "affects undead" only needs to look at Enemy.Undead, and adding Enemy.Undead.Wraith later changes not a single character of that logic.

The reverse does not hold. Query an enemy that only holds Enemy.Undead with Enemy.Undead.Skeleton and you get false. Remember it as ask with the parent and you hit broadly; ask with the child and you hit narrowly.

Tags work for more than enemy types. They suit states and abilities too.

Use caseExample tag
Species / classificationEnemy.Undead.Skeleton
StateStatus.Debuff.Poison / Status.Stunned
Element / resistanceDamage.Type.Holy / Immune.Fire
Equipment classificationItem.Weapon.Sword.TwoHanded

Gameplay Tags are also a core piece of the Gameplay Ability System (GAS), but you can use them on their own without adopting GAS. This article does not assume GAS. If you're weighing whether to bring GAS in at all, Gameplay Ability System: Deciding Whether to Adopt It can help you decide.

Sponsored

Two Ways to Add Tags

Tags have to be registered in the dictionary before you use them. Unregistered tags never appear in Blueprint dropdowns.

Left: the Gameplay Tags screen in Project Settings with its tree and plus button. Right: text lines in DefaultGameplayTags.ini, with an arrow showing they refer to the same thing

Method 1: Add Them from Project Settings

This is the everyday route.

  1. Open EditProject SettingsGameplayTags under the Project category
  2. Click the + (Add New Gameplay Tag) under Gameplay Tags
  3. Enter Enemy.Undead.Skeleton in Name, as a full path
  4. Describe its purpose in Comment and confirm with Add New Tag

You do not need to create Enemy and Enemy.Undead first. Entering the full path creates the intermediate levels automatically.

Registered tags can be renamed, deleted, or given child tags from the right-click menu. Rename updates references for you, which is safer than editing the ini directly.

Method 2: Manage Them in the ini File

Tags you add are saved by default to Config/DefaultGameplayTags.ini in your project.

[/Script/GameplayTags.GameplayTagsSettings]
+GameplayTagList=(Tag="Enemy.Undead.Skeleton",DevComment="Bone enemy")
+GameplayTagList=(Tag="Enemy.Undead.Zombie",DevComment="Rotting enemy")
+GameplayTagList=(Tag="Enemy.Beast.Wolf",DevComment="Beast enemy")

Being text means version-control diffs are readable. You can also edit it directly with the editor closed.

Once the project gets big, switching Source when adding a tag in Project Settings lets you split tags into separate ini files per feature, which reduces conflicts from teammates editing the same lines.

You can also define tags in C++. Using UE_DECLARE_GAMEPLAY_TAG_EXTERN / UE_DEFINE_GAMEPLAY_TAG_COMMENT from NativeGameplayTags.h lets C++ code reference tags as variables, so misspellings surface at compile time. Doing so requires adding "GameplayTags" to PublicDependencyModuleNames in your Build.cs (see Your First Step from Blueprint to C++).

Choosing the Right Matching Node

There are two types you work with in Blueprint.

TypeContentsUse case
Gameplay TagOne tagThings with a single answer, like "this damage's element"
Gameplay Tag ContainerA set of tagsThings with several answers, like "the properties this enemy has"
Four matching nodes stacked vertically, each showing an input tag set on the left and a true/false result on the right

The matching node's name depends on whether the target is a container or a single tag.

NodeTargetWhat it checks
Has TagContainerWhether it holds the given tag or a child of it
Has Any TagsContainerWhether it holds at least one of the given tags
Has All TagsContainerWhether it holds all of the given tags
Matches TagSingle tagWhether that tag is the given tag or a child of it

Here it is with concrete values, for an enemy holding both Enemy.Undead.Skeleton and Status.Debuff.Poison.

QueryResult
Has Tag(Enemy.Undead)true (matches as a child tag)
Has Tag(Enemy.Beast)false
Has Any Tags(Enemy.Undead, Enemy.Beast)true (one match is enough)
Has All Tags(Enemy.Undead, Status.Debuff)true (both match)
Has All Tags(Enemy.Undead, Immune.Fire)false

"Affects X" is Has Tag, "affects X or Y" is Has Any Tags, and "only when X and Y" is Has All Tags.

Use the Exact variants for exact matching. Has Tag Exact ignores the hierarchy, so querying Enemy.Undead.Skeleton with Enemy.Undead returns false. It is the option when you need "skeletons only, not undead in general."

Empty containers have a trap. When the set of tags you pass in is empty, Has Any Tags returns false, but Has All Tags interprets it as "no tags are missing" and returns true. If you are feeding conditions in from data, test the empty case once.

Sponsored

Hands-On: Building Anti-Undead Holy Water

An elemental-weakness item in a roguelike, a purification item in a horror game, an anti-air-only tower in a tower defense. "Extra effective against one specific group" shows up regardless of genre. Here we build it so that adding enemy types never requires touching the holy water at all.

A scene showing thrown holy water hitting a skeleton and a zombie with large damage numbers and a wolf with a small one

Setup for reproducing this: create a new project from the Third Person template. Register these four tags in Project Settings ahead of time.

Enemy.Undead.Skeleton
Enemy.Undead.Zombie
Enemy.Beast.Wolf
Damage.Type.Holy
KindNameType / initial value
Blueprint Class (Actor)BP_EnemyBaseParent of the three enemies
BP_EnemyBase variableEnemyTagsGameplay Tag Container / empty. Turn on Instance Editable
BP_EnemyBase variableHealthFloat / 100.0
Child BlueprintBP_SkeletonEnemy.Undead.Skeleton as the default of EnemyTags
Child BlueprintBP_ZombieEnemy.Undead.Zombie as the default of EnemyTags
Child BlueprintBP_WolfEnemy.Beast.Wolf as the default of EnemyTags
Blueprint Class (Actor)BP_HolyWaterHas a Sphere Collision (Radius 200.0)
BP_HolyWater variableBaseDamageFloat / 20.0
BP_HolyWater variableHolyMultiplierFloat / 5.0
BP_HolyWater variableTargetTagGameplay Tag / default Enemy.Undead. Turn on Instance Editable

Step 1: create the enemy parent class. Right-click in the Content Browser → Blueprint ClassActor. Name it BP_EnemyBase.

Add two variables in the My Blueprint panel. For EnemyTags, choose Gameplay Tag Container in the type dropdown. Make Health a Float and set 100.0 as its Default Value in the Details panel. Checking Instance Editable on both lets you add tags per instance placed in a level.

Step 2: create three enemies. Right-click BP_EnemyBaseCreate Child Blueprint Class three times. Open each and pick a tag from the + under the Default Value of EnemyTags. Only registered tags appear in the dropdown, so you cannot misspell anything here.

Step 3: write the damage logic in the parent class. Add a custom event called ApplyHolyDamage to BP_EnemyBase with a Damage (Float) input. All it does is subtract Damage from Health and set it.

Step 4: build the holy water Blueprint. Create BP_HolyWater, add a Sphere Collision component, and set its Sphere Radius to 200.0. Set up the three variables as listed in the table above and build the following graph from On Component Begin Overlap.

A Blueprint node graph running left to right from On Component Begin Overlap through Cast, Has Tag, Branch, Multiply, and ApplyHolyDamage
BP_HolyWater
On Component Begin Overlap (Sphere)
  → Cast To BP_EnemyBase (Object: Other Actor)
      success → Get EnemyTags (from As BP Enemy Base)
          → Has Tag (Container: EnemyTags / Tag: TargetTag)
              → Branch
                  True  → Multiply (BaseDamage × HolyMultiplier) → ApplyHolyDamage (Target: As BP Enemy Base)
                  False → ApplyHolyDamage (Damage: BaseDamage, Target: As BP Enemy Base)

You find the Has Tag node by dragging off the EnemyTags pin and searching. Wire the TargetTag variable into the Tag input. You could hardcode a tag literal there, but making it a variable lets you change the target per holy water instance placed in the level (an anti-beast version is just swapping in Enemy.Beast).

Step 5: place them and check. Line up BP_Skeleton / BP_Zombie / BP_Wolf and drop BP_HolyWater in the middle. Adding a Print String inside ApplyHolyDamage to show the remaining HP makes verification easier (see checking values with Print String).

Press Play and let the enemies enter the holy water's radius. You should see these values.

EnemyDamage takenRemaining Health
BP_Skeleton100.0 (20 × 5)0.0
BP_Zombie100.00.0
BP_Wolf20.080.0

If only the wolf is left at 80, it works. If all three take 100 damage, suspect the tag wired into Has Tag; if all three take 20, suspect the EnemyTags settings.

Now for the real point. Create BP_Wraith as a child of BP_EnemyBase, add Enemy.Undead.Wraith in Project Settings, and set it on EnemyTags. Drop it in the level and press Play without opening BP_HolyWater, and the wraith takes 100 damage and dies. That is the decisive difference from an Enum.

Two things to take away.

  • Write your checks against the parent tag: what you pass to Has Tag is Enemy.Undead, not Enemy.Undead.Skeleton. Picking the tag at the level of "how broadly do I want this to apply" is what makes the design resilient to additions
  • Tags are data, checks are logic: the tags an enemy carries live in the level or the Blueprint defaults (data), while the tag the holy water looks at is a variable (configuration). Keeping type names out of your logic means expansion becomes a data-side job. Combine it with a Data Table and the enemy parameters move out too (see managing game data with Data Tables)
Sponsored

Bonus: Good to Know for Later

Gameplay Tag Containers can be modified at runtime. For state management, add Status.Debuff.Poison with Add Gameplay Tag when poisoned and Remove Gameplay Tag when it wears off. That keeps state in one place instead of growing a pile of bool variables like bIsPoisoned.

Order tag names from broad to specific. Putting the specific part first, as in Skeleton.Enemy.Undead, throws away the benefit of grouping by hierarchy. The rule is broad category on the left, more specific as you move right.

Do not go too deep, either. Build out Item.Weapon.Melee.Sword.OneHanded.Iron.Rusty and even you will not know which level to check against. Creating only the levels that correspond to actual "I want to handle these together" groupings is enough.

Before adding a tag, ask whether you really need one. If there are only two values and no prospect of more, a bool is fine. Gameplay Tags pay off only when the categories are likely to grow and you want to handle groups together.

Lyra is a good reference for large-scale usage. Epic's official Lyra Sample Game organizes tags into per-category headers defined in C++. It is a big project though, so there is no need to copy it wholesale for solo development.


Summary

EnumName (string)Gameplay Tag
Ease of addingDefinition changes ripple everywhereFreeFree
MisspellingImpossibleUnnoticed until runtimeImpossible, chosen from a dropdown
Group matchingOnly by enumeratingNot possibleOne parent tag does it
  • A Gameplay Tag is a label with a hierarchy built from dots, usable from Blueprint alone with no C++
  • Child tags match parent tags. That one-way relationship is what makes it survive additions
  • Add them from Project Settings. The actual storage is text in Config/DefaultGameplayTags.ini
  • Containers use Has Tag / Has Any Tags / Has All Tags; single tags use Matches Tag
  • Write your checks against the tag at the level you want the effect to reach

Once you have organized your types with tags, the next step is moving the per-type numbers out into data. Managing per-enemy HP and rewards together is covered in managing game data with Data Tables, and swapping behavior per type is covered in the Blueprint Interface article.

Which Blueprint in your project currently has the longest Switch on Enum?