Preamble
Copyright 2026 Mickael Bonfill
This Specification is released under the MIT License. Permission is hereby granted, free of charge, to any person obtaining a copy of this Specification and associated documentation files, to deal in the Specification without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Specification, and to permit persons to whom the Specification is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Specification.
The author makes no, and expressly disclaims any, representations or warranties, express or implied, regarding this Specification, including, without limitation: merchantability, fitness for a particular purpose, non-infringement of any intellectual property, correctness, accuracy, completeness, timeliness, and reliability. Under no circumstances will the author or contributors be liable for any damages, whether direct, indirect, special or consequential damages for lost revenues, lost profits, or otherwise, arising from or in connection with this Specification or the use or other dealings in this Specification.
Some parts of this Specification are purely informative and so are EXCLUDED from the normative scope of this Specification. The [introduction-conventions] section of the [introduction] defines how these parts of the Specification are identified.
Where this Specification uses technical terminology, defined in the Glossary or otherwise, that refers to enabling technologies not expressly set forth in this Specification, those enabling technologies are EXCLUDED from the normative scope of this Specification.
Where this Specification identifies specific sections of external references, only those specifically identified sections define normative functionality.
The full text of the MIT License can be found in the LICENSE file at the root of the repository.
Part I: Foundations
1. Introduction
1.1 Purpose and Scope
The Universal Gameplay Ability System (UGAS) is an open, engine-agnostic specification designed to standardize gameplay logic across game engines and runtime environments. This specification defines the architecture, data structures, and behavioral contracts required to implement a consistent ability system that can be deployed on any game engine or custom runtime, including Unreal Engine, Unity, and Godot.
The scope of this specification includes:
-
Numeric gameplay state representation (Attributes)
-
Semantic state labeling (Gameplay Tags)
-
Action definition and execution (Gameplay Abilities)
-
State mutation mechanisms (Gameplay Effects)
-
Asynchronous execution patterns (Ability Tasks)
-
Client feedback systems (Gameplay Cues)
-
Network synchronization protocols
This specification does NOT define:
-
Rendering or audio implementation details
-
Physics engine integration specifics
-
Platform-specific memory management
-
User interface implementation
1.2 Design Philosophy
The UGAS specification is founded on three core principles:
Decoupled Gameplay Logic
Traditional gameplay programming relies on imperative state changes within character classes, leading to tightly coupled code where a single modification to a health variable must manually notify UI elements, sound systems, and networking layers. UGAS shifts this paradigm toward a reactive, data-driven architecture where the Actor is merely an avatar—a spatial representation—while the Gameplay Controller(GC) serves as the authoritative state container.
Reactive, Data-Driven Architecture
All state changes flow through a single mutation layer (Gameplay Effects), ensuring that every modification to the game state is tracked, predicted, and synchronized. This approach eliminates expensive per-frame polling of UI elements or AI state machines in favor of event-driven notifications.
Cross-Platform Interoperability
By defining gameplay rules as deterministic, replicable operations on abstract data structures, UGAS enables a unified framework that can be implemented across diverse execution environments. A GC can exist as a C++ component in Unreal Engine, a Data-Oriented Technology Stack (DOTS) entity in Unity, or a scripted component in Godot.
1.3 Document Conventions
Notation
This specification uses the following notational conventions:
-
Mathematical Notation: Standard mathematical symbols for summation (Σ), product (Π), and set operations (∈, ⊆, ∩, ∪)
-
Pseudocode: Language-agnostic pseudocode for algorithm descriptions
-
Interface Definitions: Abstract interface declarations using TypeScript-like syntax
Requirement Levels
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
| Keyword | Meaning |
|---|---|
MUST / REQUIRED / SHALL |
Absolute requirement of the specification |
MUST NOT / SHALL NOT |
Absolute prohibition |
SHOULD / RECOMMENDED |
Valid reasons may exist to ignore, but implications must be understood |
SHOULD NOT / NOT RECOMMENDED |
Valid reasons may exist to implement, but implications must be understood |
MAY / OPTIONAL |
Truly optional; interoperability must be ensured |
1.4 Normative References
-
RFC 2119: Key words for use in RFCs to Indicate Requirement Levels
-
IEEE 754: Standard for Floating-Point Arithmetic
-
JSON Schema: Draft 2020-12
-
YAML 1.2 Specification
2. Terminology
This section provides formal definitions for terms used throughout this specification.
- Actor
-
An entity within the game world that can possess a Gameplay Controller. Actors MAY have spatial representation, AI behavior, or player control.
- Avatar
-
The world representation (visual, physical) associated with a Gameplay Controller. The Avatar is the entity that exists in game space and interacts with the physics and rendering systems.
- Owner
-
The logical owner of a Gameplay Controller. The Owner is responsible for the persistence and lifecycle of the GC. In networked games, the Owner typically corresponds to the authoritative controller of the entity.
- Attribute
-
A named, typed value representing a quantitative aspect of an Actor’s state. Attributes implement the dual-value pattern with Base Value and Current Value.
- AttributeSet
-
A logical container that groups related Attributes. AttributeSets provide modular composition of Actor capabilities.
- Modifier
-
A temporary or permanent adjustment to an Attribute’s value. Modifiers define an operation (Add, AddPost, Multiply, Override) and a magnitude.
- Tag
-
A hierarchical, unique identifier serving as a conceptual label for Actors, Abilities, and Effects. Tags use dot-notation (e.g.,
State.Debuff.Stunned.Magic). - TagContainer
-
A collection of Tags associated with an entity. TagContainers support efficient query operations.
- TagQuery
-
A predicate expression evaluated against a TagContainer to determine matches.
- Ability
-
A self-contained unit of logic defining an action an Actor can perform. Abilities are asynchronous, stateful objects with defined lifecycles.
- AbilitySpec
-
Instance data for a granted Ability, including level, input binding, and runtime parameters.
- AbilityTask
-
An asynchronous operation within an Ability that pauses execution until a specific trigger condition is met.
- Effect
-
The mechanism by which Attributes and Tags are modified. Effects are the ONLY authorized mechanism for mutating gameplay state.
- EffectSpec
-
Lightweight application data for applying an Effect, containing magnitude, level, and context information.
- EffectContext
-
Runtime context for Effect application, including source Actor, target Actor, hit location, and causal chain information.
- Cue
-
A client-side feedback element (VFX, SFX, camera effects) triggered by Tags or Effects. Cues are purely cosmetic and do not affect gameplay logic.
- CueManager
-
Client-side system responsible for instantiating and managing Cue resources.
- GC (Gameplay Controller)
-
The central component managing an Actor’s Attributes, Tags, Abilities, and Effects. The GC is the authoritative state container for gameplay logic.
3. Architectural Overview
3.1 Four-Pillar Model
The UGAS architecture is predicated on the interaction between four distinct pillars:
┌─────────────────────────────────────────────────────────────────┐ │ GAMEPLAY CONTROLLER │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────┐ ┌───────────────┐ ┌──────────────────────┐ │ │ │ DATA │ │ SEMANTIC │ │ LOGIC │ │ │ │ LAYER │ │ LAYER │ │ LAYER │ │ │ │ │ │ │ │ │ │ │ │ Attributes │ │ Gameplay Tags│ │ Gameplay Abilities │ │ │ │ Attribute Sets│ │ Tag Containers│ │ Ability Tasks │ │ │ │ │ │ │ │ │ │ │ └──────┬────────┘ └──────┬────────┘ └──────────┬───────────┘ │ │ │ │ │ │ │ └─────────────────┼─────────────────────┘ │ │ │ │ │ ▼ │ │ ┌────────────────────────┐ │ │ │ MUTATION LAYER │ │ │ │ │ │ │ │ Gameplay Effects │ │ │ │ Modifiers │ │ │ │ Execution Calcs │ │ │ └────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘
- Data Layer (Attributes)
-
Numeric state representation. Attributes store quantitative values such as Health, Mana, Strength, and Speed. All numeric gameplay state MUST be represented through Attributes.
- Semantic Layer (Tags)
-
Qualitative state representation. Tags describe "what kind" or "in what state" an Actor exists. Tags enable logic gating, ability requirements, and state queries without coupling to specific implementations.
- Logic Layer (Abilities)
-
Behavioral definitions. Abilities encapsulate the asynchronous, stateful logic of actions Actors can perform. Abilities coordinate with Tasks for complex, multi-stage execution.
- Mutation Layer (Effects)
-
State change mechanism. Effects are the ONLY authorized mechanism for modifying Attributes or Tags. This restriction ensures all state changes are tracked, predicted, and synchronized. Ability implementations MUST NOT call
Tags.AddTag(),Tags.RemoveTag(), or any equivalent direct tag mutation API. All tag state changes MUST flow through aGameplayEffectapplied via the GC’s effect application pipeline. This is a deliberate departure from UE4 GAS (which permits "loose tags") and is the property that makes replication of tag state tractable.
3.2 Component Relationships
┌─────────────┐
│ ACTOR │
│ (Avatar) │
└──────┬──────┘
│ possesses
▼
┌─────────────┐ ┌───────────────┐ ┌─────────────┐
│ OWNER │──────────────│ GAMEPLAY │──────────────│ ATTRIBUTE │
│ ACTOR │ owns │ CONTROLLER │ contains │ SETS │
└─────────────┘ └───────┬───────┘ └─────────────┘
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│ ABILITIES │ │ TAGS │ │ EFFECTS │
│ (Specs) │ │(Container)│ │ (Active) │
└─────┬─────┘ └───────────┘ └─────┬─────┘
│ │
▼ ▼
┌───────────┐ ┌───────────┐
│ TASKS │ │ MODIFIERS │
└───────────┘ └───────────┘
3.3 Execution Model
The UGAS execution model follows a deterministic sequence for processing gameplay logic:
-
Input Processing: Hardware inputs are mapped to Input Actions, which trigger Ability activation attempts.
-
Ability Activation: The GC validates activation requirements (Tags, Costs, Cooldowns) before committing the Ability.
-
Effect Application: Abilities apply Effects to Targets. Effects create Modifiers on Attributes and grant/remove Tags.
-
Attribute Recalculation: Affected Attributes recalculate their Current Values based on active Modifiers.
-
Event Dispatch: OnAttributeChanged events propagate to registered observers.
-
Cue Triggering: Tag changes trigger appropriate Gameplay Cues on clients.
-
Replication: State changes are replicated to networked clients according to the configured replication mode.
3.4 Threading Considerations
Implementations SHOULD consider the following threading guidelines:
-
Main Thread: Ability activation, Effect application, and Attribute modification SHOULD occur on the main game thread to ensure deterministic ordering.
-
Async Tasks: AbilityTasks MAY spawn background work but MUST return results to the main thread for state modification.
-
Replication: Network replication MAY occur on dedicated networking threads but MUST synchronize with the main thread for state application.
-
Cues: Gameplay Cue instantiation MAY occur on rendering threads but MUST NOT modify gameplay state.
Part II: Core Components
4. Gameplay Controller(GC)
4.1 Responsibilities
The Gameplay Controlleris the central hub for all gameplay ability logic. An GC implementation MUST:
-
Maintain collections of granted Abilities, active Effects, and owned Tags
-
Manage one or more AttributeSets
-
Process Ability activation requests
-
Apply and remove Gameplay Effects
-
Dispatch events for state changes
-
Support network replication (if applicable)
4.2 Ownership Model
The GC implements a dual-actor ownership model:
- Owner Actor
-
The logical owner of the GC. The Owner is responsible for:
-
GC lifecycle management
-
Network authority
-
Persistence across possession changes
-
- Avatar Actor
-
The world representation associated with the GC. The Avatar provides:
-
Spatial position for targeting
-
Animation and physics integration
-
Visual representation
-
Same-Actor Configuration
For simple entities (AI-controlled enemies, destructible objects), the Owner and Avatar MAY be the same Actor:
┌─────────────────────────────┐ │ AI ENEMY │ │ ┌───────────────────────┐ │ │ │ GC │ │ │ │ Owner: this │ │ │ │ Avatar: this │ │ │ └───────────────────────┘ │ └─────────────────────────────┘
Split-Actor Configuration
For player-controlled characters in networked games, the Owner and Avatar SHOULD be separate to ensure GC persistence across respawns:
┌─────────────────────────────┐ ┌─────────────────────────────┐ │ PLAYER STATE │ │ PLAYER CHARACTER │ │ (Persists entire session) │ │ (Destroyed on death) │ │ ┌───────────────────────┐ │ │ │ │ │ GC │──┼────────┼──▶ Avatar reference │ │ │ Owner: this │ │ │ │ │ └───────────────────────┘ │ └─────────────────────────────┘ └─────────────────────────────┘
4.3 Lifecycle
Initialization Sequence
-
GC is instantiated on Owner Actor
-
AttributeSets are registered with GC
-
Owner and Avatar references are set
-
Initial Abilities are granted
-
Initial Effects are applied
-
Replication is configured (if networked)
Possession Handling
When Avatar possession changes:
-
Previous Avatar reference is cleared
-
Active Effects targeting Avatar location are re-evaluated
-
New Avatar reference is set
-
Avatar-dependent Abilities are re-validated
Destruction Cleanup
-
All active Effects are removed
-
All granted Abilities are revoked
-
Event subscriptions are cleared
-
Network replication is terminated
4.4 Interface Specification
Implementations SHOULD provide an interface for GC discovery:
interface IAbilitySystemInterface {
/**
* Returns the Gameplay Controllerassociated with this entity.
* @returns The GC instance, or null if not available
*/
GetGameplayController(): GameplayController | null;
}
Actors participating in the ability system MUST implement this interface or provide an equivalent discovery mechanism.
4.5 Public API
The following methods define the core GC interface:
Effect Context Creation
/**
* Creates a new Effect Context for outgoing effects.
* @returns A handle to the new context
*/
MakeEffectContext(): EffectContextHandle;
Effect Spec Creation
/**
* Creates an Effect Spec for application.
* @param effectClass - The Effect definition to instantiate
* @param level - The level at which to apply the effect
* @param context - The effect context handle
* @returns A handle to the new spec
*/
MakeOutgoingSpec(
effectClass: GameplayEffectClass,
level: number,
context: EffectContextHandle
): EffectSpecHandle;
Effect Application
/**
* Applies an effect to this GC's owner.
* @param spec - The effect spec to apply
* @param predictionKey - Optional prediction key for client-side prediction
* @returns Handle to the active effect, or invalid handle if application failed
*/
ApplyGameplayEffectToSelf(
spec: EffectSpecHandle,
predictionKey?: PredictionKey
): ActiveEffectHandle;
/**
* Applies an effect to a target GC.
*
* NETWORKED ENVIRONMENTS: A call originating on a client is speculative.
* The server MUST validate instigator authority, ability ownership, target
* reachability, and effect-class whitelist before executing the authoritative
* application. See §13.7 for the full validation pipeline.
*
* @param target - The target GC
* @param spec - The effect spec to apply
* @param predictionKey - Optional prediction key for client-side prediction
* @returns Handle to the active effect, or invalid handle if application failed
*/
ApplyGameplayEffectToTarget(
target: GameplayController,
spec: EffectSpecHandle,
predictionKey?: PredictionKey
): ActiveEffectHandle;
Effect Removal
/**
* Removes an active effect.
* @param handle - Handle to the active effect
* @param stacksToRemove - Number of stacks to remove (-1 for all)
* @returns True if removal succeeded
*/
RemoveActiveGameplayEffect(
handle: ActiveEffectHandle,
stacksToRemove: number = -1
): boolean;
Ability Management
/**
* Grants an ability to this GC.
* @param abilityClass - The ability class to grant
* @param level - Initial ability level
* @param inputID - Optional input binding
* @returns Handle to the granted ability spec
*/
GrantAbility(
abilityClass: GameplayAbilityClass,
level: number = 1,
inputID?: InputID
): AbilitySpecHandle;
/**
* Attempts to activate an ability.
* @param handle - Handle to the ability spec
* @returns True if activation succeeded
*/
TryActivateAbility(handle: AbilitySpecHandle): boolean;
5. Attributes
5.1 Attribute Data Structure
An Attribute MUST implement the following data structure:
struct Attribute {
/** Permanent value, modified only by Instant effects */
BaseValue: float;
/** Dynamically calculated value including all active modifiers */
CurrentValue: float;
/** Collection of active modifiers affecting this attribute */
Modifiers: ModifierStack;
/** Static configuration for this attribute */
Metadata: AttributeMetadata;
}
struct AttributeMetadata {
/** Unique identifier for this attribute */
Name: string;
/** Attribute category */
Category: AttributeCategory;
/** Minimum allowed value (optional) */
MinValue?: float | AttributeReference;
/** Maximum allowed value (optional) */
MaxValue?: float | AttributeReference;
/** Replication configuration */
ReplicationMode: AttributeReplicationMode;
}
enum AttributeCategory {
/** Consumable values (Health, Mana, Stamina) */
Resource,
/** Derived statistics (Damage, Defense, Speed) */
Statistic,
/** Meta-attributes used for calculations only */
Meta
}
5.2 Dual-Value Pattern
Every Attribute MUST implement the dual-value pattern consisting of Base Value and Current Value. This distinction is the primary mechanism for handling temporary modifications.
- Base Value
-
The permanent, persistent value of the Attribute. Base Values are modified ONLY by Instant effects and represent permanent changes such as leveling, permanent upgrades, or instant damage/healing.
- Current Value
-
The dynamically calculated result of the Base Value plus all active temporary Modifiers. Current Values are ephemeral and automatically recalculated when Modifiers are added or removed.
| Component | Modification Source | Persistence |
|---|---|---|
Base Value |
Instant Effects only |
Persistent (saved) |
Current Value |
All Modifier types |
Ephemeral (calculated) |
5.3 Modifier Pipeline
The Current Value calculation MUST follow a standardized pipeline to ensure mathematical consistency across implementations.
Formula
The Current Value \(V_{current}\) is calculated as:
Where:
- \(V_{base}\) = Base Value
- \(a_i\) = Pre-multiply flat additive modifiers (Add operations)
- \(C\) = the set of distinct Channel values among active Multiply modifiers; each modifier without a Channel belongs to its own unique implicit singleton channel
- \(m_k\) = signed bonus magnitude for each Multiply modifier (e.g., +0.25 for a +25% bonus, −0.25 for a 25% penalty)
- \(b_l\) = Post-multiply flat additive modifiers (AddPost operations; very rare)
- \(V_{min}\), \(V_{max}\) = clamping constraints
Note that clamping is not mandatory; the simplified form is:
Channel Aggregation
The channel product \(\prod_{c \in C}\!\left(1 + \sum_{k \in c} m_k\right)\) is how the "damage bucket" design is expressed at the pipeline level:
-
Same channel → bonuses ADD. All
Multiplymodifiers sharing aChannelvalue contribute their magnitudes additively. The channel’s effective factor is1 + sum of magnitudes. Two +20% bonuses in the same channel yield ×1.40, not ×1.44. -
Different channels → factors MULTIPLY. Each channel produces one effective factor; those factors are multiplied together. A ×1.40 channel and a ×1.30 channel yield ×1.82.
-
No channel → isolated singleton. A
Multiplymodifier without aChannelis in its own implicit channel, so its contribution is1 + magnitude— independent of all other modifiers.
This is the primary tool for preventing linear power creep: bonuses from the same source category (e.g., "damage bonuses from gear") are additive within a channel, while bonuses from categorically different sources (e.g., "gear bonuses" vs. "legendary powers") are multiplicative across channels.
Order of Operations
The order of operations is CRITICAL for deterministic results:
-
Sum all flat additive modifiers (
Add):flat = ΣAdd -
Apply flat additions to Base Value:
value = Base + flat -
Group
Multiplymodifiers byChannel. For each channel, sum the magnitudes:channel_factor = 1 + Σm_k -
Multiply all channel factors together and apply:
value *= Π channel_factor -
Add sum of all post-multiply flat additive modifiers (
AddPost):value += ΣAddPost -
Apply
Overridemodifiers (if any, replacing the result) — see conflict resolution below -
Apply clamping constraints
Override Conflict Resolution
When multiple active Override modifiers target the same Attribute simultaneously, implementations MUST resolve the conflict deterministically using the following ordered rules:
-
Priority wins: The Override modifier from the
GameplayEffectwith the highestPriorityvalue replaces the result. Lower-priority Overrides are ignored for that Attribute. -
Last-applied wins on tie: If two or more competing Override modifiers share the same
Priority, the one from the most recently applied effect wins (LIFO order, determined by application timestamp).
Priority defaults to 0. Effects intended to be overrideable by other effects should use lower priority values (e.g. -10); effects that must always dominate should use higher values (e.g. 100).
Example: A "Freeze" effect sets
MoveSpeedOverride to0at Priority10. A "Slow` effect also sets an Override to50at Priority5. The Freeze wins because10 > 5. If a "Root" effect then sets an Override to0at Priority10, it ties with Freeze — the more recently applied effect’s Override is used, but the end result is identical.
Example Calculation
Given: - Base Value: 100 - Add Modifier 1: +20 - Add Modifier 2: +10 - Additive Percentage 1: +10% (0.1) - Additive Percentage 2: +15% (0.15) - Multiplicative 1: 1.5× - Multiplicative 2: 2.0× - No Bonus Flat
Calculation:
Step 1-2: 100 + 20 + 10 = 130 Step 3-4: 130 × (1 + 0.1 + 0.15) = 130 × 1.25 = 162.5 Step 5-6: 162.5 × 1.5 × 2.0 = 487.5
Current Value = 487.5
5.4 Clamping and Bounds
Attributes MAY define minimum and maximum constraints. Constraints can be:
- Static Values
-
Fixed numeric bounds that do not change.
Clamping:
Min: 0.0
Max: 100.0
- Dependent Attribute References
-
Bounds referencing other Attributes, enabling dynamic constraints.
Clamping:
Min: 0.0
Max: "MaxHealth" # References another attribute
When a constraint references another Attribute: 1. The referenced Attribute’s Current Value is used as the bound 2. Changes to the referenced Attribute trigger recalculation of dependent Attributes 3. Circular dependencies MUST NOT be created
5.5 Attribute Metadata
Attribute Metadata defines static configuration:
Category
- Resource: Consumable values that are spent and recovered (Health, Mana, Stamina)
- Statistic: Derived values used in calculations (Damage, Defense, CritChance)
- Meta: Internal values used only for calculations, not displayed to players
Replication Flags
- None: Not replicated
- OwnerOnly: Replicated only to owning client
- All: Replicated to all clients
5.6 OnAttributeChanged Event
Any change to an Attribute—whether to Base Value or Current Value—MUST trigger an OnAttributeChanged event.
Event Payload
struct AttributeChangedEvent {
/** The attribute that changed */
Attribute: AttributeReference;
/** Previous current value */
OldValue: float;
/** New current value */
NewValue: float;
/** The effect that caused the change (if any) */
CausalEffect?: ActiveEffectHandle;
/** Source of the change */
Source?: GameplayController;
/** Target of the change */
Target: GameplayController;
}
Subscription Model
Observers SHOULD register for attribute change notifications:
interface IAttributeChangeObserver {
OnAttributeChanged(event: AttributeChangedEvent): void;
}
// Registration
GC.RegisterAttributeChangeObserver(
attribute: AttributeReference,
observer: IAttributeChangeObserver
): void;
// Unregistration
GC.UnregisterAttributeChangeObserver(
attribute: AttributeReference,
observer: IAttributeChangeObserver
): void;
5.7 Schema Definition
Attribute:
Name: string # Required: Unique identifier
DefaultBaseValue: float # Required: Initial base value
Category: enum # Optional: Resource | Statistic | Meta
Clamping: # Optional: Value constraints
Min: float | string # Static value or attribute reference
Max: float | string # Static value or attribute reference
ReplicationMode: enum # Optional: None | OwnerOnly | All
Metadata: # Optional: Additional configuration
DisplayName: string # Human-readable name
Description: string # Tooltip description
UICategory: string # UI grouping
6. Attribute Sets
6.1 Purpose and Composition
An Attribute Set is a logical container grouping related Attributes. Attribute Sets provide:
-
Modularity: Actors can mix and match sets based on capabilities
-
Organization: Related Attributes are defined together
-
Reusability: Common sets can be shared across Actor types
-
Serialization Boundary: Sets define units for save/load operations
6.2 Set Registration with GC
Attribute Sets MUST be registered with an GC before use:
/**
* Registers an attribute set with this GC.
* @param attributeSet - The set to register
*/
GC.RegisterAttributeSet(attributeSet: AttributeSet): void;
/**
* Unregisters an attribute set from this GC.
* @param attributeSet - The set to unregister
*/
GC.UnregisterAttributeSet(attributeSet: AttributeSet): void;
/**
* Retrieves a registered attribute set by type.
* @returns The attribute set, or null if not registered
*/
GC.GetAttributeSet<T extends AttributeSet>(): T | null;
6.3 Modular Design Patterns
Combat Attribute Set
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/attribute_set.json
Name: "CombatAttributeSet"
Attributes:
- Name: "Health"
DefaultBaseValue: 100.0
Category: Resource
Clamping:
Min: 0.0
Max: "MaxHealth"
- Name: "MaxHealth"
DefaultBaseValue: 100.0
Category: Statistic
Clamping:
Min: 1.0
- Name: "Mana"
DefaultBaseValue: 50.0
Category: Resource
Clamping:
Min: 0.0
Max: "MaxMana"
- Name: "MaxMana"
DefaultBaseValue: 50.0
Category: Statistic
Clamping:
Min: 0.0
- Name: "AttackPower"
DefaultBaseValue: 10.0
Category: Statistic
- Name: "Defense"
DefaultBaseValue: 5.0
Category: Statistic
Movement Attribute Set
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/attribute_set.json
Name: "MovementAttributeSet"
Attributes:
- Name: "MoveSpeed"
DefaultBaseValue: 600.0
Category: Statistic
Clamping:
Min: 0.0
- Name: "JumpVelocity"
DefaultBaseValue: 800.0
Category: Statistic
- Name: "GravityScale"
DefaultBaseValue: 1.0
Category: Statistic
- Name: "AirControl"
DefaultBaseValue: 0.5
Category: Statistic
Clamping:
Min: 0.0
Max: 1.0
Vehicle Attribute Set
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/attribute_set.json
Name: "VehicleAttributeSet"
Attributes:
- Name: "EngineTorque"
DefaultBaseValue: 500.0
Category: Statistic
- Name: "MaxSpeed"
DefaultBaseValue: 200.0
Category: Statistic
- Name: "TireGrip"
DefaultBaseValue: 1.0
Category: Statistic
- Name: "Fuel"
DefaultBaseValue: 100.0
Category: Resource
Clamping:
Min: 0.0
Max: "MaxFuel"
- Name: "MaxFuel"
DefaultBaseValue: 100.0
Category: Statistic
6.4 Cross-Set Dependencies
Attributes MAY reference Attributes from other registered sets:
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/attribute_set.json
Name: "DerivedStatsSet"
Dependencies:
- "CombatAttributeSet"
Attributes:
- Name: "EffectiveHealth"
DefaultBaseValue: 0.0
Category: Meta
DerivedFrom:
Expression: "Health * (1 + Defense / 100)"
Cross-set references are resolved at runtime. Implementations MUST:
-
Validate all dependencies exist before registration
-
Ensure proper recalculation order when dependencies change
-
Prevent circular dependency chains
6.5 Schema Definition
AttributeSet:
Name: string # Required: Unique set identifier
Dependencies: [string] # Optional: Required attribute sets
Attributes: [Attribute] # Required: List of attributes
Metadata: # Optional: Additional configuration
DisplayName: string
Description: string
7. Gameplay Tags
7.1 Hierarchical Naming Convention
Gameplay Tags use hierarchical dot-notation to represent semantic categories:
Category.Subcategory.Leaf
Examples:
- State.Debuff.Stunned.Magic
- Ability.Type.Melee.Slash
- DamageType.Physical.Blunt
- Cooldown.Ability.Fireball
- GameplayCue.Impact.Fire
Naming Rules
-
Each segment MUST use PascalCase
-
Hierarchies SHOULD NOT exceed 5 levels
-
Leaf tags SHOULD be specific; parent tags SHOULD be categorical
-
Reserved prefixes:
-
GameplayCue.*- Cue trigger tags -
Cooldown.*- Cooldown tracking tags -
State.*- Actor state tags -
Ability.*- Ability classification tags -
DamageType.*- Damage classification tags
-
7.2 Tag Container
A Tag Container is a collection of tags associated with an entity.
Internal Representation
A TagContainer MUST maintain reference counts per tag, not a simple set. Multiple concurrent Effects can grant the same tag; each grant increments the count; each removal decrements it. The tag is considered present only while its count is greater than zero.
struct TagContainer {
/**
* Grant counts for every explicitly-held tag.
* A tag is "explicitly present" when its count > 0.
* Managed exclusively by the GC Effect application pipeline.
*/
ExplicitTagCounts: Map<Tag, number>;
/**
* Cumulative grant counts for all explicit tags AND their ancestor tags.
* Automatically maintained by AddTag/RemoveTag: adding tag T also
* increments the count of every ancestor of T; removing T decrements them.
* Used to answer MatchesTag queries in O(1).
*/
AllTagCounts: Map<Tag, number>;
}
Operations
interface TagContainer {
/**
* @internal Reserved for the GC Effect application pipeline.
* Ability implementations MUST NOT call this directly.
* Grant tags via a GameplayEffect with GrantedTags instead.
*
* Increments the grant count of `tag` in ExplicitTagCounts and the grant
* count of every ancestor of `tag` in AllTagCounts.
* Dispatches an OnTagChanged event ONLY when the count transitions 0 → 1
* (i.e. the tag was previously absent). Subsequent grants of the same tag
* by additional Effects increment the count silently.
*/
AddTag(tag: Tag): void;
/**
* @internal Reserved for the GC Effect application pipeline.
* Ability implementations MUST NOT call this directly.
* Remove tags by removing the GameplayEffect that granted them.
*
* Decrements the grant count of `tag` in ExplicitTagCounts and the grant
* count of every ancestor of `tag` in AllTagCounts.
* MUST NOT decrement below 0; implementations MUST treat an underflow as
* a logic error (assert / log error and skip).
* Dispatches an OnTagChanged event ONLY when the count transitions 1 → 0
* (i.e. the tag is now fully absent). While the count remains > 1, no
* event is dispatched.
*/
RemoveTag(tag: Tag): void;
/**
* Returns the current grant count for `tag` in ExplicitTagCounts.
* Useful for "how many stacks of Burning are active?" queries.
* Returns 0 if the tag is not present.
*/
GetTagCount(tag: Tag): number;
/** Returns true if no explicit tags have a count > 0. */
IsEmpty(): boolean;
/** Returns the number of distinct explicit tags with count > 0. */
Count(): number;
/**
* @internal Reserved for the GC Effect application pipeline.
* Sets all counts to 0 and dispatches OnTagChanged for every tag whose
* count was > 0. Used during GC teardown only.
*/
Clear(): void;
}
7.3 Query Operations
| Operation | Map queried | Semantics | Example |
|---|---|---|---|
|
|
True if |
Checking for any type of "Stunned" status |
|
|
True if |
Immunity to "Stunned.Magic" but not "Stunned.Physical" |
|
|
Returns |
"How many stacks of Burning?" |
|
|
True if any tag in Container has |
Spell that affects "Undead" OR "Demon" |
|
|
True if every tag in Container has |
Combo requiring "Chilled" AND "Vulnerable" |
|
|
True if no tag in Container has |
Ability blocked by any "Immunity" tag |
Query Examples
// Container has: State.Debuff.Stunned.Magic, Status.Burning
container.MatchesTag("State.Debuff.Stunned") // true (parent match)
container.MatchesTag("State.Debuff.Stunned.Magic") // true (exact match)
container.MatchesTag("State.Debuff.Stunned.Physical") // false
container.MatchesTagExact("State.Debuff.Stunned") // false (not exact)
container.MatchesTagExact("State.Debuff.Stunned.Magic") // true
container.HasAny(["Status.Frozen", "Status.Burning"]) // true
container.HasAll(["State.Debuff.Stunned.Magic", "Status.Burning"]) // true
container.HasAll(["Status.Burning", "Status.Frozen"]) // false
7.4 Tag Inheritance and Implicit Tags
When a tag is added to a container, the grant counts of all ancestor tags in AllTagCounts are incremented by the same amount. When a tag is removed, ancestor counts are decremented symmetrically. This means MatchesTag on a parent tag is always consistent with the sum of grants on its descendants:
AddTag("State.Debuff.Stunned.Magic")
ExplicitTagCounts["State.Debuff.Stunned.Magic"] = 1
AllTagCounts["State.Debuff.Stunned.Magic"] = 1
AllTagCounts["State.Debuff.Stunned"] = 1 ← propagated
AllTagCounts["State.Debuff"] = 1 ← propagated
AllTagCounts["State"] = 1 ← propagated
AddTag("State.Debuff.Stunned.Magic") # second effect grants same tag
ExplicitTagCounts["State.Debuff.Stunned.Magic"] = 2
AllTagCounts["State.Debuff.Stunned.Magic"] = 2
AllTagCounts["State.Debuff.Stunned"] = 2
AllTagCounts["State.Debuff"] = 2
AllTagCounts["State"] = 2
RemoveTag("State.Debuff.Stunned.Magic") # first effect expires
ExplicitTagCounts["State.Debuff.Stunned.Magic"] = 1 # still present!
AllTagCounts["State.Debuff.Stunned.Magic"] = 1
AllTagCounts["State.Debuff.Stunned"] = 1
...
# MatchesTag("State.Debuff.Stunned") → still true, no event dispatched
RemoveTag("State.Debuff.Stunned.Magic") # second effect expires
ExplicitTagCounts["State.Debuff.Stunned.Magic"] = 0 # now absent
AllTagCounts["State.Debuff.Stunned.Magic"] = 0
AllTagCounts["State.Debuff.Stunned"] = 0
...
# OnTagChanged dispatched for the leaf and each ancestor that hit 0
This enables hierarchical queries where MatchesTag("State.Debuff") matches any active debuff, and the match remains valid as long as any descendant tag has a count > 0.
7.5 State Representation via Tags
Tags are the primary method for representing Actor states. Instead of boolean flags:
// Avoid this pattern
if (actor.isStunned && !actor.isImmune) { ... }
// Use tag queries
if (actor.Tags.MatchesTag("State.Debuff.Stunned") &&
!actor.Tags.MatchesTag("Status.Immune.Stun")) { ... }
This decouples the "How" of a state (animation, logic freeze) from the "What" of the state (the Tag).
7.6 Schema Definition
TagDefinition:
Tag: string # Full hierarchical tag name
Description: string # Human-readable description
AllowMultiple: boolean # Can multiple instances exist? (default: false)
DevComment: string # Developer notes
Tag definitions MAY be collected in a tag registry:
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/gameplay_tag.json
Tags:
- Tag: "State.Debuff.Stunned"
Description: "Actor is unable to perform actions"
- Tag: "State.Debuff.Stunned.Magic"
Description: "Stun caused by magical effect"
- Tag: "State.Debuff.Stunned.Physical"
Description: "Stun caused by physical impact"
- Tag: "Status.Immune.Stun"
Description: "Actor is immune to stun effects"
8. Gameplay Abilities
8.1 Ability Definition
A Gameplay Ability is a self-contained unit of logic defining an action an Actor can perform. Unlike simple function calls, Abilities are asynchronous, stateful objects with defined lifecycles.
Ability Class Structure
abstract class GameplayAbility {
/** Tags describing this ability */
AbilityTags: TagContainer;
/** Tags that block this ability's activation */
BlockedByTags: TagContainer;
/** Tags that this ability blocks when active */
BlockAbilitiesWithTags: TagContainer;
/** Tags required on owner for activation */
ActivationRequiredTags: TagContainer;
/** Tags that prevent activation if present */
ActivationBlockedTags: TagContainer;
/**
* Tags applied to the owner while this ability is active.
* Implementations MUST apply these as an auto-generated Infinite GameplayEffect
* on CommitAbility and remove that effect on EndAbility/CancelAbility.
* Direct tag mutation is prohibited (see §3.1).
*/
ActivationOwnedTags: TagContainer;
/** Cost effect applied on commit */
CostEffect?: GameplayEffectClass;
/** Cooldown effect applied on commit */
CooldownEffect?: GameplayEffectClass;
/** Called when ability is activated */
abstract ActivateAbility(context: AbilityContext): void;
/** Called when ability ends */
abstract EndAbility(wasCancelled: boolean): void;
}
AbilitySpec (Instance Data)
struct AbilitySpec {
/** Reference to the ability class */
AbilityClass: GameplayAbilityClass;
/** Current level of this ability instance */
Level: number;
/** Input action binding (if any) */
InputID?: InputID;
/** Handle for identification */
Handle: AbilitySpecHandle;
/** Runtime parameters */
Parameters: Map<string, any>;
/** Is currently active? */
IsActive: boolean;
/**
* Handle to the auto-generated Infinite Effect that grants ActivationOwnedTags.
* Set by CommitAbility; cleared by EndAbility/CancelAbility.
* Undefined when the ability is not active.
*/
ActiveOwnedTagsHandle?: ActiveEffectHandle;
}
8.2 Lifecycle State Machine
┌──────────────┐
│ NotGranted │
└──────┬───────┘
│ Grant
▼
┌──────────────┐
┌──────────▶│ Granted │◀──────────┐
│ │ (Inactive) │ │
│ └──────┬───────┘ │
│ │ TryActivate │
│ ▼ │
│ ┌──────────────┐ │
│ │ Activating │───────────┤
│ │ (Validating) │ Fail │
│ └──────┬───────┘ │
│ │ Commit │
│ ▼ │
│ ┌──────────────┐ │
│ │ Active │ │
│ │ (Executing) │ │
│ └──────┬───────┘ │
│ │ End/Cancel │
│ ▼ │
│ ┌──────────────┐ │
└───────────│ Ending │───────────┘
└──────────────┘
8.3 Activation Requirements
Before an Ability can activate, the following checks MUST pass:
-
Granted Check: Ability must be granted to the GC
-
Not Already Active: Ability must not currently be active (unless configured for multiple instances)
-
Required Tags: Owner must have all tags in
ActivationRequiredTags -
Blocked Tags: Owner must NOT have any tags in
ActivationBlockedTags -
Cost Verification: If CostEffect is defined, owner must have sufficient resources
-
Cooldown Verification: Cooldown tag must not be present
function CanActivateAbility(spec: AbilitySpec): boolean {
const ownerTags = GC.GetOwnedTags();
// Check required tags
if (!ownerTags.HasAll(spec.AbilityClass.ActivationRequiredTags)) {
return false;
}
// Check blocked tags
if (ownerTags.HasAny(spec.AbilityClass.ActivationBlockedTags)) {
return false;
}
// Check cooldown
if (ownerTags.MatchesTag(GetCooldownTag(spec))) {
return false;
}
// Check cost
if (!CanAffordCost(spec)) {
return false;
}
return true;
}
8.4 Commit Phase
The Commit phase is the point of no return where resources are consumed and cooldowns begin. Once committed:
-
Cost Effect is applied (resources consumed)
-
Cooldown Effect is applied (cooldown tag granted)
-
Activation Owned Tags are granted
-
Ability proceeds to execution
function CommitAbility(spec: AbilitySpec): boolean {
// Apply cost
if (spec.AbilityClass.CostEffect) {
const costSpec = MakeOutgoingSpec(spec.AbilityClass.CostEffect, spec.Level);
ApplyGameplayEffectToSelf(costSpec);
}
// Apply cooldown
if (spec.AbilityClass.CooldownEffect) {
const cooldownSpec = MakeOutgoingSpec(spec.AbilityClass.CooldownEffect, spec.Level);
ApplyGameplayEffectToSelf(cooldownSpec);
}
// Grant activation tags via an auto-generated Infinite Effect.
// Direct tag mutation is prohibited (§3.1); all tag state flows through Effects.
if (!spec.AbilityClass.ActivationOwnedTags.IsEmpty()) {
const ownedTagsSpec = MakeOwnedTagsEffect(spec.AbilityClass.ActivationOwnedTags, spec.Level);
spec.ActiveOwnedTagsHandle = ApplyGameplayEffectToSelf(ownedTagsSpec);
}
return true;
}
8.5 Costs and Cooldowns as Effects
Costs and Cooldowns are NOT separate variables but are implemented as specialized Gameplay Effects.
Cost Effect Pattern
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/gameplay_effect.json
Name: "GE_Fireball_Cost"
DurationPolicy: Instant
Modifiers:
- Attribute: "Mana"
Operation: Add
Magnitude:
Type: ScalableFloat
Value: -50.0 # Negative to subtract
Cooldown Effect Pattern
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/gameplay_effect.json
Name: "GE_Fireball_Cooldown"
DurationPolicy: HasDuration
Duration:
Type: ScalableFloat
Value: 5.0 # 5 second cooldown
GrantedTags:
- "Cooldown.Ability.Fireball"
This pattern enables external modification of costs and cooldowns. For example, a "Mana Efficiency" buff could apply a multiplier to all cost effects:
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/gameplay_effect.json
Name: "GE_ManaEfficiency_Buff"
DurationPolicy: HasDuration
Duration:
Type: ScalableFloat
Value: 30.0
Modifiers:
- Attribute: "ManaCostMultiplier"
Operation: Multiply
Magnitude:
Type: ScalableFloat
Value: -0.25 # -25% mana cost
8.6 Cancellation and Interruption
Abilities may be cancelled by:
-
Self-Cancellation: Ability logic calls EndAbility(true)
-
External Cancel: Another system calls CancelAbility on the GC
-
Cancel Tags: An Effect grants a tag in the Ability’s
CancelAbilitiesWithTagsset -
Owner Death: Owner’s Health reaches zero
function CancelAbility(handle: AbilitySpecHandle): void {
const spec = GetAbilitySpec(handle);
if (!spec.IsActive) return;
// Remove activation tags by removing the Effect that granted them.
// Direct tag mutation is prohibited (§3.1).
if (spec.ActiveOwnedTagsHandle) {
RemoveActiveGameplayEffect(spec.ActiveOwnedTagsHandle);
spec.ActiveOwnedTagsHandle = undefined;
}
// Call ability's end handler
spec.AbilityInstance.EndAbility(true /* wasCancelled */);
// Cleanup active tasks
CancelAllAbilityTasks(handle);
spec.IsActive = false;
}
8.7 Schema Definition
Ability:
Name: string # Required: Unique identifier
Tags:
AbilityTags: [string] # Tags describing this ability
BlockedByTags: [string] # Tags that block activation
BlockAbilitiesWithTags: [string] # Tags blocked while active
CancelAbilitiesWithTags: [string] # Tags cancelled on activation
ActivationRequiredTags: [string] # Required for activation
ActivationBlockedTags: [string] # Block activation if present
ActivationOwnedTags: [string] # Granted while active
Cost: string # Effect name for cost
Cooldown: string # Effect name for cooldown
Tasks: # Sequential task definitions
- Type: string # Task type name
Params: object # Task-specific parameters
Metadata:
DisplayName: string
Description: string
Icon: string
9. Gameplay Effects
9.1 Effect Structure
A Gameplay Effect defines a modification to an Actor’s state. Effects are data-only definitions that SHOULD NOT be subclassed.
struct GameplayEffect {
/** Unique identifier */
Name: string;
/** Duration behavior */
DurationPolicy: DurationPolicy;
/** Duration value (if applicable) */
Duration?: MagnitudeDefinition;
/** Periodic execution settings */
Period?: PeriodicSettings;
/** Attribute modifications */
Modifiers: Modifier[];
/** Complex calculations */
Executions: ExecutionCalculation[];
/** Tags granted while active */
GrantedTags: Tag[];
/** Tags required on target for application */
ApplicationRequiredTags: Tag[];
/** Abilities granted while active */
GrantedAbilities: AbilityGrant[];
/** Execution policy for multiple instances */
ExecutionPolicy: ExecutionPolicy;
/**
* Override conflict resolution priority.
* When multiple active effects apply an Override modifier to the same
* Attribute, the effect with the highest Priority value wins.
* On equal Priority, last-applied wins (LIFO).
* Defaults to 0. Negative values are valid.
*/
Priority: integer;
/** Gameplay cue tags */
GameplayCues: Tag[];
}
9.2 Duration Policies
| Policy | Base Value | Current Value | Persistence |
|---|---|---|---|
|
Modified |
Recalculated |
Permanent change |
|
Unchanged |
Modified |
Temporary (until expiry) |
|
Unchanged |
Modified |
Temporary (until removed) |
- Instant Effects
-
Modify the Base Value immediately and permanently. The Effect does not remain "active" after application. Classic examples: damage, healing, permanent stat increases.
- HasDuration Effects
-
Modify the Current Value for a specified duration. When the timer expires, the modifier is removed and the attribute reverts. Classic examples: buffs, debuffs, temporary bonuses.
- Infinite Effects
-
Modify the Current Value indefinitely until explicitly removed. Classic examples: passive auras, equipment bonuses, persistent status effects.
9.3 Periodic Execution
Effects with duration (HasDuration or Infinite) MAY execute periodically:
struct PeriodicSettings {
/** Time between executions */
Period: float;
/** Execute immediately on application? */
ExecuteOnApplication: boolean;
}
Periodic effects behave like repeated Instant effects within a duration container:
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/gameplay_effect.json
Name: "GE_Poison"
DurationPolicy: HasDuration
Duration:
Type: ScalableFloat
Value: 10.0
Period:
Period: 1.0
ExecuteOnApplication: false
Modifiers:
- Attribute: "Health"
Operation: Add
Magnitude:
Type: ScalableFloat
Value: -5.0 # 5 damage per second
9.4 Modifier Specification
9.4.1 Operations
| Operation | Semantics | Pipeline Step | Magnitude convention |
|---|---|---|---|
|
Pre-multiply flat additive |
Step 2 |
Absolute delta (e.g., |
|
Post-multiply flat additive |
Step 5 (very rare) |
Absolute delta |
|
Channel-aggregated bonus |
Step 4 |
Signed bonus: |
|
Replace value |
Step 6 |
Absolute replacement value |
Note: There is no
Divideoperation. A 50% reduction is expressed asMultiplywith magnitude−0.5(i.e., a −50% penalty). This eliminates the divide-by-zero edge case.
struct Modifier {
/** Target attribute */
Attribute: AttributeReference;
/**
* Modification operation.
* - Add: Pre-multiply flat additive (pipeline step 2)
* - AddPost: Post-multiply flat additive (pipeline step 7; very rare)
* - Multiply: Multiplicative factor (pipeline step 6)
* - Override: Replace the computed value entirely (pipeline step 8)
*/
Operation: ModifierOperation;
/** Magnitude calculation */
Magnitude: MagnitudeDefinition;
/**
* Optional aggregation channel name for `Multiply` modifiers.
*
* Semantics (see §5.3 Channel Aggregation for the full formula):
* - Modifiers with the SAME Channel add their bonuses together before
* the channel's effective factor (1 + sum) is computed.
* - Modifiers in DIFFERENT Channels produce independent factors that
* multiply against each other.
* - A modifier with no Channel is in its own implicit singleton channel,
* contributing independently.
*
* Example — two gear bonuses and one legendary power:
* GE_FireDmg: Multiply +0.20 Channel:"DamageBonuses"
* GE_EliteDmg: Multiply +0.15 Channel:"DamageBonuses"
* GE_Legendary: Multiply +0.50 Channel:"LegendaryPowers"
* → effective factor = (1 + 0.20 + 0.15) × (1 + 0.50) = 1.35 × 1.50 = 2.025
* vs. naive stacking: 1.20 × 1.15 × 1.50 = 2.07 (higher, causes power creep)
*
* Ignored on `Add`, `AddPost`, and `Override` modifiers.
*/
Channel?: string;
}
9.4.2 Magnitude Calculation Types
- ScalableFloat
-
Static or curve-based value.
Magnitude:
Type: ScalableFloat
Value: 25.0 # Static value
# OR
Curve: "DamageCurve" # Curve lookup
CurveInput: "Level" # Curve x-axis
- AttributeBased
-
Derived from another attribute.
Magnitude:
Type: AttributeBased
BackingAttribute: "Strength"
Source: Target # Source | Target
Coefficient: 1.5
PreMultiplyAdditive: 0.0
PostMultiplyAdditive: 10.0
# Result = (AttributeValue + PreAdd) * Coefficient + PostAdd
- CustomCalculation
-
Custom Modifier Magnitude Calculator (MMC).
Magnitude:
Type: CustomCalculation
CalculatorClass: "MMC_CriticalDamage"
- SetByCaller
-
Runtime-provided value via EffectSpec.
Magnitude:
Type: SetByCaller
DataTag: "Damage.Base" # Lookup key
Usage:
const spec = MakeOutgoingSpec(damageEffect, level);
spec.SetByCallerMagnitude("Damage.Base", calculatedDamage);
ApplyGameplayEffectToTarget(target, spec);
9.5 Execution Calculations
Execution Calculations provide full access to source and target attributes for complex, multi-attribute logic.
abstract class ExecutionCalculation {
/** Attributes to capture from source */
SourceCaptureDefinitions: AttributeCapture[];
/** Attributes to capture from target */
TargetCaptureDefinitions: AttributeCapture[];
/** Perform the calculation */
abstract Execute(
source: CapturedAttributes,
target: CapturedAttributes,
context: EffectContext
): ModifierResult[];
}
struct AttributeCapture {
Attribute: AttributeReference;
CaptureTime: CaptureTime; // OnApplication | OnExecution
}
Capture vs Snapshot Semantics
-
OnApplication: Attribute value is captured when Effect is first applied -
OnExecution: Attribute value is captured each time Effect executes
Example: Armor Penetration Calculation
class ExecCalc_PhysicalDamage extends ExecutionCalculation {
SourceCaptureDefinitions = [
{ Attribute: "AttackPower", CaptureTime: OnExecution },
{ Attribute: "ArmorPenetration", CaptureTime: OnExecution }
];
TargetCaptureDefinitions = [
{ Attribute: "Armor", CaptureTime: OnExecution }
];
Execute(source, target, context): ModifierResult[] {
const attackPower = source.Get("AttackPower");
const armorPen = source.Get("ArmorPenetration");
const targetArmor = target.Get("Armor");
const effectiveArmor = Math.max(0, targetArmor - armorPen);
const damageReduction = effectiveArmor / (effectiveArmor + 100);
const finalDamage = attackPower * (1 - damageReduction);
return [{
Attribute: "Health",
Operation: Add,
Magnitude: -finalDamage
}];
}
}
9.6 Execution Policies
Execution Policies define how multiple instances of the same Effect interact. This model replaces traditional "stacking" concepts with clearer behavioral semantics.
| Policy | Behavior |
|---|---|
|
All instances execute simultaneously; magnitude stacks N times |
|
Instances queue; executes one after another |
|
Single logical instance; durations merge (earliest start to latest end) |
RunInParallel
Each instance of the effect runs simultaneously, applying N times the magnitude.
Time ───────────────────────────────────▶ Instance 1: ████████████████ Instance 2: ████████████████ Instance 3: ████████████████ Combined magnitude at t=5: 3× base
Use case: Stackable damage-over-time effects, multiple buff sources
RunInSequence
Instances queue and execute one after another.
Time ───────────────────────────────────▶ Instance 1: ████████████████ Instance 2: ████████████████ Instance 3: ████████████████
Use case: Channeled effects, crowd control chains
Chaining mechanism: The GC owns the queue for each RunInSequence effect class. When the active instance’s duration expires (or it is manually removed), the GC automatically dequeues and begins the next instance, resetting the duration timer. Ability authors do not manage this transition; applying the same effect class while one is already active is sufficient to enqueue. The OnEffectApplied / OnEffectRemoved delegates fire for each instance individually, so callers can observe the moment one stun ends and the next begins.
RunInMerge
Multiple applications merge into a single logical instance with combined duration.
Time ───────────────────────────────────▶ Instance 1: ████████████████ Instance 2: ████████████████ Instance 3: ████████████████ Merged: ████████████████████████████
Use case: Buff refreshing, grace periods
9.7 Tag Grants
Effects MAY grant Tags while active:
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/gameplay_effect.json
Name: "GE_Burning"
DurationPolicy: HasDuration
Duration:
Type: ScalableFloat
Value: 5.0
GrantedTags:
- "State.Debuff.Burning"
- "State.Element.Fire"
Modifiers:
- Attribute: "Health"
Operation: Add
Magnitude:
Type: ScalableFloat
Value: -10.0
When the Effect is applied:
1. AddTag is called for each Granted Tag — grant counts increment
2. OnTagChanged is dispatched only for tags whose count transitions 0 → 1
3. Gameplay Cues are triggered only on that same 0 → 1 transition
When the Effect is removed (duration expires or manual removal):
1. RemoveTag is called for each Granted Tag — grant counts decrement
2. OnTagChanged is dispatched only for tags whose count transitions 1 → 0
3. Looping Gameplay Cues are stopped only on that same 1 → 0 transition
Consequence for concurrent Effects: if two Effects both grant State.Debuff.Burning, the tag’s count reaches 2. Removing the first Effect decrements to 1 — no event, no Cue change, the character remains visually on fire. Only removing the second Effect decrements to 0, dispatches OnTagChanged, and stops the looping Cue. This is the correct behaviour and falls out automatically from ref-counting.
9.8 Ability Grants
Effects MAY grant Abilities while active:
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/gameplay_effect.json
Name: "GE_FireSword_Equipped"
DurationPolicy: Infinite
GrantedAbilities:
- AbilityClass: "GA_FlameStrike"
Level: 1
InputID: "Ability.Weapon.Special"
RemoveOnEffectRemoval: true
This pattern enables equipment-based abilities where unequipping the item removes the Effect and consequently the granted Ability.
9.9 EffectSpec and EffectContext
EffectSpec Structure
struct EffectSpec {
/** Reference to effect definition */
EffectClass: GameplayEffectClass;
/** Level for magnitude calculations */
Level: number;
/** Application context */
Context: EffectContextHandle;
/** SetByCaller magnitude overrides */
SetByCallerMagnitudes: Map<string, float>;
/** Duration override (if any) */
DurationOverride?: float;
/** Period override (if any) */
PeriodOverride?: float;
}
EffectContext Structure
struct EffectContext {
/** GC that created this effect */
InstigatorGC: GameplayController;
/** Actor that caused this effect */
EffectCauser: Actor;
/** Ability that applied this effect (if any) */
SourceAbility?: GameplayAbility;
/** Object that was the origin (projectile, etc.) */
SourceObject?: Object;
/** Hit result for physics-based effects */
HitResult?: HitResult;
/** World location for positional effects */
WorldOrigin?: Vector3;
}
Handle Patterns
Handles provide lightweight references to specs and active effects:
struct EffectSpecHandle {
Data: SharedPtr<EffectSpec>;
}
struct ActiveEffectHandle {
Handle: number;
bPassedFiltersAndWasExecuted: boolean;
}
9.10 Schema Definition
# GameplayEffect Definition Schema
type: object
required:
- Name
- DurationPolicy
properties:
Name:
type: string
description: Unique effect identifier
DurationPolicy:
type: string
enum:
- Instant
- HasDuration
- Infinite
Duration:
type: object
properties:
Type:
type: string
enum:
- ScalableFloat
- AttributeBased
- SetByCaller
Value:
type: number
Period:
type: object
properties:
Period:
type: number
minimum: 0
ExecuteOnApplication:
type: boolean
default: false
ExecutionPolicy:
type: string
enum:
- RunInParallel
- RunInSequence
- RunInMerge
default: RunInParallel
Priority:
type: integer
default: 0
description: Override conflict priority. Highest value wins when multiple Override modifiers target the same Attribute. Equal priority resolves by last-applied (LIFO).
Modifiers:
type: array
items:
type: object
required:
- Attribute
- Operation
- Magnitude
properties:
Attribute:
type: string
Operation:
type: string
enum:
- Add
- AddPost
- Multiply
- Override
Magnitude:
type: object
Channel:
type: string
description: >
Aggregation channel for Multiply modifiers. Modifiers sharing a
Channel add their bonuses; channels multiply against each other.
Omit to treat this modifier as an isolated singleton channel.
Ignored on Add, AddPost, and Override operations.
GrantedTags:
type: array
items:
type: string
GrantedAbilities:
type: array
items:
type: object
properties:
AbilityClass:
type: string
Level:
type: integer
default: 1
InputID:
type: string
RemoveOnEffectRemoval:
type: boolean
default: true
GameplayCues:
type: array
items:
type: string
Part III: Asynchronous Execution
10. Ability Tasks
10.1 Purpose and Design
Ability Tasks are specialized asynchronous nodes that pause ability execution until a specific trigger condition is met. Tasks enable complex, multi-stage abilities to be written in a linear, readable fashion while executing asynchronously across frames or network ticks.
Tasks leverage the Observer design pattern for efficiency. Instead of polling a condition every frame, the ability registers a task and goes dormant. When the trigger condition is met, the task "wakes up" the ability and execution continues.
10.2 Task Lifecycle
┌─────────────┐
│ Inactive │
└──────┬──────┘
│ Instantiate
▼
┌─────────────┐
│ Ready │
└──────┬──────┘
│ Activate
▼
┌─────────────┐ Tick (if needed)
┌───▶│ Active │◀────────────────┐
│ └──────┬──────┘ │
│ │ │
│ ├────────────────────────┘
│ │ Trigger/Complete
│ ▼
│ ┌─────────────┐
│ │ Completed │
│ └─────────────┘
│
│ ┌─────────────┐
└────│ Cancelled │
└─────────────┘
Instantiation: Task is created with configuration parameters Activation: Task registers with relevant systems (timers, events, physics) Tick (optional): Some tasks require per-frame updates Completion: Trigger condition met; ability execution resumes Cancellation: Task is aborted (ability cancelled, owner died)
10.3 Predefined Task Categories
| Category | Trigger | Example Tasks |
|---|---|---|
Temporal |
Timer expiry |
WaitDelay, WaitGameTime |
Event-Based |
Gameplay event |
WaitGameplayEvent, WaitTagChanged |
Input-Based |
Input state change |
WaitInputRelease, WaitInputPressed |
State-Based |
Tag change |
WaitTagAdded, WaitTagRemoved |
Spatial |
Collision/overlap |
WaitOverlap, WaitForTarget |
Animation |
Montage notify |
WaitAnimationEvent, WaitMontageEnded |
WaitDelay
Waits for a specified duration.
class WaitDelay extends AbilityTask {
Duration: float;
OnActivate(): void {
this.StartTimer(this.Duration);
}
OnTimerComplete(): void {
this.Completed.Broadcast();
this.EndTask();
}
}
WaitGameplayEvent
Waits for a gameplay event with a matching tag.
class WaitGameplayEvent extends AbilityTask {
EventTag: Tag;
OnlyTriggerOnce: boolean;
OnActivate(): void {
this.Owner.OnGameplayEvent.Subscribe(this.EventTag, this.OnEvent);
}
OnEvent(payload: GameplayEventData): void {
this.EventReceived.Broadcast(payload);
if (this.OnlyTriggerOnce) {
this.EndTask();
}
}
}
WaitInputRelease
Waits for an input action to be released.
class WaitInputRelease extends AbilityTask {
InputID: InputID;
OnActivate(): void {
this.InputSystem.OnInputReleased.Subscribe(this.InputID, this.OnRelease);
}
OnRelease(heldDuration: float): void {
this.Released.Broadcast(heldDuration);
this.EndTask();
}
}
WaitTagAdded
Waits for a specific tag to be added to the owner.
class WaitTagAdded extends AbilityTask {
WaitTag: Tag;
OnActivate(): void {
if (this.Owner.Tags.MatchesTag(this.WaitTag)) {
this.TagFound.Broadcast();
this.EndTask();
return;
}
this.Owner.OnTagChanged.Subscribe(this.OnTagChanged);
}
OnTagChanged(tag: Tag, added: boolean): void {
if (added && this.WaitTag.Matches(tag)) {
this.TagFound.Broadcast();
this.EndTask();
}
}
}
10.4 Custom Task Implementation
Custom tasks MUST:
-
Extend the base AbilityTask class
-
Implement OnActivate() for setup
-
Implement cleanup in OnEndTask()
-
Provide delegate/event outputs for ability continuation
-
Handle cancellation gracefully
class WaitForHealthThreshold extends AbilityTask {
Threshold: float;
Comparison: ComparisonType; // LessThan | LessEqual | Greater | GreaterEqual
OnActivate(): void {
// Check immediately
if (this.CheckThreshold()) {
this.ThresholdReached.Broadcast();
this.EndTask();
return;
}
// Subscribe to attribute changes
this.Owner.OnAttributeChanged.Subscribe("Health", this.OnHealthChanged);
}
OnHealthChanged(event: AttributeChangedEvent): void {
if (this.CheckThreshold()) {
this.ThresholdReached.Broadcast();
this.EndTask();
}
}
CheckThreshold(): boolean {
const health = this.Owner.GetAttributeValue("Health");
switch (this.Comparison) {
case LessThan: return health < this.Threshold;
case LessEqual: return health <= this.Threshold;
case Greater: return health > this.Threshold;
case GreaterEqual: return health >= this.Threshold;
}
}
OnEndTask(): void {
this.Owner.OnAttributeChanged.Unsubscribe("Health", this.OnHealthChanged);
}
}
10.5 Task Ownership and Cleanup
Tasks are owned by the Ability that created them. When an Ability ends:
-
All active Tasks are cancelled
-
Task event subscriptions are cleared
-
Task resources are released
function EndAbility(wasCancelled: boolean): void {
// Cancel all active tasks
for (const task of this.ActiveTasks) {
task.Cancel();
}
this.ActiveTasks.Clear();
// Remove activation-owned tags by removing the Effect that granted them.
// This is the normal (non-cancelled) end path; CancelAbility handles the cancel path.
const spec = GC.GetAbilitySpec(this.Handle);
if (spec?.ActiveOwnedTagsHandle) {
GC.RemoveActiveGameplayEffect(spec.ActiveOwnedTagsHandle);
spec.ActiveOwnedTagsHandle = undefined;
}
// Continue with ability end logic...
}
11. Input Integration
11.1 Command Pattern Overview
The UGAS input system implements the Command pattern to decouple hardware inputs from ability execution. This separation enables:
-
Controller remapping without code changes
-
Platform-specific input schemes
-
Input buffering and queuing
-
Combo system integration
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Hardware │─────▶│ Input │─────▶│ Input │
│ Input │ │ Action │ │ ID │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
▼
┌──────────────┐
│ Ability │
│ Activation │
└──────────────┘
11.2 Input Action to Ability Mapping
Abilities are bound to Input IDs, which are mapped from Input Actions:
InputMapping:
Actions:
- Action: "IA_PrimaryAttack"
InputID: "Ability.Attack.Primary"
KeyBindings:
- Key: "MouseLeft"
- Key: "GamepadRightTrigger"
- Action: "IA_SecondaryAttack"
InputID: "Ability.Attack.Secondary"
KeyBindings:
- Key: "MouseRight"
- Key: "GamepadLeftTrigger"
- Action: "IA_Ability1"
InputID: "Ability.Slot.1"
KeyBindings:
- Key: "Q"
- Key: "GamepadFaceLeft"
Ability grants include optional Input ID binding:
GC.GrantAbility(
abilityClass: GA_Fireball,
level: 1,
inputID: "Ability.Slot.1"
);
11.3 Input Buffering
Input buffering allows players to queue inputs during animations or recovery frames:
struct InputBufferConfig {
/** Enable input buffering */
Enabled: boolean;
/** Buffer window in seconds */
BufferWindow: float;
/** Maximum buffered inputs */
MaxBufferSize: number;
}
When input buffering is enabled:
-
Input arrives during "blocked" state (animation, recovery)
-
Input is stored in buffer with timestamp
-
When block ends, buffered inputs are processed in order
-
Expired inputs (beyond buffer window) are discarded
function ProcessBufferedInputs(): void {
const now = GetCurrentTime();
// Remove expired inputs
this.InputBuffer = this.InputBuffer.filter(
input => now - input.Timestamp < this.BufferWindow
);
// Process valid inputs
for (const input of this.InputBuffer) {
if (TryActivateAbilityByInputID(input.InputID)) {
break; // Successfully activated, stop processing
}
}
this.InputBuffer.Clear();
}
11.4 Remapping Support
Input mappings SHOULD be externalizable and modifiable at runtime:
interface IInputMapper {
/** Get InputID for an action */
GetInputIDForAction(action: InputAction): InputID;
/** Remap an action to a new key */
RemapAction(action: InputAction, newKey: Key): void;
/** Reset to defaults */
ResetToDefaults(): void;
/** Save current mappings */
SaveMappings(): void;
/** Load saved mappings */
LoadMappings(): void;
}
Part IV: Feedback and Networking
12. Gameplay Cues
12.1 Design Philosophy
Gameplay Cues enforce strict separation between Mechanics and Aesthetics. This separation provides:
-
Server Optimization: Headless servers load no visual/audio resources
-
Client Customization: Visual settings don’t affect gameplay
-
Network Efficiency: Cues are not replicated; only trigger tags are
-
Platform Adaptation: Different platforms can have different cue implementations
12.2 Cue Trigger Mechanism
Cues are triggered by Tags following the GameplayCue.* convention:
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/gameplay_effect.json
Name: "GE_FireDamage"
DurationPolicy: Instant
Modifiers:
- Attribute: "Health"
Operation: Add
Magnitude:
Type: ScalableFloat
Value: -25.0
GameplayCues:
- "GameplayCue.Impact.Fire"
When the Effect is applied:
1. Server applies the Effect and modifies attributes
2. GameplayCue.Impact.Fire tag is communicated to clients
3. Clients' Cue Managers instantiate the fire impact VFX/SFX
12.3 Cue Types
Burst Cues (Fire-and-Forget) : Triggered once, play to completion, clean themselves up.
class GC_Impact_Fire extends GameplayCueBurst {
OnExecute(context: CueContext): void {
SpawnParticleSystem("PS_FireImpact", context.HitLocation);
PlaySound("SFX_FireImpact", context.HitLocation);
}
}
Looping Cues (Duration-Bound) : Persist while the triggering Effect is active.
class GC_Status_Burning extends GameplayCueLooping {
private ParticleComponent: ParticleSystem;
OnAdd(context: CueContext): void {
this.ParticleComponent = SpawnLoopingParticle("PS_BurningLoop", context.Target);
StartLoopingSound("SFX_BurningLoop", context.Target);
}
OnRemove(): void {
this.ParticleComponent.Destroy();
StopLoopingSound("SFX_BurningLoop");
}
}
12.4 Cue Manager
The Cue Manager is a client-side system responsible for:
-
Receiving cue trigger notifications
-
Matching tags to Cue implementations
-
Instantiating and managing Cue resources
-
Pooling frequently-used Cues for performance
class GameplayCueManager {
private CueRegistry: Map<Tag, GameplayCueClass>;
private ActiveLoopingCues: Map<ActiveEffectHandle, GameplayCue[]>;
HandleCueNotify(tag: Tag, context: CueContext, type: CueNotifyType): void {
const cueClass = this.CueRegistry.get(tag);
if (!cueClass) return;
switch (type) {
case Execute:
const burstCue = this.InstantiateCue(cueClass);
burstCue.OnExecute(context);
break;
case Add:
const loopingCue = this.InstantiateCue(cueClass);
loopingCue.OnAdd(context);
this.ActiveLoopingCues.get(context.EffectHandle).push(loopingCue);
break;
case Remove:
const activeCues = this.ActiveLoopingCues.get(context.EffectHandle);
for (const cue of activeCues) {
cue.OnRemove();
}
this.ActiveLoopingCues.delete(context.EffectHandle);
break;
}
}
}
12.5 Server Optimization
On headless servers:
-
Cue Manager is NOT instantiated
-
Cue assets are NOT loaded
-
Cue trigger tags are still processed for replication
-
Memory footprint is significantly reduced
Implementations SHOULD support a headless mode flag:
if (!IsHeadlessServer()) {
this.CueManager = new GameplayCueManager();
this.CueManager.LoadCueAssets();
}
13. Network Replication
13.1 Replication Architecture
UGAS defines a client-server replication model where:
-
The server is authoritative for all gameplay state
-
Clients receive replicated state updates
-
Clients may predict state changes locally
-
Server reconciles predicted state with authoritative state
┌──────────────────┐ ┌──────────────────┐ │ SERVER │ │ CLIENT │ │ │ │ │ │ ┌────────────┐ │ Replicate │ ┌────────────┐ │ │ │ GC │──┼───────────▶│ │ GC │ │ │ │(Authority) │ │ │ │ (Proxy) │ │ │ └────────────┘ │ │ └────────────┘ │ │ │ │ │ │ │ Predict │ │ │ │◀───────────┼──(Local Input) │ │ │ │ │ │ │ Reconcile │ │ │ │───────────▶│ │ └──────────────────┘ └──────────────────┘
13.2 Replication Modes
| Mode | Effects | Cues | Tags | Attributes | Use Case |
|---|---|---|---|---|---|
|
None |
All |
All |
None |
AI entities, distant actors |
|
Owner only |
All |
All |
Owner only |
Player characters |
|
All |
All |
All |
All |
Single-player, debugging |
- Minimal Mode
-
Only Cue triggers and Tag changes are replicated. Effects and Attributes are server-only. Suitable for AI entities where clients don’t need full state.
- Mixed Mode
-
Full replication to the owning client; minimal replication to others. The standard mode for player characters in multiplayer games.
- Full Mode
-
Complete replication to all clients. Used for single-player games or debugging. Higher bandwidth cost.
13.3 Bandwidth Optimization
Delta Compression
Only changed values are transmitted:
struct ReplicatedAttributeSet {
/** Bitmask of changed attributes since last update */
DirtyMask: uint32;
/** Only changed attribute values */
ChangedValues: float[];
}
Dirty Bit Tracking
Attributes track their dirty state:
function SetBaseValue(attribute: Attribute, newValue: float): void {
if (attribute.BaseValue !== newValue) {
attribute.BaseValue = newValue;
attribute.bIsDirty = true;
this.DirtyAttributes.add(attribute);
}
}
Quantization
For bandwidth-critical scenarios, attribute values MAY be quantized:
struct QuantizedHealth {
/** 0-255 representing 0-100% health */
HealthPercent: uint8;
}
13.4 Client-Side Prediction
To eliminate network latency perception, clients predict ability outcomes locally:
function TryActivateAbility_Predicted(handle: AbilitySpecHandle): void {
// Generate prediction key
const predictionKey = GeneratePredictionKey();
// Predict locally
const success = TryActivateAbility_Local(handle, predictionKey);
if (success) {
// Store predicted state
this.PredictedActivations.set(predictionKey, {
Handle: handle,
Timestamp: GetCurrentTime(),
State: CaptureState()
});
// Send to server
Server_TryActivateAbility(handle, predictionKey);
}
}
13.5 Server Reconciliation
When server response differs from prediction:
function OnServerActivationResponse(
predictionKey: PredictionKey,
serverSuccess: boolean,
serverState: GameplayState
): void {
const prediction = this.PredictedActivations.get(predictionKey);
if (!prediction) return;
if (!serverSuccess) {
// Prediction was wrong - rollback
RollbackToState(prediction.State);
} else {
// Prediction was correct - reconcile minor differences
ReconcileState(serverState);
}
this.PredictedActivations.delete(predictionKey);
}
Rollback and Replay
For significant discrepancies:
-
Revert to last known authoritative state
-
Re-apply all inputs that occurred since that state
-
Blend visually to avoid jarring corrections
function RollbackAndReplay(
authoritativeState: GameplayState,
inputHistory: Input[]
): void {
// 1. Revert state
ApplyState(authoritativeState);
// 2. Replay inputs
for (const input of inputHistory) {
if (input.Timestamp > authoritativeState.Timestamp) {
SimulateInput(input);
}
}
// 3. Blend if needed
if (VisualDiscrepancy > Threshold) {
StartVisualBlend(currentVisual, newSimulatedState);
}
}
13.6 Replication Frequency Recommendations
| Actor Type | Update Rate | Notes |
|---|---|---|
Player Character (LAN / low-latency) |
60-100 Hz |
High frequency for responsive feel |
Player Character (mobile / high-latency) |
20-30 Hz |
Reduce to manage bandwidth; compensate with aggressive client-side prediction |
Important AI |
30-60 Hz |
Moderate frequency |
Distant Actors |
10-20 Hz |
Lower frequency acceptable |
Static Objects |
On Change |
Event-based only |
High-latency guidance: On connections with RTT > 150 ms (common on mobile or cross-region play), implementations SHOULD lower the player-character replication rate to 20-30 Hz and increase prediction window depth accordingly. Attribute and Tag state SHOULD be sent at a lower rate than position to prioritise movement responsiveness. Dead-reckoning or interpolation SHOULD be applied on the receiving end.
13.7 Effect Application Authorization
ApplyGameplayEffectToTarget is the primary mutation surface of the GC pipeline and therefore a critical security boundary in networked environments.
Core requirement
In any networked environment, a call to ApplyGameplayEffectToTarget that originates on a client MUST be validated by the server before the effect is executed on authoritative state. Clients MUST NOT be permitted to mutate server-authoritative GC state directly.
Validation pipeline
The server-side validation step MUST check, at minimum:
-
Instigator authority — the instigating GC is owned by the requesting client (or is a server-controlled entity).
-
Ability ownership — the effect is being applied as part of an ability that the instigator has been granted (i.e., the ability spec exists in the instigator’s granted-ability list).
-
Target reachability — the target GC is a legitimate target for the instigator at the time of application (range, line-of-sight, or game-rule checks as appropriate to the title).
-
Effect class whitelist — the effect class is one the ability is permitted to apply; implementations SHOULD reject arbitrary
EffectClassvalues supplied by the client.
If any check fails, the server MUST reject the application and MAY roll back any prediction the client has already applied locally (via the standard reconciliation path in §13.5).
Predicted applications
When a client applies an effect locally as part of a prediction (using a PredictionKey), the local application is speculative only. The authoritative application — or its rejection — is determined by the server. Implementations MUST treat predicted effect applications as unconfirmed until the server acknowledges the prediction key.
// Client: speculative application
const predictionKey = GeneratePredictionKey();
const specHandle = MakeOutgoingSpec(GE_Damage, level, predictionKey);
ApplyGameplayEffectToTarget(target.GC, specHandle, predictionKey);
// Effect is active locally, but flagged as predicted (unconfirmed).
// Server: receives the RPC, validates, then applies authoritatively
function Server_ApplyEffect(
instigatorGC: GameplayController,
targetGC: GameplayController,
specHandle: EffectSpecHandle,
predictionKey: PredictionKey
): void {
// 1. Validate instigator owns the ability that produced this spec
if (!ValidateInstigatorAuthority(instigatorGC, specHandle)) {
RejectPrediction(predictionKey);
return;
}
// 2. Validate target is reachable / eligible
if (!ValidateTarget(instigatorGC, targetGC, specHandle)) {
RejectPrediction(predictionKey);
return;
}
// Authoritative application — triggers replication to all clients
ApplyGameplayEffectToTarget(targetGC, specHandle);
ConfirmPrediction(predictionKey);
}
Authoritative-only effects
Some effects MUST only ever be applied by the server (e.g., spawn effects, death effects, anti-cheat corrections). These effects SHOULD be tagged with Gameplay.Effect.AuthoritativeOnly and implementations MUST refuse to apply them on a client even if a prediction key is present.
Part V: Reference Implementation
14. Implementation Examples
14.1 Basic Damage Application
Effect Definition
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/gameplay_effect.json
Name: "GE_BasicDamage"
DurationPolicy: Instant
Modifiers:
- Attribute: "Health"
Operation: Add
Magnitude:
Type: SetByCaller
DataTag: "Damage.Amount"
GameplayCues:
- "GameplayCue.Impact.Generic"
Application Flow
function ApplyDamage(target: GameplayController, damage: float): void {
// 1. Create context
const context = this.GC.MakeEffectContext();
context.SetEffectCauser(this.Owner);
// 2. Create spec
const spec = this.GC.MakeOutgoingSpec(GE_BasicDamage, 1, context);
// 3. Set damage amount
spec.SetByCallerMagnitude("Damage.Amount", -damage); // Negative for subtraction
// 4. Apply to target
const handle = this.GC.ApplyGameplayEffectToTarget(target, spec);
// 5. Check success
if (handle.IsValid()) {
OnDamageApplied(target, damage);
}
}
Attribute Change Handling
class HealthObserver implements IAttributeChangeObserver {
OnAttributeChanged(event: AttributeChangedEvent): void {
const oldValue = event.OldValue;
const newValue = event.NewValue;
// Update health bar UI
this.HealthBar.SetPercent(newValue / this.MaxHealth);
// Show damage number
const damage = oldValue - newValue;
if (damage > 0) {
SpawnDamageNumber(damage, event.Target.GetLocation());
}
// Check for death
if (newValue <= 0) {
OnDeath(event.CausalEffect);
}
}
}
14.2 Buff/Debuff with Duration
Temporary Modifier
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/gameplay_effect.json
Name: "GE_StrengthBuff"
DurationPolicy: HasDuration
Duration:
Type: ScalableFloat
Value: 30.0
ExecutionPolicy: RunInMerge # Refresh duration on reapplication
Modifiers:
- Attribute: "AttackPower"
Operation: Multiply
Magnitude:
Type: ScalableFloat
Value: 0.25 # +25% damage
GrantedTags:
- "Status.Buff.Strength"
GameplayCues:
- "GameplayCue.Status.StrengthBuff"
Visual Cue Integration
class GC_Status_StrengthBuff extends GameplayCueLooping {
private AuraEffect: ParticleSystem;
private BuffIcon: UIWidget;
OnAdd(context: CueContext): void {
// Spawn visual aura
this.AuraEffect = SpawnAttached(
"PS_StrengthAura",
context.Target,
"Spine"
);
// Show buff icon in UI
this.BuffIcon = ShowBuffIcon("Icon_Strength", context.Duration);
// Play activation sound
PlaySound("SFX_BuffActivate");
}
OnRemove(): void {
this.AuraEffect.Destroy();
this.BuffIcon.Remove();
PlaySound("SFX_BuffExpire");
}
}
14.3 Ability with Cast Time
Ability:
Name: "GA_Fireball"
Tags:
AbilityTags:
- "Ability.Type.Spell"
- "Ability.Element.Fire"
ActivationOwnedTags:
- "State.Casting"
CancelAbilitiesWithTags:
- "State.Stunned"
ActivationBlockedTags:
- "State.Silenced"
Cost: "GE_Fireball_Cost"
Cooldown: "GE_Fireball_Cooldown"
Task-Based Implementation
class GA_Fireball extends GameplayAbility {
CastTime: float = 1.5;
ProjectileClass: ProjectileClass;
ActivateAbility(context: AbilityContext): void {
// 1. Commit resources
if (!CommitAbility()) {
EndAbility(true);
return;
}
// 2. Play cast animation
PlayAnimation("Anim_CastFireball");
// 3. Wait for cast time
const waitTask = WaitDelay(this.CastTime);
waitTask.OnComplete.Subscribe(this.OnCastComplete);
// 4. Listen for interruption
const interruptTask = WaitTagAdded("State.Stunned");
interruptTask.OnTagFound.Subscribe(this.OnInterrupted);
}
OnCastComplete(): void {
// Spawn and launch projectile
const projectile = SpawnProjectile(
this.ProjectileClass,
this.GetAvatarLocation(),
this.GetAimDirection()
);
projectile.SetDamageEffect(GE_FireballDamage);
EndAbility(false);
}
OnInterrupted(): void {
// Play fizzle effect
TriggerCue("GameplayCue.Ability.Interrupted");
EndAbility(true);
}
}
14.4 Complex Calculation (Armor Penetration)
class ExecCalc_ArmorPenetration extends ExecutionCalculation {
SourceCaptureDefinitions = [
{ Attribute: "AttackPower", CaptureTime: OnExecution },
{ Attribute: "ArmorPenetrationFlat", CaptureTime: OnExecution },
{ Attribute: "ArmorPenetrationPercent", CaptureTime: OnExecution },
{ Attribute: "CriticalChance", CaptureTime: OnExecution },
{ Attribute: "CriticalDamage", CaptureTime: OnExecution }
];
TargetCaptureDefinitions = [
{ Attribute: "Armor", CaptureTime: OnExecution },
{ Attribute: "DamageReduction", CaptureTime: OnExecution }
];
Execute(source, target, context): ModifierResult[] {
// Get source stats
const attackPower = source.Get("AttackPower");
const armorPenFlat = source.Get("ArmorPenetrationFlat");
const armorPenPercent = source.Get("ArmorPenetrationPercent");
const critChance = source.Get("CriticalChance");
const critDamage = source.Get("CriticalDamage");
// Get target stats
const targetArmor = target.Get("Armor");
const damageReduction = target.Get("DamageReduction");
// Calculate effective armor
const armorAfterFlat = Math.max(0, targetArmor - armorPenFlat);
const effectiveArmor = armorAfterFlat * (1 - armorPenPercent);
// Armor damage reduction formula
const armorDR = effectiveArmor / (effectiveArmor + 100);
// Base damage
let damage = attackPower * (1 - armorDR);
// Apply critical hit
if (RandomFloat() < critChance) {
damage *= critDamage;
context.SetTag("Hit.Critical");
}
// Apply flat damage reduction
damage *= (1 - damageReduction);
return [{
Attribute: "Health",
Operation: Add,
Magnitude: -damage
}];
}
}
15. Case Studies
15.1 Platformer (Mario-style)
Movement Attributes
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/attribute_set.json
Name: "PlatformerMovementSet"
Attributes:
- Name: "GravityScale"
DefaultBaseValue: 1.0
Category: Statistic
- Name: "JumpVelocity"
DefaultBaseValue: 1200.0
Category: Statistic
- Name: "AirControl"
DefaultBaseValue: 0.65
Category: Statistic
Clamping:
Min: 0.0
Max: 1.0
- Name: "CoyoteTimeDuration"
DefaultBaseValue: 0.15
Category: Statistic
- Name: "JumpBufferDuration"
DefaultBaseValue: 0.1
Category: Statistic
- Name: "VerticalVelocity"
DefaultBaseValue: 0.0
Category: Meta
- Name: "HorizontalSpeed"
DefaultBaseValue: 600.0
Category: Statistic
Jump Ability with Variable Height
class GA_Jump extends GameplayAbility {
// Handle to the Infinite Effect that grants State.InAir while airborne.
// State.Grounded is managed by the physics subsystem via its own Effect,
// not by this ability — tag ownership follows responsibility.
private inAirHandle: ActiveEffectHandle;
ActivateAbility(context: AbilityContext): void {
// Check grounded OR coyote time
if (!this.Owner.Tags.MatchesTag("State.Grounded") &&
!this.Owner.Tags.MatchesTag("Status.CoyoteTime")) {
EndAbility(true);
return;
}
// Grant State.InAir via an Effect — direct tag mutation is prohibited (§3.1).
// GE_InAir is an Infinite Effect with GrantedTags: ["State.InAir"].
// The physics subsystem independently removes its GE_Grounded effect
// when it detects the character is no longer on the ground.
const inAirSpec = MakeOutgoingSpec(GE_InAir, 1);
this.inAirHandle = ApplyGameplayEffectToSelf(inAirSpec);
const jumpVelocity = this.Owner.GetAttribute("JumpVelocity");
ApplyImpulse(Vector3.Up * jumpVelocity);
// Variable height: wait for button release
const releaseTask = WaitInputRelease("Jump");
releaseTask.OnReleased.Subscribe(this.OnJumpReleased);
// Wait for landing
const landTask = WaitGameplayEvent("Event.Landed");
landTask.OnEvent.Subscribe(this.OnLanded);
}
OnJumpReleased(heldDuration: float): void {
// Short press = cut jump short.
// VerticalVelocity is a GAS Attribute kept in sync by the physics Avatar,
// so this remains a pure GAS query — no direct physics coupling here.
if (this.Owner.GetAttribute("VerticalVelocity") > 0) {
// Apply gravity multiplier for shorter jump
const cutSpec = MakeOutgoingSpec(GE_JumpCut, 1);
ApplyGameplayEffectToSelf(cutSpec);
}
}
OnLanded(): void {
// Remove State.InAir by removing the Effect that granted it.
// The physics subsystem re-applies its GE_Grounded effect on landing.
RemoveActiveGameplayEffect(this.inAirHandle);
EndAbility(false);
}
}
Power-Up Effects
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/gameplay_effect.json
Name: "GE_SuperMushroom"
DurationPolicy: Infinite
GrantedTags:
- "State.PowerUp.Super"
Modifiers:
- Attribute: "Scale"
Operation: Multiply
Magnitude:
Type: ScalableFloat
Value: 1.0 # +100% (doubles size)
- Attribute: "Health"
Operation: Add
Magnitude:
Type: ScalableFloat
Value: 1.0 # Gain 1 hit point
GameplayCues:
- "GameplayCue.PowerUp.Super"
15.2 Racing (Forza-style)
Vehicle Attribute Sets
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/attribute_set.json
Name: "VehiclePerformanceSet"
Attributes:
- Name: "EngineTorque"
DefaultBaseValue: 400.0
Description: "Base torque in Nm"
- Name: "EngineRPM"
DefaultBaseValue: 0.0
Category: Meta
- Name: "MaxSpeed"
DefaultBaseValue: 250.0
Description: "Top speed in km/h"
- Name: "TireGripMultiplier"
DefaultBaseValue: 1.0
Category: Statistic
- Name: "AeroDownforce"
DefaultBaseValue: 100.0
Description: "Downforce coefficient"
- Name: "TireTemperature"
DefaultBaseValue: 80.0
Clamping:
Min: 20.0
Max: 150.0
Biome-Based Area Effects
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/gameplay_effect.json
Name: "GE_Biome_Mud"
DurationPolicy: Infinite
ApplicationRequiredTags:
- "Vehicle"
Modifiers:
- Attribute: "TireGripMultiplier"
Operation: Multiply
Magnitude:
Type: ScalableFloat
Value: -0.6 # -60% grip (retains 40% of normal grip)
- Attribute: "MaxSpeed"
Operation: Add
Magnitude:
Type: ScalableFloat
Value: -30.0 # Reduce top speed
GrantedTags:
- "Surface.Mud"
---
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/gameplay_effect.json
Name: "GE_Biome_Asphalt"
DurationPolicy: Infinite
ApplicationRequiredTags:
- "Vehicle"
Modifiers:
- Attribute: "TireGripMultiplier"
Operation: Override
Magnitude:
Type: ScalableFloat
Value: 1.0
GrantedTags:
- "Surface.Asphalt"
Physics Integration
class ExecCalc_VehicleTraction extends ExecutionCalculation {
SourceCaptureDefinitions = [
{ Attribute: "TireGripMultiplier", CaptureTime: OnExecution },
{ Attribute: "AeroDownforce", CaptureTime: OnExecution },
{ Attribute: "TireTemperature", CaptureTime: OnExecution },
{ Attribute: "CurrentSpeed", CaptureTime: OnExecution }
];
Execute(source, target, context): ModifierResult[] {
const baseGrip = source.Get("TireGripMultiplier");
const downforce = source.Get("AeroDownforce");
const tireTemp = source.Get("TireTemperature");
const speed = source.Get("CurrentSpeed");
// Downforce increases with speed squared
const downforceBonus = (downforce * speed * speed) / 100000;
// Tire temperature optimal range: 80-100
let tempMultiplier = 1.0;
if (tireTemp < 80) {
tempMultiplier = 0.7 + (tireTemp / 80) * 0.3;
} else if (tireTemp > 100) {
tempMultiplier = 1.0 - ((tireTemp - 100) / 50) * 0.3;
}
const effectiveTraction = baseGrip * (1 + downforceBonus) * tempMultiplier;
return [{
Attribute: "AvailableTraction",
Operation: Override,
Magnitude: effectiveTraction
}];
}
}
15.3 ARPG (Diablo-style)
Damage Bucket Architecture
The "Damage Bucket" system prevents linear power creep by organizing Multiply modifiers into named channels. Modifiers in the same channel add their bonuses; channels multiply against each other. Because the Channel mechanism is built into the modifier pipeline (§5.3), the bucket design is expressed declaratively in Effect YAML — no custom calculation code required for the stacking logic itself.
Three canonical buckets:
| Channel | What goes in it | Stacking |
|---|---|---|
|
Stat-derived scaling (e.g. Strength → +damage) |
Additive |
|
Conditional damage bonuses (fire, vs. elites, while healthy …) |
Additive |
|
Item set / legendary power multipliers |
Additive within, ×MainStat ×DamageBonuses across |
# GE_Weapon_FireSword.yaml — item that grants a fire damage bonus
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/gameplay_effect.json
Name: "GE_Weapon_FireSword"
DurationPolicy: Infinite
Modifiers:
- Attribute: "WeaponDamage"
Operation: Multiply
Magnitude:
Type: ScalableFloat
Value: 0.20 # +20% fire damage
Channel: "DamageBonuses"
# GE_Passive_EliteHunter.yaml — passive skill: +15% damage vs. elites
Name: "GE_Passive_EliteHunter"
DurationPolicy: Infinite
ApplicationRequiredTags:
- "Status.FightingElite"
Modifiers:
- Attribute: "WeaponDamage"
Operation: Multiply
Magnitude:
Type: ScalableFloat
Value: 0.15 # +15% damage vs. elites
Channel: "DamageBonuses"
# GE_Set_LegendaryPower.yaml — set bonus: +50% damage (its own channel)
Name: "GE_Set_LegendaryPower"
DurationPolicy: Infinite
Modifiers:
- Attribute: "WeaponDamage"
Operation: Multiply
Magnitude:
Type: ScalableFloat
Value: 0.50 # +50% legendary multiplier
Channel: "LegendaryPowers"
# GE_MainStat_Strength.yaml — applied by the attribute system per point of Strength
Name: "GE_MainStat_Strength"
DurationPolicy: Infinite
Modifiers:
- Attribute: "WeaponDamage"
Operation: Multiply
Magnitude:
Type: AttributeBased
BackingAttribute: "Strength"
Source: Source
Coefficient: 0.01 # +1% per Strength point
Channel: "MainStat"
With all four effects active (Strength 50, fire sword, elite hunter active, legendary set):
- MainStat channel factor: 1 + (0.01 × 50) = ×1.50
- DamageBonuses channel factor: 1 + 0.20 + 0.15 = ×1.35
- LegendaryPowers channel factor: 1 + 0.50 = ×1.50
- Final WeaponDamage multiplier: 1.50 × 1.35 × 1.50 = ×3.04
vs. naive unchanelled stacking: 1.50 × 1.20 × 1.15 × 1.50 = ×3.11 — a modest difference at low item counts that compounds severely at higher counts.
Conditional modifiers (e.g. the Vulnerability bonus) that depend on target state at hit-time still require an ExecutionCalculation, but only for the conditional logic — the stacking math is already handled by the pipeline:
class ExecCalc_ARPGDamage extends ExecutionCalculation {
Execute(source, target, context): ModifierResult[] {
// All bucket stacking is already resolved in WeaponDamage's Current Value.
const weaponDamage = source.Get("WeaponDamage");
// Only conditional logic needs to live here.
const vulnerabilityBonus = target.Tags.MatchesTag("Status.Vulnerable") ? 0.20 : 0.0;
return [{
Attribute: "Health",
Operation: Add,
Magnitude: -weaponDamage * (1 + vulnerabilityBonus)
}];
}
}
Combat Tag Queries
class GA_Whirlwind extends GameplayAbility {
ActivateAbility(context: AbilityContext): void {
// This ability tags
this.AbilityTags = ["Ability.Type.Melee", "DamageType.Physical"];
// Find targets in radius
const targets = GetActorsInRadius(this.Owner.Location, 500);
for (const target of targets) {
// Check immunities
if (target.Tags.MatchesTag("Immunity.Physical")) {
// Show immune text
SpawnFloatingText(target, "IMMUNE");
continue;
}
// Apply damage effect
const spec = MakeOutgoingSpec(GE_WhirlwindDamage, this.Level);
// Check for vulnerability bonus
if (target.Tags.MatchesTag("Status.Vulnerable")) {
spec.SetByCallerMagnitude("VulnerabilityBonus", 0.2);
}
ApplyGameplayEffectToTarget(target.GC, spec);
}
}
}
Procedural Item Effects
class ItemEquipSystem {
EquipItem(item: Item): void {
// Create infinite effect for item stats
const itemEffect = GenerateItemEffect(item);
// Apply effect
const handle = this.GC.ApplyGameplayEffectToSelf(itemEffect);
// Store handle for unequip
this.EquippedItemEffects.set(item.ID, handle);
// Grant item abilities
for (const ability of item.GrantedAbilities) {
this.GC.GrantAbility(ability.Class, ability.Level, ability.InputID);
}
}
GenerateItemEffect(item: Item): EffectSpec {
const effect = new GameplayEffect();
effect.DurationPolicy = Infinite;
// Add modifiers for each stat roll
for (const stat of item.Stats) {
effect.Modifiers.push({
Attribute: stat.AttributeName,
Operation: stat.Operation,
Magnitude: { Type: ScalableFloat, Value: stat.Value }
});
}
// Add item tag
effect.GrantedTags.push(`Item.Equipped.${item.Slot}`);
effect.GrantedTags.push(`Item.Type.${item.Type}`);
return MakeOutgoingSpec(effect, 1, MakeEffectContext());
}
}
15.4 Puzzle (2048-style)
Grid Cell Attributes
$schema: https://raw.githubusercontent.com/jbltx/ugas/v1.0.0-draft.1/schemas/attribute_set.json
Name: "PuzzleCellSet"
Attributes:
- Name: "CellValue"
DefaultBaseValue: 0.0
Category: Statistic
- Name: "GridX"
DefaultBaseValue: 0.0
Category: Meta
- Name: "GridY"
DefaultBaseValue: 0.0
Category: Meta
- Name: "MergePriority"
DefaultBaseValue: 0.0
Category: Meta
Move Ability with Tasks
class GA_GridMove extends GameplayAbility {
Direction: Vector2;
ActivateAbility(context: AbilityContext): void {
// Task 1: Scan grid
const cells = ScanOccupiedCells();
// Sort by direction (front to back)
cells.sort((a, b) => GetDirectionPriority(a, b, this.Direction));
// Calculate movements
const movements: CellMovement[] = [];
const merges: CellMerge[] = [];
for (const cell of cells) {
const result = CalculateDestination(cell, this.Direction);
if (result.CanMove) {
movements.push(result);
if (result.WillMerge) {
merges.push(result.MergeInfo);
}
}
}
// Apply movement effects
for (const move of movements) {
const moveSpec = MakeOutgoingSpec(GE_CellMove, 1);
moveSpec.SetByCallerMagnitude("NewX", move.DestX);
moveSpec.SetByCallerMagnitude("NewY", move.DestY);
ApplyGameplayEffectToTarget(move.Cell.GC, moveSpec);
}
// Apply merge effects
for (const merge of merges) {
const mergeSpec = MakeOutgoingSpec(GE_CellMerge, 1);
ApplyGameplayEffectToTarget(merge.TargetCell.GC, mergeSpec);
// Mark source for destruction via an Effect — direct tag mutation is prohibited (§3.1).
// GE_PendingDestroy is an Infinite Effect with GrantedTags: ["Status.PendingDestroy"].
const destroySpec = MakeOutgoingSpec(GE_PendingDestroy, 1);
ApplyGameplayEffectToTarget(merge.SourceCell.GC, destroySpec);
}
// Wait for animations
const animTask = WaitDelay(0.2);
animTask.OnComplete.Subscribe(this.OnMoveComplete);
}
OnMoveComplete(): void {
// Destroy merged sources
DestroyTaggedCells("Status.PendingDestroy");
// Spawn new tile
SpawnRandomTile();
// Check win/lose conditions
CheckGameState();
EndAbility(false);
}
}
Undo via Effect Audit Trail
Rather than snapshotting raw cell values, the undo system hooks into the GC Effect application pipeline via OnBeforeEffectApplied. Each effect applied during a turn is recorded alongside the pre-apply attribute values it will overwrite. Undoing a turn replays those pre-apply values back through Instant Effects — the undo state is derived entirely from the Effect layer, not from bespoke value captures.
interface EffectRecord {
TargetGC: GameplayController;
Spec: EffectSpec;
/** Attribute values captured immediately before this Effect was applied. */
PreApplyValues: Map<string, number>;
}
interface TurnRecord {
EffectsApplied: EffectRecord[];
}
class UndoSystem {
private TurnHistory: TurnRecord[] = [];
private CurrentTurn: TurnRecord | null = null;
/** Called by GA_Move at the start of each player turn. */
BeginTurn(): void {
this.CurrentTurn = { EffectsApplied: [] };
}
/**
* Hook registered on each cell GC as OnBeforeEffectApplied.
* The GC pipeline calls this immediately before applying an Effect,
* giving us a chance to snapshot the attribute values that will change.
*/
OnBeforeEffectApplied(targetGC: GameplayController, spec: EffectSpec): void {
if (!this.CurrentTurn) return;
const preApplyValues = new Map<string, number>();
for (const modifier of spec.EffectClass.Modifiers) {
preApplyValues.set(modifier.Attribute, targetGC.GetAttribute(modifier.Attribute));
}
this.CurrentTurn.EffectsApplied.push({ TargetGC: targetGC, Spec: spec, PreApplyValues: preApplyValues });
}
/** Called by GA_Move after all effects for the turn have been applied. */
CommitTurn(): void {
if (this.CurrentTurn) {
this.TurnHistory.push(this.CurrentTurn);
this.CurrentTurn = null;
}
}
Undo(): void {
if (this.TurnHistory.length === 0) return;
const lastTurn = this.TurnHistory.pop()!;
// Restore pre-apply values in reverse Effect order.
// Each restore is itself an Instant Override Effect — undo flows through
// the same pipeline as every other state change.
for (const record of [...lastTurn.EffectsApplied].reverse()) {
const restoreSpec = MakeOutgoingSpec(GE_RestoreValues, 1);
for (const [attr, value] of record.PreApplyValues) {
restoreSpec.SetByCallerMagnitude(attr, value);
}
ApplyGameplayEffectToTarget(record.TargetGC, restoreSpec);
}
}
}
GE_RestoreValues is an Instant Effect with one Override modifier per restored attribute, driven by SetByCaller magnitudes. Every undo operation passes through the standard Effect pipeline: it is observable, replicable, and appears in the Effect audit trail exactly like any other state change.
Appendix A: Mathematical Notation
Variable Naming Conventions
| Symbol | Meaning |
|---|---|
\(V\) |
Value (generic) |
\(V_{base}\) |
Base Value of an Attribute |
\(V_{current}\) |
Current Value of an Attribute |
\(V_{min}\), \(V_{max}\) |
Minimum/Maximum bounds |
\(a\) |
Additive modifier magnitude |
\(p\) |
Percentage modifier magnitude |
\(m\) |
Multiplicative factor |
\(t\) |
Time variable |
\(\Delta_t\) |
Time delta |
\(n\) |
Count/index variable |
Summation and Product Notation
Summation (\(\sum\)): Sum of values over an index range
Product (\(\prod\)): Product of values over an index range
Set Theory Notation for Tags
| Notation | Meaning |
|---|---|
T |
A single Tag |
C |
A TagContainer (set of Tags) |
T ∈ C |
Tag T is a member of Container C |
C₁ ⊆ C₂ |
Container C₁ is a subset of C₂ |
C₁ ∩ C₂ |
Intersection of two containers |
C₁ ∪ C₂ |
Union of two containers |
C₁ ∩ C₂ ≠ ∅ |
Containers have at least one common element |
Appendix B: Complete Schema Reference
Schema URL Versioning Policy
All $schema URLs in UGAS data files use the pattern:
https://raw.githubusercontent.com/jbltx/ugas/{version}/schemas/{schema-name}.json
Where {version} is the git tag of the UGAS release the data file was authored against (e.g. v1.0.0-draft.1). Each released version tag MUST maintain stable schema URLs — schemas at a given version tag MUST NOT be modified after release.
Data files SHOULD pin to the exact UGAS version they were authored against. Tooling that processes UGAS data files SHOULD validate against the schema URL declared in $schema, and SHOULD fail clearly when the URL is unreachable or the schema is invalid, rather than silently skipping validation.
GameplayController Schema Definition
# Gameplay Controller Interface Schema Definition
# Based on UGAS Specification v1.0.0-draft.1 - Section 4
type: object
required:
- OwnerActor
- AttributeSets
properties:
OwnerActor:
type: object
description: Logical owner of the GC (responsible for lifecycle, network authority, persistence)
properties:
ActorID:
type: string
description: Unique identifier for the owner actor
ActorType:
type: string
description: Type of the owner actor
AvatarActor:
type: object
description: World spatial representation (optional, can be same as Owner)
properties:
ActorID:
type: string
description: Unique identifier for the avatar actor
ActorType:
type: string
description: Type of the avatar actor
AttributeSets:
type: array
items:
type: object
properties:
Name:
type: string
description: AttributeSet identifier
Attributes:
type: array
items:
type: object
properties:
Name:
type: string
BaseValue:
type: number
CurrentValue:
type: number
minItems: 1
description: Registered attribute containers
GrantedAbilities:
type: array
items:
type: object
required:
- AbilityClass
properties:
AbilityClass:
type: string
description: Ability class identifier
Level:
type: integer
default: 1
minimum: 1
description: Ability level
InputID:
type: string
description: Optional input binding identifier
Handle:
type: string
description: Unique handle for this granted ability instance
bIsActive:
type: boolean
default: false
description: Whether the ability is currently active
description: All abilities granted to this GC
ActiveEffects:
type: array
items:
type: object
required:
- Handle
- EffectClass
properties:
Handle:
type: string
description: Unique identifier for this active effect
EffectClass:
type: string
description: GameplayEffect class reference
Duration:
type: number
description: Remaining duration in seconds (-1 for infinite)
Stacks:
type: integer
minimum: 1
default: 1
description: Number of effect stacks
StartTime:
type: number
description: Timestamp when effect was applied
Level:
type: integer
minimum: 1
default: 1
InstigatorGC:
type: string
description: Reference to the GC that caused this effect
description: Currently active effects applied to this GC
OwnedTags:
type: array
items:
type: string
pattern: "^[A-Z][a-zA-Z0-9]*(\\.[A-Z][a-zA-Z0-9]*)*$"
description: Current semantic state tags (hierarchical dot notation)
ReplicationMode:
type: string
enum: [Minimal, Mixed, Full, None]
default: Mixed
description: "Replication strategy: Minimal (only cues & tags for AI), Mixed (full to owner, minimal to others for players), Full (complete to all for single-player/spectators), None (no replication for server-only)"
bIsActive:
type: boolean
default: true
description: Whether this GC is currently active
Metadata:
type: object
description: Optional metadata for display and debugging
properties:
DisplayName:
type: string
Description:
type: string
Tags:
type: array
items:
type: string
DebugCategory:
type: string
Attribute Schema Definition
# Attribute Definition Schema
# Based on UGAS Specification v1.0.0-draft.1 - Appendix B
type: object
required:
- Name
- DefaultBaseValue
properties:
Name:
type: string
description: Unique identifier for this attribute
DefaultBaseValue:
type: number
description: Initial base value
Category:
type: string
enum: [Resource, Statistic, Meta]
default: Statistic
Clamping:
type: object
properties:
Min:
oneOf:
- type: number
- type: string
description: Attribute reference
Max:
oneOf:
- type: number
- type: string
description: Attribute reference
ReplicationMode:
type: string
enum: [None, OwnerOnly, All]
default: All
Metadata:
type: object
properties:
DisplayName:
type: string
Description:
type: string
UICategory:
type: string
Icon:
type: string
AttributeSet Schema Definition
# AttributeSet Definition Schema
# Based on UGAS Specification v1.0.0-draft.1 - Appendix B
type: object
required:
- Name
- Attributes
properties:
Name:
type: string
description: Unique set identifier
Dependencies:
type: array
items:
type: string
description: Required attribute sets
Attributes:
type: array
items:
$ref: "#/definitions/Attribute"
Metadata:
type: object
properties:
DisplayName:
type: string
Description:
type: string
definitions:
Attribute:
type: object
required:
- Name
- DefaultBaseValue
properties:
Name:
type: string
description: Unique identifier for this attribute
DefaultBaseValue:
type: number
description: Initial base value
Category:
type: string
enum: [Resource, Statistic, Meta]
default: Statistic
Clamping:
type: object
properties:
Min:
oneOf:
- type: number
- type: string
Max:
oneOf:
- type: number
- type: string
ReplicationMode:
type: string
enum: [None, OwnerOnly, All]
default: All
Metadata:
type: object
properties:
DisplayName:
type: string
Description:
type: string
UICategory:
type: string
Icon:
type: string
Ability Schema Definition
# Ability Definition Schema
# Based on UGAS Specification v1.0.0-draft.1 - Appendix B
type: object
required:
- Name
properties:
Name:
type: string
description: Unique identifier for this ability
Tags:
type: object
properties:
AbilityTags:
type: array
items:
type: string
description: Tags that describe this ability
BlockedByTags:
type: array
items:
type: string
description: Tags that prevent this ability from running
BlockAbilitiesWithTags:
type: array
items:
type: string
description: While this ability is active, block abilities with these tags
CancelAbilitiesWithTags:
type: array
items:
type: string
description: Cancel abilities with these tags when this ability activates
ActivationRequiredTags:
type: array
items:
type: string
description: Tags required on the GC to activate this ability
ActivationBlockedTags:
type: array
items:
type: string
description: Tags that block activation of this ability
ActivationOwnedTags:
type: array
items:
type: string
description: Tags granted to the GC while this ability is active
Cost:
type: string
description: Reference to cost GameplayEffect
Cooldown:
type: string
description: Reference to cooldown GameplayEffect
Tasks:
type: array
items:
type: object
required:
- Type
properties:
Type:
type: string
description: Task type identifier
Params:
type: object
description: Task-specific parameters
Metadata:
type: object
properties:
DisplayName:
type: string
Description:
type: string
Icon:
type: string
Effect Schema Definition
# GameplayEffect Definition Schema
# Based on UGAS Specification v1.0.0-draft.1 - Appendix B
type: object
required:
- Name
- DurationPolicy
properties:
Name:
type: string
description: Unique effect identifier
DurationPolicy:
type: string
enum:
- Instant
- HasDuration
- Infinite
Duration:
$ref: "#/$defs/MagnitudeDefinition"
Period:
type: object
properties:
Period:
type: number
minimum: 0
description: Time interval for periodic execution
ExecuteOnApplication:
type: boolean
default: false
description: Whether to execute immediately on application
ExecutionPolicy:
type: string
enum:
- RunInParallel
- RunInSequence
- RunInMerge
default: RunInParallel
Priority:
type: integer
default: 0
description: >-
Override conflict resolution priority. When multiple active effects apply an Override
modifier to the same Attribute, the effect with the highest Priority value wins.
On equal Priority, last-applied wins (LIFO). Negative values are valid.
Modifiers:
type: array
items:
$ref: "#/$defs/Modifier"
Executions:
type: array
items:
type: object
properties:
CalculatorClass:
type: string
description: Custom calculation class reference
GrantedTags:
type: array
items:
type: string
description: Tags granted while this effect is active
ApplicationRequiredTags:
type: array
items:
type: string
description: Tags required on target for effect to apply
GrantedAbilities:
type: array
items:
type: object
properties:
AbilityClass:
type: string
description: Ability class to grant
Level:
type: integer
default: 1
description: Level of the granted ability
InputID:
type: string
description: Optional input binding
RemoveOnEffectRemoval:
type: boolean
default: true
description: Remove ability when effect expires
GameplayCues:
type: array
items:
type: string
description: Visual/audio cues to trigger
$defs:
MagnitudeDefinition:
type: object
required:
- Type
properties:
Type:
type: string
enum:
- ScalableFloat
- AttributeBased
- CustomCalculation
- SetByCaller
Value:
type: number
description: Static value for ScalableFloat
Curve:
type: string
description: Curve table reference
CurveInput:
type: string
description: Input parameter for curve evaluation
BackingAttribute:
type: string
description: Attribute to use for AttributeBased magnitude
Source:
type: string
enum:
- Source
- Target
description: Which GC to read the backing attribute from
Coefficient:
type: number
default: 1
description: Multiplicative coefficient
PreMultiplyAdditive:
type: number
default: 0
description: Value added before multiplication
PostMultiplyAdditive:
type: number
default: 0
description: Value added after multiplication
CalculatorClass:
type: string
description: Custom calculation class for CustomCalculation type
DataTag:
type: string
description: Tag for SetByCaller data lookup
Modifier:
type: object
required:
- Attribute
- Operation
- Magnitude
properties:
Attribute:
type: string
description: Target attribute to modify
Operation:
type: string
enum:
- Add
- AddPost
- Multiply
- Override
description: >-
Mathematical operation to apply.
Add: pre-multiply flat additive (pipeline step 2, before percentage and multiply steps).
AddPost: post-multiply flat additive (pipeline step 7, after all multiply steps; very rare).
Multiply: multiplicative factor at step 6 — use a reciprocal magnitude (e.g. 0.5) instead of a Divide operation.
Override: replaces the computed result at step 8.
Magnitude:
$ref: "#/$defs/MagnitudeDefinition"
Channel:
type: string
description: >-
Optional named aggregation channel. Modifiers in the same channel sum together;
modifiers in different channels multiply against each other. Used for damage-bucket
systems (see §15.3). Defaults to the global channel if omitted.
Tag Schema Definition
# Tag Registry Schema
# Based on UGAS Specification v1.0.0-draft.1 - Appendix B
type: object
properties:
Tags:
type: array
items:
type: object
required:
- Tag
properties:
Tag:
type: string
pattern: "^[A-Z][a-zA-Z0-9]*(\\.[A-Z][a-zA-Z0-9]*)*$"
description: Hierarchical tag in dot notation (e.g., State.Debuff.Stunned)
Description:
type: string
AllowMultiple:
type: boolean
default: false
DevComment:
type: string
Appendix C: References and Citations
BibTeX Entries
@online{epicgames_gas,
author = {{Epic Games}},
title = {Understanding the Unreal Engine Gameplay Ability System},
year = {2024},
url = {https://dev.epicgames.com/documentation/en-us/unreal-engine/understanding-the-unreal-engine-gameplay-ability-system},
urldate = {2026-02-03}
}
@online{tranek_gasdoc,
author = {Dan Tranek},
title = {GASDocumentation: Understanding Unreal Engine's GameplayAbilitySystem},
year = {2024},
url = {https://github.com/tranek/GASDocumentation},
urldate = {2026-02-03}
}
@online{unity_gas,
author = {{Unity Technologies}},
title = {Unity Gameplay Ability System},
year = {2024},
url = {https://github.com/sjai013/unity-gameplay-ability-system},
urldate = {2026-02-03}
}
@online{godot_attributes,
author = {{OctoD}},
title = {Godot Gameplay Attributes},
year = {2024},
url = {https://github.com/OctoD/godot_gameplay_attributes},
urldate = {2026-02-03}
}
@book{gregory_engine,
author = {Jason Gregory},
title = {Game Engine Architecture},
edition = {3rd},
publisher = {A K Peters/CRC Press},
year = {2018},
isbn = {978-1138035454}
}
@online{gambetta_prediction,
author = {Gabriel Gambetta},
title = {Client-Side Prediction and Server Reconciliation},
year = {2021},
url = {https://www.gabrielgambetta.com/client-side-prediction-server-reconciliation.html},
urldate = {2026-02-03}
}
@online{gaffer_sync,
author = {Glenn Fiedler},
title = {State Synchronization},
year = {2019},
url = {https://gafferongames.com/post/state_synchronization/},
urldate = {2026-02-03}
}
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
1.0 |
February 2026 |
Mickael Bonfill |
Initial specification |
End of Specification