Description
Changelog
Reviews (0)

GameplayTags Foundation for Godot

License

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

Discord GitHub followers

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_name String property (including GameplayTagCondition/GameplayTagOperation's own fields) gets a searchable Tree popup instead of a raw text field, built from every .gptags file in the project plus whatever GameplayTagRegistry already knows about. GameplayTagPickerProperty (editor/GameplayTagPickerProperty.gd) is reusable by other addons directly — no compile-time dependency needed, it's a plain global class_name.
  • Compact GameplayTagCondition/GameplayTagOperation editors — 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 .gptags file in the project (add/rename/delete, registered/unregistered toggle) — the closest Godot analog to Unity's Project Settings > Subsystems > Gameplay Tags page (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.

Reviews

GameplayTags Foundation has no reviews yet.

Login to write a review.