A GameplayTagCollection is a container that holds a set of tags alongside their values. It is the object that represents the state of something in your game, the active effects on a character, the flags describing the current world state, the capabilities an object currently possesses, the conditions a trigger is monitoring. Any time you need a thing to “have” tags, a GameplayTagCollection is how you express that.
The collection is designed to be embedded wherever state needs to live. It serialises cleanly, fires change notifications automatically, and provides a full query surface on top of its contents. You rarely interact with the raw tag identifiers alone once you have a collection in play.
Tags Have Values, Not Just Presence #
Every tag in a collection is stored alongside a numeric value. The value is a 64-bit unsigned integer, though the collection exposes typed accessors so you can work with floats, signed integers, or doubles without thinking about the underlying storage.
When a tag is absent from the collection its value is zero. This is not just convention, it is the definition the whole system uses. Zero means not present. Any non-zero value means the tag is present, and that value carries whatever meaning your design gives it.
For the simplest case, presence and absence are all you need. Adding a tag sets its value to one. Removing it sets the value back to zero. The tag is either there or it isn’t, and that is enough for flags, states, and Boolean conditions.
For richer cases the value carries magnitude. A poison effect applied three times has a value of three. A charge mechanic accumulates toward a threshold. A resource tracks its current quantity. The same tag serves as both the identity of the concept and the measure of its current state, with no need for a separate field alongside it.
Arithmetic Operations #
Rather than reading a value and writing a modified version back, the collection accepts operations directly. An operation names a tag, names an arithmetic instruction, and names an operand. The collection applies the instruction to the tag’s current value and handles the zero-equals-absent rule automatically, so you never need to remember to remove a tag when arithmetic brings it to zero.
The available instructions cover the common cases for game state management. Set replaces the current value entirely. Add and Subtract accumulate or drain. Multiply and Divide scale. Min and Max enforce a floor or ceiling without conditional logic in calling code.
This matters most when multiple systems are independently modifying the same tag. A poison that adds to a stack count, a cure that subtracts, and an immunity that sets the value to zero regardless of current state can all operate on the same tag through the same interface without coordinating with each other.
Typed Value Access #
The underlying storage is always the same 64-bit integer, but the collection provides typed accessors so you can work with floats, signed integers, longs, and doubles in the way that makes sense for each tag’s role. A health value read as a float, a level read as an integer, a timer read as a double, all stored consistently and accessible without manual conversion.
There is one thing worth knowing about this. Because the storage is a single integer, the representation of a float or double is its raw bit pattern reinterpreted as a number. This is efficient and lossless, but it means a value written as a float and read back as an integer will not give you a sensible integer. The type of a tag’s value is a design choice you make and should apply consistently.
Tags as Enumerations #
There is a pattern worth calling out explicitly. A tag’s value can itself be the identifier of another tag. You can store Classes.Melee.Warrior as the value of Player.Class, effectively using the collection to map one tag to another.
This gives you an enumeration-style pattern that is fully integrated with the hierarchy system. A query that checks what value Player.Class holds can then ask whether that value is a descendant of Classes.Melee, matching Warriors, Berserkers, Paladins, or any other melee subclass without enumerating them. It is a compact way to model selection from a typed, hierarchical vocabulary.
Querying the Collection #
The collection provides a full set of query methods that work across its contents.
The foundation is Contains, which asks whether a given tag is present. By default this is hierarchy-aware, so asking whether the collection contains Effects.Debuff will return true if the collection holds Effects.Debuff.Poison, Effects.Debuff.Slow, or any other registered descendant. Switching to exact match asks only about that specific tag, ignoring the hierarchy entirely.
Building on that, ContainsAll asks whether every tag in a given set is present, ContainsAny asks whether at least one is, and ContainsNone asks whether none are. All three respect the same exact-match flag.
For set operations, GetShared returns the tags that two collections have in common, and GetExclusive returns the tags present in one but not the other. GetMatchingTags returns every tag in the collection that falls under a given parent tag, and GetExcludingTags returns every tag that does not.
Change Notifications #
The collection notifies interested parties when its contents change, and it does so at two levels of granularity.
The collection-level notification fires whenever anything changes. Any system that needs to know “something is different” without caring what specifically can subscribe at this level and react accordingly.
Per-tag subscriptions are more precise. A subscriber nominates a specific tag and receives a callback that includes the tag that changed, its previous value, and its new value. If the nominated tag has registered descendants, those changes bubble up too, unless the subscriber opts into exact-match-only notifications. This means a subscriber watching Effects.Debuff can choose to hear about every debuff change across the whole family, or only changes to that exact tag.
Subscriptions are managed on the collection directly. They activate and deactivate with the collection rather than requiring any global event bus or messaging infrastructure. The connection is as local as the collection itself.
Serialisation #
The collection serialises as a list of tag-and-value pairs. The tags are stored as their integer IDs, the values as their raw numeric form. No strings are included in the serialised representation.
On deserialisation, any entry with a value of zero is discarded, keeping the in-memory state consistent with the zero-equals-absent rule even if serialised data was written in a different version of the collection.
Because tag IDs are hashes of path strings, a serialised collection is stable across platforms and compiler versions. It is not stable across tag renames, for the same reason covered in the GameplayTag article. Renaming a tag changes its ID, and any previously serialised data referencing the old ID will not resolve to the renamed tag automatically.
