Files
MagentaDolphin ecd87ac96d feat(skills): ship ue-design-skills bundle, licensing and delivery gate
Phase 0 of the handoff plan, as a marketplace rather than a flat skills/
directory. Content moved out of the LyraResearch archive and depersonalised:
addresses stay in the archive, recipes ship.

- plugins/ue-design-skills: 17 skills, 232 failure-mode entries, each with the
  six required fields; catalog.json as the harness-neutral source of truth and
  .claude-plugin/ as one adapter over it.
- _gate: 16 rules, one poisoned fixture per rule, plus surface coverage so a
  declared file cannot silently miss the line rules.
- ADR-0002 (harness-neutral bundle behind a marketplace) and ADR-0003 (split
  licensing: CC BY-ND 4.0 prose, Apache-2.0 code and metadata).
- LICENSE files at both levels, CONTRIBUTING.md, docs/licensing-options.md as
  the material the licence decision grew from.

Verified: gate.py 0 violations; test_gate.py 16/16 rules redden on their
fixtures with a clean baseline and 2 root files reaching the line rules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 23:48:55 +07:00

16 KiB
Raw Permalink Blame History

name, description
name description
ue-gas-architecture Design or review production Gameplay Ability System architecture in Unreal Engine: ability packages and revocation receipts, buffered ability input on the ability system component, activation policies and exclusivity groups, attribute set responsibility boundaries, gameplay effect contexts, tag relationship policy, match phases as abilities, and the health/death seam. Use when adding combat, abilities, equipment-granted powers, phases or rounds, damage and healing, or diagnosing ability activation and lifecycle bugs.

UE GAS architecture

The invariant:

GAS owns capabilities and effects; domain components own durable gameplay state; data packages compose capabilities; every temporary grant keeps a receipt.

Worked analysis of a production GAS layer, including the parts worth copying verbatim: patterns. Detection recipes for silent GAS failures: failure modes.

Related skills: ue-data-driven-architecture, ue-input-architecture, ue-modular-gameplay, ue-cosmetics-and-teams, ue-multiplayer-authority.

Method, not architecture — how to check any claim in this bundle before repeating it: ue-evidence-discipline.


1. Package grants as ability sets

Do not scatter calls to grant abilities, apply effects and add attribute sets across pawns, equipment and modes. Define one immutable capability package:

AbilitySet
├─ abilities: class + level + optional semantic input tag
├─ gameplay effects: class + level
└─ attribute sets: class

The same package can be granted by pawn data at spawn, by an equipment definition while equipped, by a feature action for a mode, by a pickup, or by a test fixture.

Grant order is not cosmetic

Attributes, then abilities, then effects. Effects in the third group may modify attributes created by the first; the reverse order silently applies modifiers to attributes that do not exist yet.

Grant only on authority

The package's grant function must reject non-authority mutation at its first line. Clients receive replicated specs, effects and attributes; they do not author grants.

Return a grant receipt

GrantedHandles
├─ AbilitySpecHandles
├─ ActiveGameplayEffectHandles
└─ GrantedAttributeSet pointers

The revoke function removes exactly those resources. This is what prevents one equipment item from revoking another owner's grants — and it is the single most reusable piece of a GAS layer.

Decide permanence explicitly

Passing no receipt means "grant for the lifetime of the ability system component". That is a legitimate design choice, and it must be a choice rather than an omission. Review every caller:

Caller Receipt required?
pawn baseline from pawn data optional, permanent by design
equipment yes
feature action yes, per actor and per activation context
temporary pickup yes, or rely on effect duration handles
test or cheat command yes

A single reference implementation demonstrates both: the pawn baseline passes nothing because it is meant to last as long as the player state, and equipment passes a receipt stored in its own applied-item entry.


2. Treat input as a buffered concern of the ability system component

Input events should not call activation directly. Send a semantic input tag to the ability system component and process once per frame after player input:

InputAction
→ InputTag pressed/released
→ ASC records spec handles
→ PlayerController.PostProcessInput
→ ASC.ProcessAbilityInput
→ activation policy chooses the activation set
→ TryActivateAbility

What this buys:

  • simultaneous inputs resolve together instead of in binding order;
  • held input is distinguishable from newly pressed input;
  • activation can be suppressed globally for a frame;
  • activation policy belongs to the ability rather than to each binding site.

Required buffers

Held spec handles, pressed this frame, released this frame. Clear the transient buffers after processing, and drop stale handles when specs are removed.

Order within the frame matters

Collect held abilities first, then pressed, then activate everything in one pass, and only then process releases. Activating as you iterate means a held input can activate an ability and then immediately deliver it a press event it should never have seen.

Add a global input block tag

