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

15 KiB

name, description
name description
ue-modular-gameplay Design or review modular gameplay in Unreal Engine: experience definitions, feature plugins, reusable action sets, feature actions, actor extension handlers, component init-state chains, primary asset bundles, and runtime activation and deactivation. Use when adding game modes, seasonal or downloadable features, modular components, abilities, input or UI, or when fixing initialization-order and feature teardown bugs.

UE modular gameplay

The invariant:

A feature may select and attach behaviour without modifying the receiving base class, but it owns every resource it adds and must remove exactly that resource.

Measured patterns from a reference product: patterns. Eighteen detection recipes for silent modularity failures: failure modes.

Read the failure modes before adopting this architecture. Nearly all of them are teardown defects, and teardown defects share one property: activation is what gets tested, so the half that is broken is the half nobody exercises.

Related skills: ue-data-driven-architecture, ue-input-architecture, ue-ui-architecture, ue-gas-architecture, ue-multiplayer-authority.

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


1. Separate the four layers

Playlist / session entry
└─ Experience definition
   ├─ feature plugins to activate
   ├─ reusable action sets
   ├─ mode-specific actions
   └─ default pawn data

Feature data asset
└─ lifecycle and asset scanning of one plugin

Action set
└─ reusable composition slice

Feature action
└─ one apply/rollback operation

Do not merge them:

  • the playlist owns map, session and menu metadata, not gameplay composition;
  • the experience owns one runtime composition, not plugin lifecycle policy;
  • the feature data asset owns plugin registration and scanning, not a game mode;
  • an action set expresses reuse; it is not an inherited base experience;
  • an action performs one focused mutation and owns its rollback state.

Prefer composition over experience inheritance

A mode should read as a sum:

SharedInput + StandardComponents + StandardHUD + ModeDelta

not as a hidden defaults chain:

Base -> Shooter -> Team -> TeamDeathmatch

Flat action sets keep dependencies, asset loading and teardown auditable. A reference product measured for this skill goes further and forbids the second form in validation, pointing authors at composition instead.


2. Experience loading is a state machine

Unloaded
→ LoadingAssets
→ LoadingGameFeatures
→ ExecutingActions
→ Loaded
→ Deactivating
→ Unloaded

Each transition needs an explicit completion criterion. Never track this with a handful of booleans — bAssetsReady && bPluginsReady && bActionsDone cannot express "failed", and failure is the state you will need most.

The server selects; every peer realises locally

Replicate the selected definition, never the result of loading it:

server selects experience ID
→ replicated definition reaches the client
→ server and client run the same load pipeline
→ role-specific bundles and actions decide their local work

This is "replicate intent, realise presentation locally" applied to composition.

Document selection precedence

If an experience can come from matchmaking, a URL, developer settings, the command line, world settings and a default, define the exact precedence, log the winning source, validate the asset ID before loading, and provide an explicit fallback.

Delay only for a named dependency

Deferring selection by one frame so that startup settings can initialise is legitimate because the dependency has a name. A next-tick timer with no identified prerequisite is initialization-order debt with a timer wrapped around it. Recipe: MG-12.


3. Bundle and role boundaries

Collect the experience plus every action set as primary assets, then request named bundles: a shared bundle always, the client bundle when a client exists, the server bundle when authority exists.

Mark soft references at their declaration:

meta=(AssetBundles="Client")
meta=(AssetBundles="Client,Server")
Action target Expected role
HUD, layout, input icons Client
scoring authority, bots, team assignment Server
replicated scoring component Client + Server
ability classes and effects needed for prediction usually both

A client/server flag on an action does not guarantee correct cook bundles. Validate both, separately.

Editor bundle loading is not shipping

A policy that loads client data when not a dedicated server and server data when not a client-only build loads both in the editor. Every memory and load-time figure taken in-editor therefore describes a configuration that ships to nobody. Use it for direction of change only. Recipe: MG-18.


4. Extend actors through receivers and extension handlers

Do not scan the world for actors and patch them. Use the component manager's extension protocol:

receiver actor registers itself
action registers an extension handler for a target class
manager invokes the handler for existing AND future receivers
returned request handle owns the subscription

The receiver lifecycle is symmetric and belongs in the base class:

PreInitializeComponents : add receiver
BeginPlay               : send ready events
EndPlay                 : remove receiver

Note what this buys: base gameplay classes register as receivers and know nothing about features. If a class does not inherit the modular base, those three calls are written by hand — and forgetting the third is a leak with no symptom until the second activation.

Use semantic readiness events

A generic "extension added" event means only that the actor is known to the manager. It may not yet have its pawn data, ability system, controller or input. Emit named events at the real dependency boundary — abilities ready, bind inputs now, actor ready — and have handlers accept both the generic event and the semantic one, so activation order stops mattering. Recipe: MG-13.

Retain request handles

Handler registration and component requests return ownership handles that are reference counted. Dropping the handle is the unsubscribe operation; there is no explicit removal call. Storing them per activation context is therefore not bookkeeping, it is the only teardown mechanism that exists.


5. Use an init-state chain

Networked actor composition has non-deterministic arrival order. Replace scattered checks in BeginPlay, possession callbacks and replication callbacks with a monotonic chain:

Spawned → DataAvailable → DataInitialized → GameplayReady
  • Spawned — the actor and component exist.
  • DataAvailable — required replicated and config data exist.
  • DataInitialized — every participating feature reached DataAvailable and cross-feature initialisation ran.
  • GameplayReady — safe for active gameplay.

The load-bearing barrier is "have all features on this actor reached DataAvailable", which synchronises components without any of them naming the others.

