Unity Design System
Use this skill to turn a Unity project into a maintainable design system: clear authoring data,
explicit runtime state, small domain modules, deterministic startup, typed events, focused services,
and editor-safe content workflows.
Prefer patterns that fit Unity's inspector and scene workflow without hiding dependencies behind
global state. Treat every large MonoBehaviour or service as a liability until its responsibilities
are visible and bounded.
Workflow
- Inspect local instructions, project structure, service/bootstrap entry points, representative
domain modules, UI screens, save/reset code, and tests.
- Separate observed patterns into:
- Standards to keep: repeatable patterns that reduce coupling or improve authoring safety.
- Patterns to repair: repeated smells that increase runtime risk or maintenance cost.
- If implementing, change the smallest slice that establishes the standard. Do not redesign every
domain at once.
- Validate with focused tests for plain C# logic, Unity editor tests where needed, and a scene/play
smoke check for lifecycle or UI changes.
Core Standards
- Use
ScriptableObject for authoring data and content definitions.
- Keep runtime state in runtime objects, services, scene instances, or save payloads, not shared assets.
- Split each game domain by reason to change: data, events, interfaces, pure logic, save data,
authoring assets, and service orchestration.
- Keep services as orchestration boundaries. Put calculations, validation, matching, and state
transitions in plain C# classes.
- Use typed events for cross-system notifications and UI updates, not for opaque command chains.
- Make required dependencies visible through serialized references, constructors, or one composition
root. Treat missing required dependencies as startup errors.
- Flag any MonoBehaviour or service over 200 LOC, or any class with more than 5 public methods, for
extraction unless there is a clear Unity lifecycle reason.
- Prefer deletion over speculative abstraction. Add interfaces only when variants share a real
lifecycle.
Domain Module Template
For gameplay domains such as brewing, inventory, economy, customer, recipe, audio, progression,
processing, combat, cards, enemies, or reset, prefer this shape:
Scripts/<Domain>/
├── Data/ # Enums, IDs, small value types
├── Events/ # Typed event structs for requests and notifications
├── Interface/ # Narrow provider/service interfaces
├── Logic/ # Pure rules, validators, matchers, registries, calculators
├── SaveData/ # Serializable save payloads and reset category metadata
├── ScriptableObjects/ # Authoring data and config assets
└── Service/ # Unity lifecycle adapter and orchestration boundary
Rules:
- Do not force empty folders for tiny domains, but use the same mental split.
- Domain event files should be easy to find; do not hide public event structs at the bottom of UI
or scene controller files.
- Service methods should read like orchestration: validate, update runtime object, publish events,
call save, update optional presentation hooks.
Authoring Data
Use ScriptableObject assets for ingredients, recipes, cards, actions, enemies, processing stations,
shop upgrades, audio clips, scenes, difficulty, tutorial pages, boss definitions, and config values.
Rules:
- Asset classes expose mostly read-only properties.
- Asset classes do not mutate per-run state.
- Use
[CreateAssetMenu], [Header], [Tooltip], [Range], and OnValidate to make authoring safe.
- Store stable content IDs on definitions; use runtime IDs for spawned instances.
- Prefer explicit serialized references for required config over scattered
Resources.Load strings.
Good shape:
public sealed class IngredientData : ScriptableObject
{
[SerializeField] private IngredientId id;
[SerializeField] private string displayName;
[SerializeField] private MaterialEffectProfile effectProfile;
public IngredientId Id => id;
public string DisplayName => displayName;
public MaterialEffectProfile EffectProfile => effectProfile;
private void OnValidate()
{
if (id == IngredientId.None)
Debug.LogWarning($"[{nameof(IngredientData)}] {name} has no id.");
}
}
Runtime State
Create runtime wrappers for stateful gameplay concepts such as active brew sessions, customer queues,
card stacks, cooldowns, EX state, current health, active modifiers, selected phase, and spawned enemy
handles.
Rules:
- A
ScriptableObject should not own fields like IsActive, IsEx, CooldownTimer, current stack
count, current HP, or active scene references.
- Runtime data can reference its definition, but the definition should not reference runtime state.
- Reset runtime state through constructors, explicit reset services, or reset orchestrators.
- Plain runtime classes should be testable without loading scenes.
Good shape:
public sealed class BrewSession
{
private readonly RawBrewTotals totals = new();
public BrewingPhase Phase { get; private set; } = BrewingPhase.NoIngredient;
public MaterialEffectProfile RawProfile => totals.TotalProfile;
public bool TryAddMaterial(ProcessedMaterial material)
{
if (Phase != BrewingPhase.NoIngredient)
return false;
return totals.TryAdd(material);
}
}
Pure Logic First
Put deterministic rules in plain C# classes or structs:
- Validators:
ValidateAddMaterial, ValidateStartBrew, ValidatePlacement.
- Calculators: price, quality, score, XP, cooldown.
- Matchers: customer request match, recipe match, target selection.
- State helpers: stabilizers, phase transition rules, runtime statistics.
- Registries: maps from stable IDs to authoring assets.
Rules:
- Keep Unity side effects out of rule code unless the rule is intrinsically visual or physics-bound.
- Return structured results, not just booleans, when UI/debug needs failure reasons.
- Write focused NUnit tests for these classes before adding scene-level tests.
Services And Bootstrap
Use one composition root to instantiate persistent services, register them, load persistent scenes,
then initialize services in deterministic order.
Rules:
- A service may own runtime state and coordinate side effects, but should not contain every business
rule for the domain.
- Keep
ServiceLocator calls near composition roots, UI adapters, and service boundaries. Avoid
ServiceLocator.Get<T>() inside plain logic, modifiers, or data objects.
- Prefer interfaces for provider contracts when consumers only need a small surface.
- Provide explicit initialization priorities only when order matters.
- Unregister or clear static service registries during teardown or test reset.
- Missing required services should fail fast. Optional polish dependencies can be nullable.
Service shape:
public sealed class BrewingService : InitializableServiceBase<BrewingService>, IBrewingProvider
{
private BrewingSession session;
public override Task Initialize()
{
session = new BrewingSession();
EventBus.Subscribe<RequestStartBrewEvent>(OnStartRequested);
return Task.CompletedTask;
}
protected override void OnDestroy()
{
EventBus.Unsubscribe<RequestStartBrewEvent>(OnStartRequested);
base.OnDestroy();
}
}
Event System
Use typed event structs for broad notifications such as phase changes, UI readiness, item usage,
achievement unlocks, score changes, combat transitions, save/reset progress, and service readiness.
Rules:
- Subscribe in
OnEnable/Initialize; unsubscribe in the paired lifecycle method.
- Use request events for user/system intent and result events for state changes.
- Keep event definitions in domain event files.
- Avoid duplicated event names in different scopes unless there is a namespace-level reason.
- Avoid chains where event A publishes B publishes C without one workflow owner.
- Event bus debugging should expose subscriber counts and recent publish history.
- Isolate subscriber exceptions if a failure in one listener should not block the rest.
UI System
UI renders state, publishes intent, and delegates rules to services or pure logic.
Rules:
- Use reusable UI base components only for repeated mechanics: show/hide, modal pause, tabs,
carousel, buttons, drag zones, effect displays.
- Split large screens into presenters/controllers for pagination, action buttons, effect previews,
drop zones, animation, and service adapters.
- UI may cache services on enter/awake, but it should prefer provider interfaces when possible.
- If UI registers as a visual provider, it must unregister on close/exit/disable.
- Use a reference-counted pause manager for modal UI.
- Avoid
GameObject.Find, tag lookup, and Camera.main in frequently used UI paths.
Save And Reset
Centralize persistence behind generic save payloads and explicit reset orchestration.
Rules:
- Every save payload derives from a base save type with explicit filename and reset policy.
- Async save/load returns
Task. Reserve async void for Unity event entrypoints only.
- Do not fire-and-forget important saves during reset, scene changes, or quit flows.
- Separate platform IO, serialization, encryption/obfuscation, registry, and reset selection if the
save manager grows large.
- Reset should delete matching save payloads, reset in-memory services by priority, and publish
progress/completion events.
- Treat hardcoded keys and fixed IVs as obfuscation only; do not describe them as strong security.
Editor Tooling
Build small editor tools for repetitive production tasks:
- Event bus debugger.
- Service/bootstrap debugger.
- Bootstrap inspector with testing overrides.
- Selective save reset tools.
- Asset generators and validators for catalogs, recipes, cutscenes, scenes, audio, or content IDs.
Rules:
- Put editor-only code under
Editor/ or wrap it in #if UNITY_EDITOR.
- Validate selected assets before generating output.
- Make generated folders deterministic and safe to overwrite only after confirmation.
Anti-Patterns To Fix
God MonoBehaviours Or Services
Smell:
- One class handles input, pagination, state restore, service lookups, animation, audio, VFX,
validation, saving, debug, and business rules.
Fix:
- Extract validators, presenters, factories, runtime state objects, and side-effect services.
- Keep MonoBehaviours as Unity adapters/orchestrators.
Hidden Global Dependencies
Smell:
- Frequent
ServiceLocator.Get<T>(), .Instance, FindObjectOfType, FindAnyObjectByType,
GameObject.Find, FindGameObjectWithTag, or Camera.main inside gameplay logic.
Fix:
- Wire dependencies at bootstrap, through serialized fields, or constructors.
- Use interfaces for plain C# services and pass them through constructors.
- Cache optional lookups once at initialization.
Static Lifecycle Leaks
Smell:
- Static service/event registries have no unregister or test reset path.
- Services keep references to destroyed scene UI or visual providers.
Fix:
- Add explicit
Unregister, ClearAll, or test teardown paths.
- Pair every register/subscribe with unregister/unsubscribe.
- Prefer weak/event ownership only when the lifecycle is genuinely shared.
Event Spaghetti
Smell:
- Events are used as internal method calls, commands have no owner, or behavior requires tracing many
publish/subscribe hops.
Fix:
- Use one orchestrator per workflow.
- Use events for notifications and UI updates.
- Keep request/result event pairs explicit.
Resources.Load Drift
Smell:
- Config and content dependencies are loaded through scattered string paths.
Fix:
- Prefer serialized config references in bootstrap assets or service prefabs.
- If
Resources.LoadAll is used for catalogs, keep paths centralized in one registry and validate
loaded assets.
Async Fire-And-Forget
Smell:
- Important saves, resets, or scene transitions are hidden behind
async void.
Fix:
- Return
Task from service APIs and await at workflow boundaries.
- Keep
async void only at Unity event entrypoints, with local exception handling.
Soft Failure For Required Wiring
Smell:
- Required dependencies log a warning and continue with permissive behavior.
Fix:
- Validate required references in
Awake/OnValidate.
- Disable the component or throw a clear startup error when required wiring is absent.
Duplicate Or Ambiguous Types
Smell:
- Two event structs/classes share the same name in different scopes without clear namespacing.
Fix:
- Use domain-prefixed names such as
SettingsOpenRequestedEvent or place all related events in one
namespace/file.
Half-Built Abstractions
Smell:
- Interfaces or enum branches exist but implementations are empty or no-op.
Fix:
- Delete unused abstraction until needed, or complete behavior with focused tests/demo scene.
Review Checklist
Output Style
When asked to review a Unity codebase, lead with findings ordered by risk. Include file references,
why each pattern matters, and the smallest repair path. When asked to implement a design system,
make the standard real in one representative domain first, then leave a repeatable template for the
next domains.
1---2name: unity-design-system3description: Use when designing, implementing, reviewing, or refactoring Unity game architecture and reusable gameplay/UI systems. Trigger on Unity design system, Unity architecture, ScriptableObject architecture, data-driven gameplay, EventBus, ServiceLocator, bootstrap, domain module layout, phase/state machines, save/load, reset flows, editor tooling, runtime state safety, god object cleanup, or requests to identify good/bad Unity codebase patterns and turn them into maintainable standards.4---56# Unity Design System78Use this skill to turn a Unity project into a maintainable design system: clear authoring data,9explicit runtime state, small domain modules, deterministic startup, typed events, focused services,10and editor-safe content workflows.1112Prefer patterns that fit Unity's inspector and scene workflow without hiding dependencies behind13global state. Treat every large MonoBehaviour or service as a liability until its responsibilities14are visible and bounded.1516## Workflow17181. Inspect local instructions, project structure, service/bootstrap entry points, representative19 domain modules, UI screens, save/reset code, and tests.202. Separate observed patterns into:21 - **Standards to keep**: repeatable patterns that reduce coupling or improve authoring safety.22 - **Patterns to repair**: repeated smells that increase runtime risk or maintenance cost.233. If implementing, change the smallest slice that establishes the standard. Do not redesign every24 domain at once.254. Validate with focused tests for plain C# logic, Unity editor tests where needed, and a scene/play26 smoke check for lifecycle or UI changes.2728## Core Standards2930- Use `ScriptableObject` for authoring data and content definitions.31- Keep runtime state in runtime objects, services, scene instances, or save payloads, not shared assets.32- Split each game domain by reason to change: data, events, interfaces, pure logic, save data,33 authoring assets, and service orchestration.34- Keep services as orchestration boundaries. Put calculations, validation, matching, and state35 transitions in plain C# classes.36- Use typed events for cross-system notifications and UI updates, not for opaque command chains.37- Make required dependencies visible through serialized references, constructors, or one composition38 root. Treat missing required dependencies as startup errors.39- Flag any MonoBehaviour or service over 200 LOC, or any class with more than 5 public methods, for40 extraction unless there is a clear Unity lifecycle reason.41- Prefer deletion over speculative abstraction. Add interfaces only when variants share a real42 lifecycle.4344## Domain Module Template4546For gameplay domains such as brewing, inventory, economy, customer, recipe, audio, progression,47processing, combat, cards, enemies, or reset, prefer this shape:4849```text50Scripts/<Domain>/51├── Data/ # Enums, IDs, small value types52├── Events/ # Typed event structs for requests and notifications53├── Interface/ # Narrow provider/service interfaces54├── Logic/ # Pure rules, validators, matchers, registries, calculators55├── SaveData/ # Serializable save payloads and reset category metadata56├── ScriptableObjects/ # Authoring data and config assets57└── Service/ # Unity lifecycle adapter and orchestration boundary58```5960Rules:61- Do not force empty folders for tiny domains, but use the same mental split.62- Domain event files should be easy to find; do not hide public event structs at the bottom of UI63 or scene controller files.64- Service methods should read like orchestration: validate, update runtime object, publish events,65 call save, update optional presentation hooks.6667## Authoring Data6869Use `ScriptableObject` assets for ingredients, recipes, cards, actions, enemies, processing stations,70shop upgrades, audio clips, scenes, difficulty, tutorial pages, boss definitions, and config values.7172Rules:73- Asset classes expose mostly read-only properties.74- Asset classes do not mutate per-run state.75- Use `[CreateAssetMenu]`, `[Header]`, `[Tooltip]`, `[Range]`, and `OnValidate` to make authoring safe.76- Store stable content IDs on definitions; use runtime IDs for spawned instances.77- Prefer explicit serialized references for required config over scattered `Resources.Load` strings.7879Good shape:8081```csharp82public sealed class IngredientData : ScriptableObject83{84 [SerializeField] private IngredientId id;85 [SerializeField] private string displayName;86 [SerializeField] private MaterialEffectProfile effectProfile;8788 public IngredientId Id => id;89 public string DisplayName => displayName;90 public MaterialEffectProfile EffectProfile => effectProfile;9192 private void OnValidate()93 {94 if (id == IngredientId.None)95 Debug.LogWarning($"[{nameof(IngredientData)}] {name} has no id.");96 }97}98```99100## Runtime State101102Create runtime wrappers for stateful gameplay concepts such as active brew sessions, customer queues,103card stacks, cooldowns, EX state, current health, active modifiers, selected phase, and spawned enemy104handles.105106Rules:107- A `ScriptableObject` should not own fields like `IsActive`, `IsEx`, `CooldownTimer`, current stack108 count, current HP, or active scene references.109- Runtime data can reference its definition, but the definition should not reference runtime state.110- Reset runtime state through constructors, explicit reset services, or reset orchestrators.111- Plain runtime classes should be testable without loading scenes.112113Good shape:114115```csharp116public sealed class BrewSession117{118 private readonly RawBrewTotals totals = new();119120 public BrewingPhase Phase { get; private set; } = BrewingPhase.NoIngredient;121 public MaterialEffectProfile RawProfile => totals.TotalProfile;122123 public bool TryAddMaterial(ProcessedMaterial material)124 {125 if (Phase != BrewingPhase.NoIngredient)126 return false;127128 return totals.TryAdd(material);129 }130}131```132133## Pure Logic First134135Put deterministic rules in plain C# classes or structs:136137- Validators: `ValidateAddMaterial`, `ValidateStartBrew`, `ValidatePlacement`.138- Calculators: price, quality, score, XP, cooldown.139- Matchers: customer request match, recipe match, target selection.140- State helpers: stabilizers, phase transition rules, runtime statistics.141- Registries: maps from stable IDs to authoring assets.142143Rules:144- Keep Unity side effects out of rule code unless the rule is intrinsically visual or physics-bound.145- Return structured results, not just booleans, when UI/debug needs failure reasons.146- Write focused NUnit tests for these classes before adding scene-level tests.147148## Services And Bootstrap149150Use one composition root to instantiate persistent services, register them, load persistent scenes,151then initialize services in deterministic order.152153Rules:154- A service may own runtime state and coordinate side effects, but should not contain every business155 rule for the domain.156- Keep `ServiceLocator` calls near composition roots, UI adapters, and service boundaries. Avoid157 `ServiceLocator.Get<T>()` inside plain logic, modifiers, or data objects.158- Prefer interfaces for provider contracts when consumers only need a small surface.159- Provide explicit initialization priorities only when order matters.160- Unregister or clear static service registries during teardown or test reset.161- Missing required services should fail fast. Optional polish dependencies can be nullable.162163Service shape:164165```csharp166public sealed class BrewingService : InitializableServiceBase<BrewingService>, IBrewingProvider167{168 private BrewingSession session;169170 public override Task Initialize()171 {172 session = new BrewingSession();173 EventBus.Subscribe<RequestStartBrewEvent>(OnStartRequested);174 return Task.CompletedTask;175 }176177 protected override void OnDestroy()178 {179 EventBus.Unsubscribe<RequestStartBrewEvent>(OnStartRequested);180 base.OnDestroy();181 }182}183```184185## Event System186187Use typed event structs for broad notifications such as phase changes, UI readiness, item usage,188achievement unlocks, score changes, combat transitions, save/reset progress, and service readiness.189190Rules:191- Subscribe in `OnEnable`/`Initialize`; unsubscribe in the paired lifecycle method.192- Use request events for user/system intent and result events for state changes.193- Keep event definitions in domain event files.194- Avoid duplicated event names in different scopes unless there is a namespace-level reason.195- Avoid chains where event A publishes B publishes C without one workflow owner.196- Event bus debugging should expose subscriber counts and recent publish history.197- Isolate subscriber exceptions if a failure in one listener should not block the rest.198199## UI System200201UI renders state, publishes intent, and delegates rules to services or pure logic.202203Rules:204- Use reusable UI base components only for repeated mechanics: show/hide, modal pause, tabs,205 carousel, buttons, drag zones, effect displays.206- Split large screens into presenters/controllers for pagination, action buttons, effect previews,207 drop zones, animation, and service adapters.208- UI may cache services on enter/awake, but it should prefer provider interfaces when possible.209- If UI registers as a visual provider, it must unregister on close/exit/disable.210- Use a reference-counted pause manager for modal UI.211- Avoid `GameObject.Find`, tag lookup, and `Camera.main` in frequently used UI paths.212213## Save And Reset214215Centralize persistence behind generic save payloads and explicit reset orchestration.216217Rules:218- Every save payload derives from a base save type with explicit filename and reset policy.219- Async save/load returns `Task`. Reserve `async void` for Unity event entrypoints only.220- Do not fire-and-forget important saves during reset, scene changes, or quit flows.221- Separate platform IO, serialization, encryption/obfuscation, registry, and reset selection if the222 save manager grows large.223- Reset should delete matching save payloads, reset in-memory services by priority, and publish224 progress/completion events.225- Treat hardcoded keys and fixed IVs as obfuscation only; do not describe them as strong security.226227## Editor Tooling228229Build small editor tools for repetitive production tasks:230231- Event bus debugger.232- Service/bootstrap debugger.233- Bootstrap inspector with testing overrides.234- Selective save reset tools.235- Asset generators and validators for catalogs, recipes, cutscenes, scenes, audio, or content IDs.236237Rules:238- Put editor-only code under `Editor/` or wrap it in `#if UNITY_EDITOR`.239- Validate selected assets before generating output.240- Make generated folders deterministic and safe to overwrite only after confirmation.241242## Anti-Patterns To Fix243244### God MonoBehaviours Or Services245246Smell:247- One class handles input, pagination, state restore, service lookups, animation, audio, VFX,248 validation, saving, debug, and business rules.249250Fix:251- Extract validators, presenters, factories, runtime state objects, and side-effect services.252- Keep MonoBehaviours as Unity adapters/orchestrators.253254### Hidden Global Dependencies255256Smell:257- Frequent `ServiceLocator.Get<T>()`, `.Instance`, `FindObjectOfType`, `FindAnyObjectByType`,258 `GameObject.Find`, `FindGameObjectWithTag`, or `Camera.main` inside gameplay logic.259260Fix:261- Wire dependencies at bootstrap, through serialized fields, or constructors.262- Use interfaces for plain C# services and pass them through constructors.263- Cache optional lookups once at initialization.264265### Static Lifecycle Leaks266267Smell:268- Static service/event registries have no unregister or test reset path.269- Services keep references to destroyed scene UI or visual providers.270271Fix:272- Add explicit `Unregister`, `ClearAll`, or test teardown paths.273- Pair every register/subscribe with unregister/unsubscribe.274- Prefer weak/event ownership only when the lifecycle is genuinely shared.275276### Event Spaghetti277278Smell:279- Events are used as internal method calls, commands have no owner, or behavior requires tracing many280 publish/subscribe hops.281282Fix:283- Use one orchestrator per workflow.284- Use events for notifications and UI updates.285- Keep request/result event pairs explicit.286287### `Resources.Load` Drift288289Smell:290- Config and content dependencies are loaded through scattered string paths.291292Fix:293- Prefer serialized config references in bootstrap assets or service prefabs.294- If `Resources.LoadAll` is used for catalogs, keep paths centralized in one registry and validate295 loaded assets.296297### Async Fire-And-Forget298299Smell:300- Important saves, resets, or scene transitions are hidden behind `async void`.301302Fix:303- Return `Task` from service APIs and await at workflow boundaries.304- Keep `async void` only at Unity event entrypoints, with local exception handling.305306### Soft Failure For Required Wiring307308Smell:309- Required dependencies log a warning and continue with permissive behavior.310311Fix:312- Validate required references in `Awake`/`OnValidate`.313- Disable the component or throw a clear startup error when required wiring is absent.314315### Duplicate Or Ambiguous Types316317Smell:318- Two event structs/classes share the same name in different scopes without clear namespacing.319320Fix:321- Use domain-prefixed names such as `SettingsOpenRequestedEvent` or place all related events in one322 namespace/file.323324### Half-Built Abstractions325326Smell:327- Interfaces or enum branches exist but implementations are empty or no-op.328329Fix:330- Delete unused abstraction until needed, or complete behavior with focused tests/demo scene.331332## Review Checklist333334- [ ] Domain modules separate data, events, interfaces, logic, save data, assets, and services.335- [ ] Authoring data is in `ScriptableObject`; runtime state is not.336- [ ] Plain logic covers calculations, validation, matching, and phase transitions.337- [ ] Required dependencies are visible and validated.338- [ ] Services initialize from one composition root in deterministic order.339- [ ] ServiceLocator usage is confined to boundaries and supports cleanup.340- [ ] Event subscriptions are paired and event definitions are discoverable.341- [ ] UI renders state and publishes intent; gameplay rules live outside UI.342- [ ] Visual providers and listeners unregister on lifecycle exit.343- [ ] Factories create runtime instances and do not mutate shared assets.344- [ ] Large MonoBehaviours/services are split by responsibility.345- [ ] Save/load has awaitable lifecycle paths and clear reset categories.346- [ ] `Resources.Load` strings are centralized or replaced by serialized references.347- [ ] Debug logs are gated or removed from hot runtime paths.348- [ ] Editor tooling exists for repetitive data/setup work.349350## Output Style351352When asked to review a Unity codebase, lead with findings ordered by risk. Include file references,353why each pattern matters, and the smallest repair path. When asked to implement a design system,354make the standard real in one representative domain first, then leave a repeatable template for the355next domains.