scenario-unity-architecture

Installation
SKILL.md

Unity architecture (gameplay programmer / architect)

Expert architecture in Unity is judged by what survives the edges: the second Play session, a destroyed object mid-await, a relaunch of the built player, a class rename, a merge of two branches, a prefab dropped into an empty scene. Data lives in assets, state lives in plain C# and files, systems talk through events they do not own, and every claim is pinned by a test the agent can run without a mouse. Target: Unity 6000.3.21f1, Input System 1.20.0, Test Framework 1.6.0, macOS. If a sibling skill named here is missing from your available skills, ask the user to install it (npx skills add scenario-labs/skills --skill <name>); unattended, proceed from tool schemas and flag the gap.

REQUIRED BACKGROUND: scenario-unity-expert (channels, review loop, 6.3 traps). Toolkit: ut_env, ut_run, ut_live plus this skill's scripts/ut_arch.py, scripts/AgentKit/Architecture/ jobs and scripts/templates/ (a tested Game.Core / Game.Runtime / Game.Editor / tests layout).

Status (2026-09-24): every procedure ran in Unity 6000.3.21f1 on this Mac: tests/code/unity-architecture/run_all.sh --live (results in archive/tests/unity-architecture/live_results.jsonl).

Stance (the expert delta)

  1. ScriptableObjects are definitions, never saves. Play-mode writes to an SO persist in the Editor and reset on every launch of a build (Code Monkey, 5a-ztc5gcFw [00:04:17]). Observed sharper: the write lives only in Editor memory (IsDirty false, the .asset unchanged even after SaveAssets), so a file hash never sees it; only an in-memory guard in the process that plays does (so_guard, the prefab SO test). A macOS player run twice started from the baked value while the save FILE carried over. Runtime modifiers apply to a struct copy (config.Stats, Tarodev, tE1qH8OxO2Y [00:11:18]); live registries are runtime sets, never scene searches (Hipple, raQ3iHhE_Kk [00:40:06]). Saves: versioned JSON of ids, atomic write, backup, migrations.
  2. Read Enter Play Mode before trusting any Play test. With domain reload off (an option in 6.3, the default for new 6.6 projects) a static counter read 2 and a static event had 2 subscribers on the second Play (observed). Unregister static handlers on ExitingPlayMode (the Manual's best practice; observed clean on its own) and reset values at SubsystemRegistration; DoublePlay proves both. Scene reload off makes Editor startup unrepresentative, and a script change still reloads the domain (6.3 Manual).
  3. Every await has an owner. Awaits survive Destroy and Play exit (Unity, eXkFPoEp3BA [00:13:28]): a token-less loop ticked 13 to 14 more times in the 0.3 s after Destroy (observed). MonoBehaviours pass destroyCancellationToken (Lifetime.Linked(this) when IO must also stop at exit); plain C# services pass Application.exitCancellationToken. Never await an Awaitable twice (pooled: AsTask()), never block on a Task. Release players skip the main-thread check: off the main thread a development player threw on persistentDataPath, Time.time, transform and new GameObject, the release player threw nothing (observed). Smoke-test threading in a development build.
  4. Designers wire, code decides. Serialized SO references are the injector designers can see (Hipple [00:14:39]). Channels decouple sender and receivers, iterate listeners backwards ([00:33:23]) and ship a debug plan: counts, a log toggle, RaiseDebug() from a job, an inspector button ([00:36:07]). UnityEvent is a serialized function call ([00:29:26]): rare designer-wired responses only (0 B per invoke measured on 6.3, but targets are hard references resolved by method name). One composition root runs first ([DefaultExecutionOrder(-1000)], git-amend, PJcBJ60C970 [00:12:01]; without it a consumer found nothing, observed) and fails loudly. Each prefab runs alone in an empty scene ([00:16:29]). Statics suit a small solo project only (Weimann, hQE8lQk9ikE [00:01:28]; Tarodev [00:03:00]), with resets. Enums only for sets fixed "by some law"; categories designers extend become SO assets ([00:46:39]).
  5. Rebinding is a state machine with six traps. The action must be disabled (the package throws otherwise); Escape does not cancel a Button rebind by default; a composite head cannot be rebound; duplicates are judged on effectivePath, composite parts only against earlier parts, modifier combos per shortcutKeysConsumeInput (off by default: Shift+B also fires B); Reset All deletes the persisted overrides; LoadBindingOverridesFromJson wipes existing overrides unless passed false (samyam, csqVa2Vimao [00:12:51], [00:27:44], [00:33:22]; 1.20 manual; all tested). For the UI, start from the Rebinding UI sample: in 1.20 it already disables and disposes but lacks four fixes (ut_arch.rebind_audit). Each new of a generated input class is a separate asset: share one.
  6. Assemblies are a contract. A Core with noEngineReferences holds rules testable in milliseconds; runtime asmdefs never reference editor ones (the Editor compiles it, the player build fails, observed). Test asmdefs reference Unity.InputSystem.TestFramework; testables was not needed on 6000.3.21f1 (observed).
  7. Serialized data survives refactors and merges only on purpose. [FormerlySerializedAs] for fields, [MovedFrom] for renamed [SerializeReference] classes (without it the entry loaded as null, observed), [Serializable] on every [SerializeReference] type (6.3 tolerated its absence; 6.4 requires it per the version notes). UnityYAMLMerge exits 2 on any conflicted field even with -l/-r, and without them writes the BASE value (observed): check the exit code, then open the result in Unity.

Establish first

Installs
161
GitHub Stars
1.0K
First Seen
Sep 26, 2026