unity-project-architecture
Use when writing or restructuring Unity C# code - assembly definitions and dependency direction, small components with cached references, ScriptableObjects for data and events, DI instead of singletons, Input System, Addressables, serialization safety and Unity 6 API changes.
- License 许可
- Apache-2.0
- 最近更新
- 2026-11-15
- Stars
- ★ 122
- Forks
- 18
01 前言 Front Matter
- name
- unity-project-architecture
- source
- informed by the Unity 6 Manual (assembly definitions, ScriptableObject, Input System, Addressables, serialization); own wording
- category
- architecture
- tech_stack
- Unity
- description
- Use when writing or restructuring Unity C# code - assembly definitions and dependency direction, small components with cached references, ScriptableObjects for data and events, DI instead of singletons, Input System, Addressables, serialization safety and Unity 6 API changes.
03 SKILL.md 正文
Unity Project Architecture
Overview
Unity makes it easy to put everything in one MonoBehaviour that finds its collaborators with FindObjectOfType and a static Instance. That works for a jam and collapses after a few months: nothing is testable, compile times grow, and scene order decides behaviour. Structure the project as assemblies with a dependency direction, plain C# for rules, thin components for engine glue, and data in assets.
Core principle: Rules in plain C# assemblies, components as adapters, data in ScriptableObjects, dependencies passed in. The repository's existing layout and DI choice win.
Assemblies and direction
Game.Core (noEngineReferences: rules, math, state machines, save DTOs)
↑
Game.Runtime (MonoBehaviours, ScriptableObjects, adapters; references Core)
↑
Game.UI (views and presenters; references Runtime/Core, never the reverse)
Game.Editor (includePlatforms: Editor; tools, validators)
Tests.EditMode / Tests.PlayMode (test assemblies; see unity-testing)
{
"name": "Game.Core",
"rootNamespace": "Game.Core",
"references": [],
"noEngineReferences": true,
"autoReferenced": false
}
- An assembly per layer keeps compile times down and makes a wrong-way reference a compile error instead of a review comment.
Game.CorewithnoEngineReferences: truecan be compiled and tested by plaindotnet testwhen the editor is missing (testable-game-logic).- Gameplay never references UI; UI observes gameplay through events.
Components
// ❌ lookups every frame, public mutable fields, rules inside the component
public class Enemy : MonoBehaviour {
public int hp = 100;
void Update() {
var player = GameObject.Find("Player");
if (Vector3.Distance(transform.position, player.transform.position) < 5) GetComponent<Animator>().SetTrigger("Attack");
}
}
// ✅ cached references, serialized private fields, rule delegated to plain C#
[RequireComponent(typeof(Animator))]
public sealed class EnemyView : MonoBehaviour {
static readonly int AttackHash = Animator.StringToHash("Attack");
[SerializeField] EnemyDefinition definition;
Animator _animator; EnemyBrain _brain; ITargetProvider _targets;
public void Construct(ITargetProvider targets) => _targets = targets;
void Awake() { _animator = GetComponent<Animator>(); _brain = new EnemyBrain(definition.AttackRange); }
void Update() {
if (_brain.ShouldAttack(transform.position, _targets.PlayerPosition)) _animator.SetTrigger(AttackHash);
}
}
[SerializeField] privateover public fields; public API through properties and methods.Awakeinitialises the object itself;OnEnable/Startwire up to others;OnDisableunsubscribes everythingOnEnablesubscribed.[RequireComponent]for hard dependencies on the same GameObject;TryGetComponentinstead ofGetComponent+ null check.- Physics in
FixedUpdatethrough the Rigidbody API (MovePosition,AddForce,linearVelocity), nevertransform.positionon a dynamic body.
Data and events in ScriptableObjects
- Definitions (weapons, enemies, levels) are ScriptableObjects (data-driven-gameplay-design); runtime state is a separate object built from them. Changes made to a ScriptableObject in Play mode persist in the editor — never write gameplay state into one.
- Event channels (
GameEvent : ScriptableObjectwithRaise()and listeners) decouple scenes, but keep them few and named for what happened (PlayerDied), not what should happen.
Dependencies, not singletons
- Use the repository's container if it has one (VContainer, Zenject/Extenject): register services in a
LifetimeScope/installer and inject via constructor (plain classes) or[Inject]method (components). - Without one: a bootstrap scene or composition root creates services and passes them in (
Construct(...)as above). - No new
static Instancefor gameplay state. Engine-wide stateless helpers are fine as static classes.
Input, assets, async
- Input System, not the legacy
Inputclass — actions from the.inputactionsasset, rebinding and device glyphs (game-input-and-accessibility). A project still on the legacy Input Manager stays on it unless the task migrates it. - Addressables for content loaded at runtime (game-asset-pipeline); no new content in
Resources/. EveryLoadAssetAsynchandle is released. - Async:
Awaitable(Unity 6) or UniTask where the repo uses it; cancel withdestroyCancellationTokenso a destroyed object's continuation does not run.async voidonly for event handlers, with try/catch.
Serialization safety
- Renaming a serialized field loses its data in every scene and prefab unless you add
[FormerlySerializedAs("oldName")]. - Polymorphic serialized fields need
[SerializeReference]; plain[SerializeField]slices to the base type. - Keep Asset Serialization on Force Text so scenes and prefabs diff and merge (UnityYAMLMerge); prefer prefabs and nested prefabs over large scene edits so changes stay reviewable.
Unity 6 API notes (follow the pinned version)
Rigidbody.velocity→linearVelocity;drag/angularDrag→linearDamping/angularDamping.FindObjectsOfType/FindObjectOfType→FindObjectsByType(FindObjectsSortMode.None)/FindAnyObjectByType(and avoid both in gameplay paths).Object.InstantiateAsyncfor large prefabs;Awaitable.NextFrameAsync,Awaitable.WaitForSecondsAsync.- Unity 6 LTS lines: 6000.0 LTS support ends in October 2026, 6000.3 LTS is supported to the end of 2027 — never upgrade the project on your own.
Common Mistakes
GameObject.Find,FindAnyObjectByTypeorGetComponentinUpdate.- A MonoBehaviour holding the rule logic, making it testable only in PlayMode.
static Instancemanagers that depend on scene load order.- Renaming serialized fields without
FormerlySerializedAs. - Subscribing in
OnEnablewithout unsubscribing inOnDisable— leaks and calls on destroyed objects.
Red Flags
- A UI assembly referenced by gameplay code.
- A new
Resources.Loadcall. transform.position =on an object with a non-kinematic Rigidbody.- A component longer than a few hundred lines with rules, input and presentation mixed.
04 同仓库更多 Skills
visual-craft
暂无简介
ux-copy
Use when you write any words a user reads in a mockup or a hand-off - content language and voice, buttons, labels, errors, empty states, confirmations, numbers and dates, and the longest-string check
screen-mockup-html
Use when you draw screens as the design task's HTML mockup - ONE canvas document holding every screen and every variant side by side, the sanitizer-safe recipe, phone and wide frames, the states grid, static control states, and the loopback self-check with screenshots at 375, 768 and 1440 attached to the task
performance-awareness
How the performance score moves for your role. Use when the score context shows a declining trend, or before handing off work you have not self-checked against every criterion.
design-variants
Use when a screen design needs variants - how many, variants as side-by-side rows on the one canvas, what makes two variants genuinely different, recommending one, how the human's choice is read, and narrowing to the chosen one in revision
design-system-authoring
Use when you derive, change or extend a design system - where tokens live per stack, the DTCG token tree, role-based names for both themes, base vs layer vs drift, the DESIGN.md section order, the inventory format, the contrast check and the lint every proposal returns