Skip to content

Latest commit

 

History

29 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Important

🔀 This repo has moved to Codeberg

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.

GameplayTags Foundation for Godot

License Maintained Godot

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.


🛠 Also Available For

O3DE Unity


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. 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.

Support

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.


Become a GitHub Sponsor

Prefer a one-time purchase? Heathen's own storefront is live, direct source access at heathen.group/pricing, no sponsorship required.

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 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.


Install

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.

.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.

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 Archive/addons/FoundationGameplayTags/bin/.


Usage overview

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, 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.

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 is a native GDExtension class (src/public/editor/GameplayTagPickerProperty.h), reusable by other addons directly with no compile-time dependency needed.
  • 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" settings panel: a single unified tree merging every .gptags file in the project, with leaves colored by source addon (label and color derived from each addon's own plugin.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 .gptags file 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.

Not yet ported

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.

License

Apache 2.0.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages