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 a GameplayEffect applied 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:

  1. Input Processing: Hardware inputs are mapped to Input Actions, which trigger Ability activation attempts.

  2. Ability Activation: The GC validates activation requirements (Tags, Costs, Cooldowns) before committing the Ability.

  3. Effect Application: Abilities apply Effects to Targets. Effects create Modifiers on Attributes and grant/remove Tags.

  4. Attribute Recalculation: Affected Attributes recalculate their Current Values based on active Modifiers.

  5. Event Dispatch: OnAttributeChanged events propagate to registered observers.

  6. Cue Triggering: Tag changes trigger appropriate Gameplay Cues on clients.

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

  1. Maintain collections of granted Abilities, active Effects, and owned Tags

  2. Manage one or more AttributeSets

  3. Process Ability activation requests

  4. Apply and remove Gameplay Effects

  5. Dispatch events for state changes

  6. 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
  1. GC is instantiated on Owner Actor

  2. AttributeSets are registered with GC

  3. Owner and Avatar references are set

  4. Initial Abilities are granted

  5. Initial Effects are applied

  6. Replication is configured (if networked)

Possession Handling

When Avatar possession changes:

  1. Previous Avatar reference is cleared

  2. Active Effects targeting Avatar location are re-evaluated

  3. New Avatar reference is set

  4. Avatar-dependent Abilities are re-validated

Destruction Cleanup
  1. All active Effects are removed

  2. All granted Abilities are revoked

  3. Event subscriptions are cleared

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

\[V_{current} = \max\left( V_{min},\ \min\left( V_{max},\ \left( V_{base} + \sum a_i \right) \times \prod_{c \in C} \left(1 + \sum_{k \in c} m_k\right) + \sum b_l \right) \right)\]

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:

\[V_{current} = \left( V_{base} + \sum a_i \right) \times \prod_{c \in C} \left(1 + \sum_{k \in c} m_k\right) + \sum b_l\]
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 Multiply modifiers sharing a Channel value contribute their magnitudes additively. The channel’s effective factor is 1 + 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 Multiply modifier without a Channel is in its own implicit channel, so its contribution is 1 + 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:

  1. Sum all flat additive modifiers (Add): flat = ΣAdd

  2. Apply flat additions to Base Value: value = Base + flat

  3. Group Multiply modifiers by Channel. For each channel, sum the magnitudes: channel_factor = 1 + Σm_k

  4. Multiply all channel factors together and apply: value *= Π channel_factor

  5. Add sum of all post-multiply flat additive modifiers (AddPost): value += ΣAddPost

  6. Apply Override modifiers (if any, replacing the result) — see conflict resolution below

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

  1. Priority wins: The Override modifier from the GameplayEffect with the highest Priority value replaces the result. Lower-priority Overrides are ignored for that Attribute.

  2. 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 MoveSpeed Override to 0 at Priority 10. A "Slow` effect also sets an Override to 50 at Priority 5. The Freeze wins because 10 > 5. If a "Root" effect then sets an Override to 0 at Priority 10, 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:

  1. Validate all dependencies exist before registration

  2. Ensure proper recalculation order when dependencies change

  3. 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
  1. Each segment MUST use PascalCase

  2. Hierarchies SHOULD NOT exceed 5 levels

  3. Leaf tags SHOULD be specific; parent tags SHOULD be categorical

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

MatchesTag(T)

AllTagCounts

True if AllTagCounts[T] > 0 — matches T itself or any descendant of T that is present

Checking for any type of "Stunned" status

MatchesTagExact(T)

ExplicitTagCounts

True if ExplicitTagCounts[T] > 0 — exact tag only, no hierarchy

Immunity to "Stunned.Magic" but not "Stunned.Physical"

GetTagCount(T)

ExplicitTagCounts

Returns ExplicitTagCounts[T] (0 if absent)

"How many stacks of Burning?"

HasAny(Container)

AllTagCounts

True if any tag in Container has AllTagCounts > 0

Spell that affects "Undead" OR "Demon"

HasAll(Container)

AllTagCounts

True if every tag in Container has AllTagCounts > 0

Combo requiring "Chilled" AND "Vulnerable"

HasNone(Container)

AllTagCounts

True if no tag in Container has AllTagCounts > 0

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:

  1. Granted Check: Ability must be granted to the GC

  2. Not Already Active: Ability must not currently be active (unless configured for multiple instances)

  3. Required Tags: Owner must have all tags in ActivationRequiredTags

  4. Blocked Tags: Owner must NOT have any tags in ActivationBlockedTags

  5. Cost Verification: If CostEffect is defined, owner must have sufficient resources

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

  1. Cost Effect is applied (resources consumed)

  2. Cooldown Effect is applied (cooldown tag granted)

  3. Activation Owned Tags are granted

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

  1. Self-Cancellation: Ability logic calls EndAbility(true)

  2. External Cancel: Another system calls CancelAbility on the GC

  3. Cancel Tags: An Effect grants a tag in the Ability’s CancelAbilitiesWithTags set

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

Instant

Modified

Recalculated

Permanent change

HasDuration

Unchanged

Modified

Temporary (until expiry)

Infinite

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

Add

Pre-multiply flat additive

Step 2

Absolute delta (e.g., +10 adds 10)

AddPost

Post-multiply flat additive

Step 5 (very rare)

Absolute delta

Multiply

Channel-aggregated bonus

Step 4

Signed bonus: +0.25 = +25%, −0.25 = −25%. Modifiers in the same Channel add their bonuses; channel effective factors multiply across channels. See §5.3 Channel Aggregation.

Override

Replace value

Step 6

Absolute replacement value

Note: There is no Divide operation. A 50% reduction is expressed as Multiply with 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

RunInParallel

All instances execute simultaneously; magnitude stacks N times

RunInSequence

Instances queue; executes one after another

RunInMerge

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:

  1. Extend the base AbilityTask class

  2. Implement OnActivate() for setup

  3. Implement cleanup in OnEndTask()

  4. Provide delegate/event outputs for ability continuation

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

  1. All active Tasks are cancelled

  2. Task event subscriptions are cleared

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

  1. Input arrives during "blocked" state (animation, recovery)

  2. Input is stored in buffer with timestamp

  3. When block ends, buffered inputs are processed in order

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

  1. Receiving cue trigger notifications

  2. Matching tags to Cue implementations

  3. Instantiating and managing Cue resources

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

  1. Cue Manager is NOT instantiated

  2. Cue assets are NOT loaded

  3. Cue trigger tags are still processed for replication

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

Minimal

None

All

All

None

AI entities, distant actors

Mixed

Owner only

All

All

Owner only

Player characters

Full

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:

  1. Revert to last known authoritative state

  2. Re-apply all inputs that occurred since that state

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

  1. Instigator authority — the instigating GC is owned by the requesting client (or is a server-controlled entity).

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

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

  4. Effect class whitelist — the effect class is one the ability is permitted to apply; implementations SHOULD reject arbitrary EffectClass values 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

"MainStat"

Stat-derived scaling (e.g. Strength → +damage)

Additive

"DamageBonuses"

Conditional damage bonuses (fire, vs. elites, while healthy …)

Additive

"LegendaryPowers"

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

\[\sum_{i=1}^{n} a_i = a_1 + a_2 + \cdots + a_n\]

Product (\(\prod\)): Product of values over an index range

\[\prod_{k=1}^{n} m_k = m_1 \times m_2 \times \cdots \times m_n\]

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