Rules

  • states only advance; they reset with the actor's lifecycle, not by backtracking;
  • every transition has an observable prerequisite;
  • replication callbacks re-enter the same check rather than duplicating init;
  • feature-specific work runs once, at a named transition;
  • test server, owning client and simulated proxy separately — a proxy never acquires a controller, and a gate that waits for one deadlocks it while working perfectly in a single-process test.

6. Action design contract

An action class implements behaviour; its instance stores targets and configuration. A world-aware base action should filter to game worlds, handle worlds that already exist at activation, subscribe for future ones, and release its world delegates on deactivation — leaving resource tracking to the concrete action.

State must be keyed by activation context

One action object can be active in several contexts at once — client and server in a single-process test are the common case. State in plain member fields is a cross-context leak. Key it:

struct FPerContextData { /* ... */ };
TMap<FGameFeatureStateChangeContext, FPerContextData> ContextData;

Recipe: MG-11.

One responsibility per action

Good: add components; add abilities; add input binding; add input mapping context; add widgets; add cue path.

Bad: "set up shooter mode".

A focused action has a testable rollback and can be reused in an action set.

The mandatory ownership ledger

Write this table before implementing any action:

Added resource Stored ownership Removal
extension callback request handle release the handle
component component request remove the owned component
ability, effect, attribute set granted handles revoke the handles
input bind bind handles per pawn remove those exact binds
input mapping context player + context remove from the same subsystem
widget extension extension handle unregister
layout widget or layout handle deactivate and remove
global delegate delegate handle unbind

If the middle column is blank, reject the action. Every teardown failure in the measured reference is a blank middle column that shipped. Recipes: MG-01 through MG-05.


7. Deactivation is not optional

"Modular" is not proven by successful activation. The round trip is the test:

inactive baseline
→ activate       → verify additions exactly once
→ deactivate     → verify exact baseline
→ reactivate     → verify no duplicates and no stale state

Run it with: actors that existed before activation; actors spawned after it; an actor destroyed before the feature deactivates; two worlds in one process; client and server roles; a partial load failure; and a plugin required by two experiences at once.

Removal must be idempotent

Resources come off by two independent paths: the explicit reset at deactivation, and reactively when the actor dies first and the manager reports it. Both must be safe to run, in either order, and must operate on "is there a record?" rather than assuming one. Recipe: MG-08.

Reference-count shared plugin activation

If two experiences need one plugin, count activations. One owner deactivating must not unload a plugin the other still requires. Check that the count is not editor-only — a reference count compiled out of shipping builds is a reference count that protects development and nothing else. Recipe: MG-16.

Diff requirements on experience change

old - new         = deactivate and unload
new - old         = load and activate
intersection      = keep active

Do not unload everything and reload it immediately. If this diff is not implemented, define experience changes as map-travel-only and say so in writing — that is a legitimate scope decision, and leaving it undefined is not.


8. Failure handling

A production loader must not assert its way through configuration failure. Represent failure as a state:

Loaded
Failed(reason, failing asset / plugin / action)

Handle: an invalid asset ID; a missing plugin URL; a cancelled load; plugin activation failure; action validation failure; timeout or partial completion; and a deactivation pauser that never completes.

The loading screen consumes loader state and reason. A permanent loading screen is not error handling — it is the absence of it, rendered.


9. Validation checklist

Experience

  • Asset ID registered and resolves.
  • Default pawn data valid, or explicitly no pawn.
  • Plugin names resolve to URLs.
  • Action sets non-null and duplicate-free.
  • Reusable behaviour in action sets; only the mode delta inline.
  • Client and server bundles cover every soft reference.

Action

  • Target actor class valid; addition list non-empty.
  • Existing and future actors both handled.
  • Semantic readiness event used where the generic one is too early.
  • Every request, delegate, grant, widget and input handle retained.
  • Deactivation reverses activation exactly.
  • Repeated activate/deactivate is idempotent.
  • Per-context state cannot leak into another world.
  • Index or key passed to a deferred callback matches the entry it names.

Init states

  • Every prerequisite is explicit.
  • Every participating feature can reach every state.
  • No feature waits on a signal produced only by a path it disabled.
  • Authority and local-control branches tested.
  • Late replication re-enters the same check.

10. Review questions

Before approving a modular feature:

  1. Which definition selects it?
  2. Which plugin owns its code and content?
  3. Which action applies each effect?
  4. Which readiness state makes that effect safe?
  5. Which handle proves ownership?
  6. What is the exact inverse operation?
  7. What is loaded on client, on server, in the editor?
  8. Can another feature share the same plugin or resource?
  9. What happens on partial failure?
  10. Does activate → deactivate → activate return to the same observable state?

If questions 4 to 6 have no precise answer, the feature is dynamically attached but not modular. That distinction is the entire subject of this skill: attaching is easy and demonstrable, detaching is neither, and only one of them gets demonstrated.


Provenance

The findings behind the failure modes come from a line-by-line source audit of the feature-action layer of Epic's Lyra Starter Game on Unreal Engine 5.6, read rather than run. The engine-side component manager and feature subsystem are not part of that project, so every statement about them is inferred from call sites in the audited code and is marked as such rather than presented as measured.

That audit found the architecture sound and its teardown half incomplete in five separate actions — which is why this skill treats the ownership ledger and the round-trip test as mandatory rather than advisory. Several of the gaps are recorded by the original authors in their own comments, which is the strongest confirmation a source audit can produce.

Source addresses stay in the research archive; what ships is the detection recipe. Each entry carries a stable identifier (MG-01, MG-02, …) that resolves back to the audited location there.

Evidence boundary

One project, one engine version, one workspace. Binary assets were not read, so which actions the shipped features actually configure is unknown and is recorded as open rather than guessed. Re-run the recipes against your own tree before trusting any specific claim.