-
Notifications
You must be signed in to change notification settings - Fork 0
GameplayTag Architecture
"Tags are cheap until they are wrong. Then they are the most expensive thing in your project."
UE5's GameplayTag system is one of the engine's most powerful features for data-driven design. Tags replace magic strings and enums with a hierarchical naming system that supports partial matching, editor filtering, and runtime queries. They are used everywhere: abilities, effects, states, events, categories, conditions.
But GameplayTags have a governance problem. In a framework with 13 systems, each defining its own tags, three things go wrong:
1. Naming collisions. Two systems define tags with the same prefix. The audio system creates PGX.Audio.Channel.Master. The message system creates PGX.Audio.SomeEvent. Now "audio" is a shared namespace with no clear owner. When you see a tag starting with PGX.Audio, you cannot know which system defined it or which system should respond to it.
2. Editor noise. A developer editing a Save configuration Data Asset opens the tag picker. They see every tag in the project — hundreds of them — from every system. The save domain tag they need is buried in a list that includes audio channels, game flow states, and loading contexts. Finding the right tag becomes a needle-in-a-haystack exercise.
3. Cross-DLL hazards. Tags defined in one DLL (one plugin's runtime module) must be accessible from another DLL (an editor module, or a different plugin). UE5 provides multiple mechanisms for this, and choosing the wrong one produces link errors, runtime crashes, or silent failures depending on the build configuration.
PGX addresses all three with a branch ownership model, editor enforcement, and a standardized cross-DLL pattern.
Every tag in PGX follows a hierarchical naming convention:
PGX.{System}.{Category}.{Value}
The critical rule: each system owns its branches exclusively. The Audio system owns everything under PGX.Audio.*. The Save system owns everything under PGX.Save.*. No system may define tags under another system's branch.
This is not a naming convention — it is an ownership contract. When you see PGX.Audio.Channel.Master, you know with certainty:
- The Audio system defined this tag
- The Audio system is responsible for this tag's behavior
- Only the Audio system should react to this tag by default
- The tag's documentation lives in the Audio system's card
PGX defines 180+ tags across its 13 systems. The hierarchy organizes into system-specific branches:
- Configuration tags: Used in Data Assets to identify domains, channels, profiles, contexts
- State tags: Used at runtime to represent system states and transitions
- Event tags: Used for cross-system communication through the message bus and event handler
- Category tags: Used for classification and filtering in the Data Registry
- Condition tags: Used for conditional behavior evaluation
Each system's tags are declared in a dedicated header and defined in a corresponding source file. The declaration/definition split follows UE5's pattern for cross-DLL symbol visibility.
Naming conventions are only as strong as their enforcement. A tag convention documented in a wiki page is a suggestion. A tag convention enforced in the editor is a rule.
PGX enforces branch ownership through metadata on editable tag properties. When a Data Asset has a tag field, that field's metadata restricts the tag picker to show only tags from the owning system's branch.
For example: a save configuration Data Asset has a "Domain Tag" field. The tag picker for that field shows only tags under PGX.Save.Domain.*. The developer cannot accidentally assign an audio channel tag to a save domain field — the picker simply does not show those tags.
This enforcement applies to 18 editable tag properties across the framework's Data Assets and type structures.
Three systems are intentionally unrestricted:
GameFlow — The game flow system acts as a hub. It receives state change requests from any system. A save operation might trigger a game flow transition. An audio event might depend on the current game flow state. Restricting GameFlow's tags to a single branch would prevent this cross-system coordination.
Message — The message bus is a pub/sub system. Any system can publish a message with any tag. Any system can subscribe to any tag. Restricting the message system's tag fields would defeat its purpose as a universal communication channel.
EventHandler — The event handler resolves behavior based on tags from any system. An "auto-save" handler triggers on save-related tags. A "pause-audio" handler triggers on audio-related tags. Restricting event handler tag fields would prevent cross-system behavior composition.
These three exceptions are by design: they are the systems whose purpose is to connect other systems. Restricting their tags would undermine the architecture.
UE5's GameplayTag system offers multiple mechanisms for defining tags, each with different DLL visibility characteristics. PGX standardized on one pattern after discovering (through real compilation failures) that the alternatives fail under specific build configurations.
Tags are declared in headers and defined in source files. The declaration makes the tag's existence visible to any module that includes the header. The definition creates the actual tag instance in the module that compiles the source file.
This pattern works across all UE5 build configurations: unity builds, non-unity builds, adaptive builds. It is the most robust approach.
Editor-only modules cannot use the standard declaration/definition pattern because the tag registration mechanism requires a runtime module context. Editor modules that need to reference framework tags use a runtime request mechanism instead — asking the tag system for a tag by name at runtime rather than linking to a compiled tag instance.
This distinction matters because PGX has both runtime modules (where systems live) and editor modules (where inspector panels and tooling live). Tags defined in runtime modules are consumed by editor modules through the request pattern.
Defining tags statically in headers fails under UE5's adaptive non-unity build configuration. The build system's static assertion catches the violation and produces a compilation error. This failure mode is build-configuration-dependent — it may compile locally but fail on CI, or compile in unity mode but fail when the build system selects non-unity for specific translation units.
PGX discovered this through a migration that initially moved all tags to static header definitions, which then had to be reverted when the adaptive build configuration revealed the issue.
Tags that identify configuration entries: save domains (PGX.Save.Domain.Inventory), audio channels (PGX.Audio.Channel.Music), loading contexts (PGX.Loading.Context.MainMenu). These tags live in Data Assets and are used at initialization time.
Tags that represent runtime states: game flow states (PGX.GameFlow.State.InGame), loading phases (PGX.Loading.Phase.Precache). These tags change during gameplay and drive system behavior.
Tags that identify events and messages: bridge events (PGX.Bridge.Event.MatchStarted), handler triggers (PGX.EventHandler.Trigger.AutoSave). These tags flow through the message bus and event handler.
Tags that classify data: registry categories (PGX.Registry.Category.Item), construction types (PGX.Construction.Type.GameMode). These tags organize content for queries and filtering.
Tags that represent testable conditions: platform capabilities (PGX.Profile.Platform.Mobile), feature flags (PGX.GameFlow.Feature.Multiplayer). These tags drive conditional logic without hardcoded enums.
Framework users extend the tag hierarchy by adding their own tags under system branches. The convention:
- PGX-defined tags:
PGX.Save.Domain.Default,PGX.Save.Domain.Global - User-defined tags:
PGX.Save.Domain.Inventory,PGX.Save.Domain.Progression
The user follows the same naming pattern. The tag picker shows both framework-defined and user-defined tags within the appropriate branch. The system processes all tags uniformly — there is no distinction between "built-in" and "custom" tags at runtime.
Enums are tempting. They are type-safe, auto-complete in the IDE, and produce clear compilation errors when used incorrectly.
But enums have a fatal flaw for frameworks: they are closed. Adding a new enum value requires modifying the enum definition, which lives in the framework's source code. A framework user cannot add a new save domain by adding an enum value — they would have to fork the framework.
GameplayTags solve this: the user creates new tags in the project's tag configuration. No framework modification required. No recompilation. No fork.
The tradeoff — tags are stringly-typed and can be misspelled — is mitigated by the editor's tag picker UI and the branch restriction metadata that limits which tags appear in which context.
- Development Preview
- Getting Started
- Release branch catalog
- Public Plugin Matrix
- Early Preview Plugins
- Known Issues
- Architecture Overview
- Plugin Topology
- Module Reference
- Configuration and Registry
- Data-Driven Design
- Profiles and Budgets
- Gameplay Tag Architecture
- Initialization Pipeline
- Cross-Plugin Communication
- Message System
- Event Handlers
- Logging and Trace
- Runtime Flows
- Blueprint API Design
- Editor Integration
- Editor Visual System