When a project-defined "ability input blocked" tag is present on the component: clear the buffers, activate nothing, and define explicitly whether already-active held abilities receive a release or a cancel. One tag is cheaper and far more reviewable than disabling dozens of input actions during UI, death or stun.


3. Put activation rules on the ability

Activation policy — when

  • OnInputTriggered — activate once when pressed;
  • WhileInputActive — activate and retry while held;
  • OnSpawn — activate when granted and the avatar is ready.

Do not infer these from the input event type at the binding site. The ability knows its own lifecycle; the binding does not.

OnSpawn needs two entry points, not one: the ability may be granted to a live avatar, or the avatar may appear for an already-granted ability. Handle both, and skip activation when the avatar is being torn down.

Activation group — concurrency

  • Independent — unaffected by others;
  • Exclusive_Replaceable — exclusive, but another exclusive may displace it;
  • Exclusive_Blocking — exclusive and prevents other exclusives from starting.

The component tracks active counts per group and enforces transitions. This replaces an N×N web of per-ability blocking tags for broad concurrency rules.

Rules:

  • an ability may enter a group only if not blocked;
  • entering an exclusive group cancels other replaceable exclusives;
  • group counts must balance across activate and end;
  • an ability in a replaceable group must not be able to refuse cancellation — that combination is a contradiction and should log rather than obey;
  • changing group at runtime is explicit and validated.

Death is the clearest worked example: on activation it cancels everything not explicitly marked as surviving death, refuses cancellation, and moves itself into the blocking group for the duration.


4. Move tag relationships into a policy asset

Abilities carry their intrinsic tags. A pawn or archetype selects a relationship map:

AbilityTag
├─ tags to block
├─ tags to cancel
├─ activation required tags
└─ activation blocked tags

This removes repeated policy from N ability defaults and lets the same ability classes run under different archetype rules. It matters most when abilities arrive from separate feature plugins that must not know about each other.

Relationship validation

  • the key tag is valid and appropriately scoped;
  • parent-tag matching is intentional, not incidental;
  • block and cancel are not confused with each other;
  • required and blocked sets are not contradictory;
  • cycles are documented and tested;
  • every archetype that needs policy actually selects a map.

A linear scan is acceptable for a small matrix. Profile before assuming it scales: in the measured reference the lookup is a linear array walk performed on every activation and on every block/cancel application, and its authors marked it as provisional in three separate places.


5. Split attribute sets by direction of responsibility

Do not create one character-attributes dumping ground.

Receiving set. Health, max health, the meta attributes damage and healing, clamping, and the out-of-health signal. Damage and healing are transient inputs to execution, not durable replicated state: they arrive, convert into a health change, and reset to zero in the same call.

Source set. Base damage, base heal, outgoing coefficients — the values captured from the instigator.

Execution calculations capture the source set from the instigator and the receiving set from the target. The split makes "who contributes this value" structural rather than a comment.

Gates on the receiving set

  • clamp in the correct hook, and in all of them that can change the value;
  • guard against duplicate out-of-health events with an explicit flag that resets when the value recovers;
  • record pre-execution values when messages need a before-and-after;
  • notify domain components through delegates;
  • keep game-flow state — death started, death finished — out of the attribute set entirely.

Mark what modifiers must not touch

Attributes that may only be changed by an execution should say so in metadata. This is the difference between "we agreed not to modify health directly" and a rule the editor enforces.


6. Extend the gameplay effect context for combat metadata

When damage needs source or target data beyond the stock context, derive a project context and implement allocation, script-struct identity, duplication and network serialization.

Store only what must survive execution and replication. A context is not a place to hang a mutable combat object graph.

Validation:

  • the context survives duplication with its custom fields intact;
  • custom fields serialize deterministically;
  • object references are network-safe, and fields that are deliberately not replicated are documented as server-only at their declaration;
  • execution calculations handle absent hit data;
  • script exposure does not permit invalid mutation.

7. Separate numeric death detection from the death lifecycle

effect execution changes Health
→ receiving set emits out-of-health
→ health component sends a death gameplay event
→ death ability starts
→ health component enters DeathStarted, then DeathFinished
→ game mode handles respawn

Responsibilities: GAS owns the number; the health component owns the state and its replication; the death ability owns the sequence; the game mode owns the player lifecycle.

Do not represent death only as "health is zero". Consumers need a monotonic state they can observe over the network, and a predicted transition needs somewhere to be corrected.

The death trigger must be unambiguous

Configure it in the ability's class defaults so exactly one path can start it, and have the ability's end path force the final transition — insurance against a designer-authored sequence that does not reach the end.


8. Model match phases as abilities when the lifecycle fits

A phase ability runs on the ability system component of the game state:

