Important
The active copy now lives at codeberg.org/Heathen-Engineering/Godot-GameplayTags-Foundation — please point your git remote, UPM manifest, or Gem reference there going forward. That's where new commits, releases, and issues actually happen now.
This GitHub copy is preserved as-is and still works for anyone already pointing at it, but it isn't receiving new updates. It will be archived (read-only) once every downstream package that depends on it has finished migrating too, not immediately.
Questions? Discord.
A hierarchical, dot-path GameplayTags implementation for Godot 4. Hashed identifiers give 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
Tip
Looking for the easiest way to install?
Copy Archive/addons/FoundationGameplayTags/ straight into your project's addons/ folder. There's no external package manager step. See Install below.
- 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. This is 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.
For general questions, help, and troubleshooting, join our Discord. Thousands of developers are there and can often help faster than waiting on a maintainer. Please use GitHub Issues for a confirmed bug or a feature request that needs tracking, not general support questions.
Prefer a one-time purchase? Heathen's own storefront is live, direct source access at heathen.group/pricing, no sponsorship required.
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
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 that 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.
Copy Archive/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, either inline (tags: PackedStringArray) or loaded from one or more .gptags JSON files (gptags_paths: 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.
{
"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.
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/buildOutput lands in Archive/addons/FoundationGameplayTags/bin/.
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.
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) # falsevar 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# 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)active.subscribe(registry.hash("Effects.Buff"), func(tag, prev, next):
print("Buff stack changed: ", prev, " -> ", next), false) # false = also fires for descendantsC#: 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.
| 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 |
| 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.
| 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, a 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.
| 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 |
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.GameplayTagPickerPropertyis a native GDExtension class (src/public/editor/GameplayTagPickerProperty.h), reusable by other addons directly with no compile-time dependency needed. - 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" settings panel: a single unified tree merging every
.gptagsfile in the project, with leaves colored by source addon (label and color derived from each addon's ownplugin.cfg, so a new addon shipping tags needs no manual registration) and a per-source dim/bright toggle for colorblind-friendly comparison. Live filter/search prunes to matches and their ancestor path. Add, rename, and delete are available on your project's own Default file; tags contributed by another addon's.gptagsfile are read-only here, since a rename or delete only ever rewrites that one file's own entries, never a project-wide search-and-replace across resources or code.
Unity's NativeIntervalMap/NativeDescendantsMap (Burst/Job-System-readable snapshot structures) are Unity-specific internals, not API surface. The interval-encoding algorithm itself is ported; what's skipped is the Burst-safe snapshot/copy machinery those exist for, since Godot's GDExtension already runs the registry natively with no equivalent boundary to cross. GameplayTagsSubsystem (Unity Game Framework bootstrap lifecycle) is ported, and integrates with Godot-Game-Framework's Subsystems dock the same way every other Foundation-tier gem does.
Apache 2.0.