Gameplay · guide
Gameplay tags
One hierarchical, interned tag type, numbered so that "is this under Damage.Fire" is two integer comparisons.
Edit this page on GitHubDocuments
What it is#
A gameplay tag is a dotted name — Damage.Fire.Burn, Creature.Undead.Skeleton,
State.InCombat — held as four bytes. A GameplayTagTable is every tag a build knows, baked into a
tree and numbered by a pre-order walk, so a tag's descendants occupy a contiguous range of numbers.
That numbering is the whole design. A GameplayTagRange is one tag and everything beneath it, and
asking whether a tag falls under a prefix is index >= start && index < end.
What it is for#
Rules a designer writes at the altitude they mean them. *Fire resistance reduces Damage.Fire.*.*
*Immune to control blocks Effect.Control.*.* *This quest counts Creature.Undead.*.*
Requirements, immunities, loot conditions, quest objectives, effect stacking, chat gating,
matchmaking eligibility, achievement criteria and interaction filters are all tag queries — which is
how the gameplay libraries stay opinionated without being closed. A game adds a rule by writing a tag
in a .vxdef, not by writing a class.
You do not use a tag where an identity is wanted. *Which item is this* is a
DefId; a tag says what kind of thing something is, and several things
share one.
Using it#
The table is baked by the content build out of every tag every definition mentions, plus whatever a
module declared for its own code. A game gets it from
DefinitionCatalog.Tags; building one by hand is what a test does.
Resolve authored prefixes once, at load, and keep the GameplayTagRange. That is what keeps the
frame path free of both the table and the string.
⚠ A prefix the content does not have resolves to an empty range, which matches nothing. The other reading — an unknown prefix matching everything — is how a misspelling makes a boss immune to all damage, and it reads correctly in review.
⚠ Never persist a tag's index. Adding a tag renumbers the ones after it, so a saved index means
something else after the next content build. Persist GameplayTagTable.SymbolOf, which is a hash of
the name.
Examples#
Baking a table and resolving a rule against it:
using Vixen.Gameplay;static class Vocabulary { public static GameplayTagTable Build() => new GameplayTagTableBuilder() .Add("Damage.Fire.Burn") .Add("Damage.Frost") .Add("Creature.Undead.Skeleton") .Build(); // Resolved once. Everything above Damage.Fire.Burn — Damage.Fire and Damage — is implied by // mentioning the leaf, so nothing has to declare an ancestor. public static GameplayTagRange FireDamage(GameplayTagTable tags) => tags.RangeOf("Damage.Fire"); public static bool IsFire(GameplayTagRange fire, GameplayTag damage) => fire.Contains(damage);}A counted tag set, which is what an entity holds:
using Vixen.Gameplay;static class Stunning { // Two effects grant State.Stunned; one expires; the target is still stunned. A plain set loses // that, and the bug it produces reproduces once a week in a raid and never on a desk. public static bool StillStunned(GameplayTagTable tags) { var stunned = tags.Require("State.Stunned"); var set = new GameplayTagSet(); set.Add(stunned); set.Add(stunned); set.Remove(stunned); return set.Contains(stunned); }}A query, which is what a definition authors:
using Vixen.Gameplay;static class Eligibility { public static GameplayTagQuery UndeadInCombat(GameplayTagTable tags) => GameplayTagQuery.Resolve( tags, all: ["Creature.Undead"], any: ["State.InCombat", "State.Mounted"], none: ["State.Invulnerable"] );}See also#
- Definitions and the content walk — where the table comes from.
- Requirements — a tag query plus a numeric predicate.
- Effects — granted tags, blocked tags and immunities, all tag queries.