If you’ve worked with enumerations, you already understand the basic problem a GameplayTag solves.
You want to label things states, effects, abilities, factions and you want to check whether a given thing has a given label.
Enumerations do that, but they stop being comfortable the moment your design gets complex. You end up with hundreds of values in a flat list, no way to group related concepts, and every system that needs to check for “any kind of debuff” having to enumerate every debuff type individually. Add a new debuff and you’re hunting down every one of those checks to update it.
A GameplayTag is a named identifier that solves the grouping problem by making hierarchy a first-class feature. Tags are written in a dot-separated path format Effects.Debuff.Poison, Player.State.Stunned, Ability.Active.Sprint where every segment of the path is itself a meaningful node. The system understands that Effects.Debuff.Poison lives beneath Effects.Debuff, which lives beneath Effects. That relationship isn’t just naming convention; it’s structural, and it’s what makes queries so powerful.
What a Tag Actually Is #
At runtime a GameplayTag is a single 64-bit integer the hash of its dot-path string. The string itself isn’t stored anywhere on the tag. It gets hashed once when the tag is registered or first referenced, and from that point forward the system works entirely with the number.
This matters for performance. Comparing two tags is an integer comparison. Storing a tag is storing a number. Using a tag as a key in a lookup table is hashing an integer. None of this involves string memory, string allocation, or character-by-character comparison. The human-readable name is always recoverable from the registry when you need it in a log, in a debug display, in an editor tool but the game logic never pays for it.
Writing Tag Paths #
Tag paths use dot-separated segments where each segment contains only letters, digits, and underscores. No spaces, no special characters, no empty gaps between dots.
Casing is preserved and significant Effects.Debuff and effects.debuff hash to different values and are treated as completely different tags. Most teams pick a convention (title case per segment is common) and stick to it across the project.
Depth is flexible. A tag can be as shallow as a single word or as deep as the taxonomy demands. Two-level paths work fine for simple projects. Deeper hierarchies make sense when you have rich categorization a crafting system might naturally reach Items.Equipment.Weapons.Melee.Swords.OneHanded without that depth causing any friction at runtime.
The Hierarchy Is Structural #
This is the part worth really internalizing. When you register Effects.Debuff.Poison, the system doesn’t just record that tag it records Effects, Effects.Debuff, and Effects.Debuff.Poison as distinct related nodes, and it knows the ancestry chain between them. You don’t declare the parent nodes separately. They come into existence automatically as part of registering any child.
What this means in practice is that a check for Effects.Debuff can match Effects.Debuff.Poison, Effects.Debuff.Slow, Effects.Debuff.Burn, or anything else nested beneath it without any of those specific tags being listed in the check. Add a new debuff type to your library and every existing broad query catches it automatically. The system grows with your design instead of fighting it.
Validity and the Zero Tag #
Every tag implementation reserves the zero ID as the “no tag” value an unset or null tag. This is what you get from an uninitialized tag field, and it’s distinct from any real tag because no valid path hashes to zero.
The IsValid check is just asking whether the ID is non-zero. It’s instantaneous and has no overhead. You’ll use it often any time you have a tag field that might not have been set yet, checking IsValid before acting on it is good practice.
One nuance worth knowing: a tag can be valid (non-zero ID) without being registered in the library. If something hands you a tag ID from an external source a save file, a network message, a mod the ID might not correspond to any path the registry knows about. IsValid won’t catch that; it only tells you the number is non-zero. Whether a tag is recognized by the system is a registry question, separate from whether the tag value itself is meaningful.
Asking About Relationships #
A tag can be asked directly whether it is a descendant or ancestor of another tag. These aren’t string comparisons on the path they’re lookups into the precomputed ancestry data that the registry maintains.
Checking whether a tag is a child of another tag answers at any depth. Effects.Debuff.Poison.DoT is a child of Effects.Debuff.Poison, and also of Effects.Debuff, and also of Effects. The inverse checking whether a tag is a parent of another works the same way in the other direction.
These relationship queries only return meaningful results when both tags are registered. If the ancestry map doesn’t have a record of the relationship, the answer is false even if the path strings would seem to imply it.
Names and Display #
The registry keeps a table that maps integer IDs back to their dot-path strings. Any tag can return its name for display purposes useful in editor tooling, debug overlays, log messages, and anywhere a human needs to read what a tag is rather than just what number it is.
The name is always the complete path, not just the final segment. A tag for Effects.Debuff.Poison returns that full string, not Poison in isolation.
If a tag’s ID isn’t in the registry an unregistered tag from an external source, for example the name lookup returns nothing. Only the numeric ID is known.
Renaming Tags Is a Migration #
Because a tag is stored as a hash of its path, renaming a tag in your library changes its ID. Anything that was serialized under the old name saved games, configuration files, prefab data, network messages holds the old hash. Once the name changes, that hash no longer maps to the renamed path.
This is the same problem that comes with renaming an enumeration value, and the solution is the same: treat renames as data migrations. Either update serialized data to the new ID, or keep the old path as an alias during a transition period. For a game in active production with save data in the wild, this is the one area where tag management needs deliberate care.
Examples #
Coming Soon