Game.Playing
├─ Game.Playing.Warmup
├─ Game.Playing.SuddenDeath
└─ Game.PostGame

Starting a phase grants and activates the ability, registers its tag, cancels incompatible siblings, and notifies observers. Ending the ability ends the phase.

Hierarchical tags give the rule for free: parents and children coexist, siblings do not. Starting Game.Playing.SuddenDeath while Game.Playing is active leaves the parent running; starting Game.PostGame ends the whole subtree.

Use this pattern when

  • the phase has a real enter/active/exit lifecycle;
  • ability tasks, effects and tags are natural fits for its logic;
  • the server owns transitions;
  • observers want tag-based matching rather than an enum switch.

Do not use it when

  • a replicated enum fully describes the state;
  • the phase must never be cancellable;
  • clients must query authoritative phase state and no replication path has been designed for it. This is the trap: if the phase abilities are server-only, a client-side query of "is this phase active" answers false forever, and it answers it without an error.

Observer handles are mandatory

Registration returns a handle and supports unsubscribe. An array of callbacks with no handles grows until the world resets, and every entry pins whatever it captured.


9. Failure feedback belongs at the ability system seam

Activation failure should produce structured tags, not a log line:

Ability.ActivateFail.Cooldown
Ability.ActivateFail.Cost
Ability.ActivateFail.TagsBlocked
Ability.ActivateFail.IsDead

For a locally controlled avatar, route them straight to feedback. For a server-side rejection of a locally predicted action, notify the owning client with the ability and the failure tags. Map tags to user-facing text and to animations in data, and deliver them over a message bus so the UI never learns about GAS.

Never parse log strings in UI.

Compose costs rather than accepting one

A single cost effect is rarely enough. A list of inline cost objects — each with its own check, its own application, its own failure tag, and an optional "only charge on hit" rule — turns cost into data. The hit determination should be computed once and cached, on authority only.


10. Equipment and feature integration

Equipment grants ability sets when equipped and retains the receipt in its applied-item entry. Unequip reverses exactly that receipt.

A feature action that grants abilities retains, per actor: direct ability handles, attribute instances, ability-set receipts, and the extension request handle.

Every integration answers:

  • Who owns the ability system component — the pawn or the player state?
  • Which object is the source object for the spec?
  • When is avatar info initialized?
  • Is the grant permanent or reversible?
  • What happens if the actor is destroyed before the feature deactivates?

See ue-modular-gameplay for actor-extension timing and rollback.


11. Review checklist

Ability set

  • authority-only grant, checked at the top of the function;
  • grant order is attributes, abilities, effects;
  • every class reference non-null and of the expected type;
  • semantic input tag valid and constrained;
  • receipt retained for every temporary grant;
  • revoke removes only owned resources;
  • source object deliberately chosen.

Ability system component

  • input buffers processed once per frame, after player input;
  • blocked-input behaviour explicit for held abilities;
  • activation group counters balance;
  • avatar change activates spawn-policy abilities exactly once;
  • relationship map selected per archetype;
  • failure tags reach local feedback.

Attributes and damage

  • source and target responsibilities separated into different sets;
  • meta attributes are not treated as durable state;
  • clamping occurs in every path that can change the value;
  • team and friendly-fire rules applied in one authoritative calculation;
  • out-of-health cannot fire twice for one death;
  • custom effect context serializes.

Phases and death

  • the server owns transitions;
  • observers can unregister;
  • sibling and parent tag semantics are tested, not assumed;
  • clients have a defined observation path that does not depend on a server-only subsystem;
  • death start and finish are durable states, not health comparisons.

12. Decision summary

Use GAS for a concern when at least one is true: it is a grantable and revocable capability; prediction or networked activation matters; it is naturally expressed through effects, tags, cost and cooldown; it benefits from task-based lifecycle with cancel and end.

Keep it out of GAS when the concern is durable domain state better served by a replicated component, presentation only, static definition composition, or a global lifecycle with no ability semantics.

A production GAS architecture is not "everything is an ability". It is a clean seam between capability lifecycle, numeric effects, domain state and data-driven composition.


Provenance

The patterns and failure modes in references/ come from a line-by-line audit of the GAS layer of Epic's Lyra Starter Game on Unreal Engine 5.6 — roughly 4 900 lines across 51 files — read as source rather than run.

Source addresses stay in the research archive that produced this skill; what ships is the detection recipe. Each entry carries a stable identifier (GA-01 and up) that resolves back to the audited location, so any specific claim can be produced on request.

Evidence boundary

One project, one engine version, one workspace. The architecture described here is transferable; the specific defects are evidence, not guarantees about other versions. Re-run the detection recipes against your own tree before acting on any specific claim.