GameplayTags Foundation for Godot
A hierarchical, dot-path GameplayTags implementation for Godot 4 — hashed identifiers with zero runtime string cost once registered, plus a full conditional rules engine (GameplayTagCondition/GameplayTagOperation) for driving gameplay and narrative logic.
- License: Apache 2.0
- Origin: Heathen Group
- Platforms: Windows, Linux, macOS
Extension Resolver This asset uses Extension Resolver to identify missing dependencies and help you install them.
Requirements
- Godot 4.6 or compatible
- godot-cpp, checked out locally, for building from source
- Godot-xxHash (compiled in directly — see Build)
- Godot-Game-Framework, enabled in the consuming project — a runtime dependency, not a build-time one. If it's missing, enabling this plugin walks you through fetching it automatically via Extension Resolver for Godot; see that project's README for how the manifest-driven resolution works.
Become a GitHub Sponsor
Support Heathen by becoming a GitHub Sponsor. Sponsorship directly funds the development and maintenance of free tools like this, as well as our game development Knowledge Base and community on Discord.
Sponsors also get access to our private SourceRepo, which includes developer tools for O3DE, Unreal, Unity, and Godot. Learn more or explore other ways to support @ heathen.group/kb
What it does
GameplayTags Foundation gives you a structured, hierarchy-aware tag system built on these core types:
| Type | Purpose |
|---|---|
GameplayTagRegistry |
Engine singleton tracking every registered tag and its hierarchy |
GameplayTagCollection |
A RefCounted container of tags with numeric stack values and set operations |
GameplayTagCondition |
A Resource predicate that tests a tag's presence or value in a collection |
GameplayTagOperation |
A Resource state mutation — applies arithmetic to a tag's value, guarded by conditions |
Tags follow a dot-separated hierarchy. Registering "Effects.Buff.Strength" automatically makes it a descendant of both "Effects.Buff" and "Effects". Hierarchy is stored as parent links compiled to interval (nested-set) encoding — so is_descendant_of is an O(1) range comparison, not a hash-set lookup. This is the algorithm Unity-GameplayTags-Foundation uses, ported here in place of the descendant-hash-set approach the earlier O3DE port shipped with.
GameplayTagCondition and GameplayTagOperation are Resource-derived (not just script-side value types) specifically so they can be authored inline on Ogham Storyteller graph nodes and edited in the Inspector — this is a core dependency for Godot-Ogham-Storyteller-Foundation.
Install
Copy addons/FoundationGameplayTags/ into your project's addons/ folder. Enable the plugin from Project Settings → Plugins.
Author your project's default tags in a GameplayTagsList resource (tags: PackedStringArray), then add FoundationAutoload.tscn as an AutoLoad and assign your list(s) to its tag_lists export — they're registered on _ready(), before any other autoload can query the registry.
Build
Requires godot-cpp and Godot-xxHash checked out locally.
cmake -S addons/FoundationGameplayTags -B addons/FoundationGameplayTags/build \
-DGODOT_CPP_PATH=/path/to/Godot-cpp \
-DGODOT_XXHASH_PATH=/path/to/Godot-xxHash
cmake --build addons/FoundationGameplayTags/build
Output lands in addons/FoundationGameplayTags/bin/.
Usage overview
.gptags file format
{
"registered": true,
"tags": [ "Shop.Inventory.Money", "Shop.Basket.Total" ]
}
Same JSON schema as Unity-GameplayTags-Foundation's .gptags runtime tag source — a tag list authored for one engine is a plain copy-paste for the other. tags lists only the authored dot-paths; parent segments are implied, not required. registered defaults to false if omitted (Unity's convention for inert/draft tag sets, e.g. unvetted UGC/mod content) — set it true for tags you actually want registered.
Registering tags at runtime
var registry := Engine.get_singleton("GameplayTagRegistry")
registry.register_from_string(
"Effects.Buff.Strength\n" +
"Effects.Buff.Speed\n" +
"Effects.Debuff.Slow\n" +
"Status.Burning"
)
Use GameplayTagRegistry.validate_tag(name) to check a name before registering it. Intermediate nodes (Effects, Effects.Buff, ...) are inferred automatically.
Hierarchy queries
var strength := registry.hash("Effects.Buff.Strength")
var buff := registry.hash("Effects.Buff")
var effects := registry.hash("Effects")
registry.is_descendant_of(strength, buff) # true
registry.is_descendant_of(strength, effects) # true
registry.is_descendant_of(buff, strength) # false
Working with collections
var active := GameplayTagCollection.new()
# add_tag stacks — repeated calls increment the count
active.add_tag(registry.hash("Status.Burning"))
active.add_tag(registry.hash("Effects.Buff.Speed"))
active.add_tag(registry.hash("Effects.Buff.Speed")) # now 2
# Presence check — exact_match=false matches the tag or any descendant
var on_fire := active.contains(registry.hash("Status.Burning"), true)
var any_buff := active.contains(registry.hash("Effects.Buff"), false)
# Numeric values, arithmetic
active.apply(registry.hash("Effects.Buff.Speed"), GameplayTagCollection.ARITHMETIC_ADD, 3)
var speed_stacks := active.get_value(registry.hash("Effects.Buff.Speed")) # 5
# Tags reaching 0 are removed automatically
active.apply(registry.hash("Status.Burning"), GameplayTagCollection.ARITHMETIC_SET, 0)
# Set operations
var required := GameplayTagCollection.new()
required.add_tag(registry.hash("Effects.Buff.Speed"))
required.add_tag(registry.hash("Status.Frozen"))
var has_all := active.contains_all(required, true) # false — Frozen absent
var has_any := active.contains_any(required, true) # true — Speed present
Conditions and operations
# Condition: "World.PlayerReputation" must be >= 10
var rep_check := GameplayTagCondition.new()
rep_check.tag_name = "World.PlayerReputation"
rep_check.comparison = GameplayTagCondition.COMPARISON_GREATER_EQUAL
rep_check.compare_value = 10
var passed := rep_check.evaluate(active)
# Operation: add 5 to "World.PlayerReputation" if the condition passes
var give_rep := GameplayTagOperation.new()
give_rep.tag_name = "World.PlayerReputation"
give_rep.arithmetic = GameplayTagCollection.ARITHMETIC_ADD
give_rep.value = 5
give_rep.add_condition(rep_check) # optional guard conditions
active.apply_operation(give_rep) # applies only if rep_check passes
# Evaluate a list of conditions with AND/OR/XOR precedence
var conditions: Array[GameplayTagCondition] = [cond_a, cond_b, cond_c]
var result := GameplayTagCondition.evaluate_all(conditions, active)
Per-tag change subscriptions
active.subscribe(registry.hash("Effects.Buff"), func(tag, prev, next):
print("Buff stack changed: ", prev, " -> ", next), false) # false = also fires for descendants
C# — every type above has a matching wrapper class in Heathen.GameplayTags (GameplayTagRegistry, GameplayTagCollection, GameplayTagCondition, GameplayTagOperation) so C# code never touches Engine.GetSingleton/Variant.Call directly. See the CSharp/ folder.
Public API reference
GameplayTagRegistry (engine singleton)
| Method | Description |
|---|---|
hash(text) |
Hash a string to an id (xxHash3_64bits, seed 0) |
register_from_string(tags) |
Register newline-delimited tags and build hierarchy |
unregister_from_string(tags) |
Remove tags from the working set |
unregister_all() |
Reset the working set to project defaults |
make_tag(name) |
Hash + register a single tag path in one call; returns its id |
is_registered(name) / is_registered_id(id) |
Check whether a tag has been registered |
validate_tag(name) (static) |
Validate format without registering |
get_name(id) |
Reverse-lookup a dot-path name from an id |
get_all_names() / get_all_ids() |
Every registered name / id |
is_descendant_of(tag, ancestor) |
O(1) interval range test |
is_ancestor(ancestor_id, candidate) |
Unity-naming alias for the same test, arguments swapped |
get_descendants(tag) |
All registered descendants (O(registered tags) range scan) |
get_interval_generation() |
Increments every interval rebuild; use to detect cached-data staleness |
registry_changed (signal) |
Fires after any registration/unregistration |
GameplayTagCollection (RefCounted)
| Method | Description |
|---|---|
add_tag(tag) |
Increment the tag's value by one (stacking presence) |
remove_tag(tag) |
Remove a tag unconditionally |
clear() |
Remove all tags |
apply(tag, arithmetic, value) |
Apply arithmetic to a tag's value; tags reaching 0 are removed |
apply_operation(operation) |
Apply a GameplayTagOperation; returns whether it was applied |
get_value(tag) / typed variants |
get_float/get_int/get_long/get_double (bit-reinterpreted) + matching setters |
get_max_value_under(tag) |
Max value across the tag and its present descendants |
contains(tag, exact_match) |
Presence check; exact_match=false matches tag or any descendant |
contains_all/any/none(other, exact_match) |
Set-vs-set presence checks |
get_matching_tags/get_excluding_tags(tag, exact_match) |
Filtered id lists |
get_shared/get_exclusive(other, exact_match) |
Set intersection / symmetric difference |
subscribe(tag, callback, exact_match) / unsubscribe(tag, callback) |
Per-tag change notifications, ancestor-aware |
is_empty() / count() |
Utility |
changed (signal) |
Fires after any mutation |
Arithmetic values: ARITHMETIC_SET, ARITHMETIC_ADD, ARITHMETIC_SUBTRACT, ARITHMETIC_MULTIPLY, ARITHMETIC_DIVIDE, ARITHMETIC_MIN, ARITHMETIC_MAX. A result of 0 removes the tag.
GameplayTagCondition (Resource)
| Property | Description |
|---|---|
tag_name |
Dot-path of the tag to test |
comparison |
See Comparison values below |
compare_value |
RHS for numeric comparisons (ignored for Exists/NotExists, IsMemberOf/IsParentOf/IsExactly) |
compare_tag_name |
RHS tag — dynamic value source for numeric ops, or the reference tag for identity ops |
exact_match |
false rolls up to the max value across the tag and its present descendants (get_max_value_under) |
logic_op |
How this condition chains to the next condition: LOGIC_AND/LOGIC_OR/LOGIC_XOR |
compare_value_type |
VALUE_TYPE_UNSIGNED/VALUE_TYPE_SIGNED/VALUE_TYPE_DECIMAL/VALUE_TYPE_TAG |
evaluate(collection) |
Returns the bool result of this single condition |
evaluate_all(conditions, collection) (static) |
AND-before-OR-before-XOR three-phase reduction; empty list → true |
Comparison values: COMPARISON_EXISTS, COMPARISON_NOT_EXISTS, COMPARISON_EQUAL, COMPARISON_NOT_EQUAL, COMPARISON_LESS, COMPARISON_LESS_EQUAL, COMPARISON_GREATER, COMPARISON_GREATER_EQUAL, COMPARISON_IS_MEMBER_OF, COMPARISON_IS_PARENT_OF, COMPARISON_IS_EXACTLY.
GameplayTagOperation (Resource)
| Property | Description |
|---|---|
tag_name |
Dot-path of the tag to mutate |
arithmetic |
The arithmetic operator (GameplayTagCollection.Arithmetic values) |
value |
The constant operand |
value_tag_name |
When set, the operand is resolved from this tag's collection value at apply time instead of value |
value_type |
VALUE_TYPE_UNSIGNED/VALUE_TYPE_SIGNED/VALUE_TYPE_DECIMAL |
conditions |
Array[GameplayTagCondition] guard conditions; empty = always apply |
should_apply(collection) |
Returns true if all guard conditions pass |
apply(collection) |
Applies if should_apply is true; returns whether it was applied |
add_condition(condition) / clear_conditions() |
Mutate the condition list |
Editor tooling
Enable the plugin (Project Settings > Plugins) to get:
- Tag-picker Inspector field — any
tag_name/*_tag_nameStringproperty (includingGameplayTagCondition/GameplayTagOperation's own fields) gets a searchable Tree popup instead of a raw text field, built from every.gptagsfile in the project plus whateverGameplayTagRegistryalready knows about.GameplayTagPickerProperty(editor/GameplayTagPickerProperty.gd) is reusable by other addons directly — no compile-time dependency needed, it's a plain globalclass_name. - Compact
GameplayTagCondition/GameplayTagOperationeditors — one row (tag picker + comparison/arithmetic dropdown + value + exact-match toggle) instead of Godot's default multi-line block, since these are commonly embedded several-to-a-list (e.g. Ogham options). - "GameplayTags" bottom-panel dock — tag tree CRUD over every
.gptagsfile in the project (add/rename/delete, registered/unregistered toggle) — the closest Godot analog to Unity'sProject Settings > Subsystems > Gameplay Tagspage (Godot has no nested-tab Project Settings equivalent; bottom panels are the native idiom instead).
Changelog for version v1.1.0
No changelog provided for this version.