t1k:unity:dots-core:architecture
| Field | Value |
|---|---|
| Module | dots-core |
| Version | 2.3.2 |
| Effort | high |
| Tools | — |
Keywords: architecture, design patterns, DOTS, ECS
How to invoke
Section titled “How to invoke”/t1k:unity:dots-core:architectureDOTS Architecture — Component & System Design
Section titled “DOTS Architecture — Component & System Design”Skill Purpose
Section titled “Skill Purpose”Decision framework for Unity DOTS ECS architectural choices. Guides DOTS vs MonoBehaviour tier selection, component/system creation, SOLID principles for ECS, and anti-patterns that hurt reusability.
Library scope: gameplay mechanics ONLY. The DOTS library handles combat, AI, navigation, inventory, progression, puzzles, and game flow — pure gameplay data and logic. Rendering, UI, audio, and platform-specific code do NOT belong in DOTS packages. Use MonoBehaviour/UI Toolkit (Tier 2/3) for non-gameplay concerns — they are better suited and avoid unnecessary DOTS complexity.
Related skills:
dots-ecs(API reference) ·dots-rpg(domain components) ·dots-jobs-burst(performance) ·dots-performance(profiling, chunk efficiency, parallelism)
When This Skill Triggers
Section titled “When This Skill Triggers”- Deciding whether to create a new component or extend an existing one
- Deciding whether to create a new system or add logic to an existing one
- Reviewing component granularity (too fat? too thin?)
- Reviewing system responsibility (doing too much?)
- Planning module boundaries for a reusable DOTS package
- Refactoring ECS code for reusability across projects
- Checking for demo-specific logic in reusable packages
Quick Decision Trees
Section titled “Quick Decision Trees”Create vs Update Component?
Section titled “Create vs Update Component?”Create vs Update System?
Section titled “Create vs Update System?”SOLID for ECS
Section titled “SOLID for ECS”See solid-ecs.md
Reusability Checklist
Section titled “Reusability Checklist”Atomicity Audit
Section titled “Atomicity Audit”Refactor / Rename a Library Symbol
Section titled “Refactor / Rename a Library Symbol”See refactor-rename-checklist.md
Addressables-First Pattern (library v2.0.0+)
Section titled “Addressables-First Pattern (library v2.0.0+)”See addressables-pattern.md — AssetReferenceGameObject authoring → AssetReferenceBakerExtensions.BakeAssetReference → runtime Entity prefab. Enforcement test bans Resources.Load( / AssetDatabase.LoadAssetAtPath / non-ECS Object.Instantiate(GameObject) in Runtime asmdefs. Consumer projects MUST ship AddressablesBuildPlayerProcessor + AddressablesEnforcement.Allowlist.
3-Tier Architecture (DOTS vs MonoBehaviour)
Section titled “3-Tier Architecture (DOTS vs MonoBehaviour)”See dots-vs-mono-tiers.md for full decision framework.
| Tier | When | Entity Count | Burst? | Example |
|---|---|---|---|---|
| 1: Pure DOTS | Per-entity gameplay | 10-10K | Yes | Combat, Nav, AI, Stats, Spawning |
| 2: DOTS Data + MB Bridge | Singleton → managed API | 1 | No | Camera (Cinemachine), Audio, VFX, UI bindings |
| 3: Pure MonoBehaviour | No ECS data | 0 | No | Scene loading, Save/Load, Localization |
Quick rule: 10+ entities → Tier 1. Singleton + managed API → Tier 2. No ECS → Tier 3.
Bridge Helpers — Use These for ALL ECS-Reading MonoBehaviours
Section titled “Bridge Helpers — Use These for ALL ECS-Reading MonoBehaviours”ECSMonoBehaviourBase is the canonical base class for any runtime MonoBehaviour that reads ECS singletons — not just UI. Audit (2026-04-26) found the World.DefaultGameObjectInjectionWorld != null && IsCreated guard duplicated in 28 demo files (input handlers, camera controllers, bootstraps). All should inherit ECSMonoBehaviourBase and override OnECSUpdate(EntityManager em) instead of re-implementing the world-ready guard.
Sibling helper: SubsceneBypassBootstrap<TLevelData> — for JSON-driven config bootstraps that need to skip SubScene baking. Override OnConfigure(EntityManager em, TLevelData data); base class handles JSON parse + world-readiness guard.
Rule: any non-DOTS MonoBehaviour reading ECS state in Update() / LateUpdate() MUST inherit one of these two helpers. No raw World.DefaultGameObjectInjectionWorld checks in new code.
Key Principles
Section titled “Key Principles”- Components = Data, Systems = Logic — never mix
- One system, one job — each system does exactly one transformation
- Tag > Enum dispatch — prefer tag components over byte/enum branching for OCP
- Config > Constants — tuning values belong in component data, not
GameplayConstants - Buffer priority — when filling fixed-capacity buffers, always prioritize important entries
- Bridge pattern — isolate external dependencies behind a bridge system (read abstraction -> write concrete)
- Atomicity — components and systems must be atomic (indivisible, single-concern). See atomicity-guide.md
- Example: A large component with 12+ fields mixing concerns -> split into 3 focused components by domain
- Example: A monolithic reset system -> per-module resets (Combat/AI/Inventory) + shared signal tag bridge
Dimension-Agnostic Design
Section titled “Dimension-Agnostic Design”When building systems that need to work in both 2D and 3D:
| Approach | Verdict | Why |
|---|---|---|
Position2D component | WRONG | Duplicates Position, forces system branching |
#if DOTS_2D | WRONG | Forks library, doubles maintenance |
ISystem<T2D, T3D> generics | WRONG | Over-engineering, Burst unfriendly |
| float3 everywhere (z=0 for 2D) | CORRECT | All math works identically; no branching |
| Conditional via data | CORRECT | Systems auto-skip via [RequireMatchingQueriesForUpdate] |
| Enum fields for shape variants | CORRECT | AoEShape.Circle/Cone/Line — single system handles all |
| Bool flags for behavior | CORRECT | ParabolicArc.IsFlat — linear vs parabolic in one system |
Key pattern: Configuration authoring sets different data -> same systems produce different behavior.
Performance Architecture
Section titled “Performance Architecture”-> See dots-performance skill for chunk efficiency calculator, system parallelism rules, and profiling workflows.
Key performance principles for architectural decisions:
- Smaller components = more entities per 16KB chunk = better cache
- IEnableableComponent over add/remove for frequently changing state (no archetype move)
- ISystem + [BurstCompile] always — never SystemBase for new code (5x faster)
- ScheduleParallel for >100 entities, SystemAPI.Query for <100, Run for <10
- Split systems when they process 2+ independent data sets (enables parallelism)
Anti-Patterns (Never Do)
Section titled “Anti-Patterns (Never Do)”| Anti-Pattern | Fix |
|---|---|
| System reads enum and branches to 3+ behaviors | Split into separate systems with tag queries |
| Component has 10+ fields, most unused per entity | Split into focused components |
| System does perception AND state management | Extract into separate systems |
Demo tuning values in GameplayConstants | Move to per-entity component config |
| Filling buffer without priority ordering | Always prioritize important entries (enemies > allies) |
Main-thread SystemAPI.Get* in nested loops | Jobify with ComponentLookup<T> |
using 5+ module namespaces in one system | System has too many responsibilities — split it |
SystemBase for any new system | Use ISystem struct — 5x faster, full Burst support |
Missing [BurstCompile] on system methods | Silent Mono fallback — 10-100x slower |
Managed calls in [BurstCompile] code | Silent ISystem failure — system never registers, no error |
Speed zone writing to MoveSpeed.Value | Speed zones are transient — store multiplier in WaypointFollower.SpeedMultiplier, apply locally. Mutating the canonical stat component corrupts StatSyncSystem’s derived pipeline |
| Duplicating utility loops (FindWalletIndex, FindDefinition) | Extract to static [BurstCompile] utility class in {Domain}/Utilities/ folder. Pattern: CurrencyWalletUtility, QuestUtility, AIHelpers |
| Game-specific OR over-specific instance names in library packages | NEVER use game/demo names (ColorFit, BackpackCrawler) in Packages/ code. Beyond demo names, also name the generic ROLE, not a concrete instance: ProjectileRegistryTag not ArrowRegistryTag (when a generic ProjectileData/ProjectileCollisionSystem already exists, Arrow is an inconsistent island); QueuePuzzle not ColorFitGameState. Mage/Knight/Sword/Arrow belong in enum values or demo content, never in a type name. Ask: “Would this name make sense in a different game?” See library-quality-mandate-unity.md § “Naming charter” (SSOT). |
| Skipping Play mode validation after implementation | ALWAYS run Play mode test (enter Play via MCP or dots-validator) after implementing new systems. EditMode tests only verify logic — Play mode catches baking failures, SubScene issues, system ordering bugs, and rendering problems |
| Renaming/deleting a public library symbol without grepping consumers | Run grep -rn "OldName" Packages/ Assets/Demos/ BEFORE the deletion. Migrate every caller in the SAME commit. See refactor-rename-checklist.md |
| Moving a Runtime helper to a Tests asmdef without checking Runtime consumers | If consumer Runtime code (e.g. Assets/Demos/**/Runtime/**) extends or references the type, the move breaks the consumer. Check consumers and either keep in Runtime, or migrate the consumer in the same commit |
Foundation asmdef references a mode/genre asmdef (e.g. DOTSCore.UI → DOTSGamemodes.Survivor) | Layering inversion — dependencies must flow DOWN only. Route the shared data through the infrastructure layer (dots-bridges) or a foundation-owned event type instead. dots-core → dots-bridges → mode-package is the only clean DAG. See Gotchas → “Package layering”. |
Refactor / Rename a Library Symbol — Pre-Delete Checklist
Section titled “Refactor / Rename a Library Symbol — Pre-Delete Checklist”Before deleting OR renaming OR relocating ANY public type, method, namespace, or asmdef in a consumed DOTS library package (e.g., Packages/unity-dots-library/):
- Grep both halves of the workspace:
grep -rn "SymbolName" Packages/ Assets/ --include="*.cs" - Migrate every caller in the SAME commit as the rename — never as a follow-up
- Check asmdef constraints: if you move a type from a Runtime asmdef to a Tests asmdef (with
UNITY_INCLUDE_TESTSdefine), every Runtime consumer breaks at runtime, not at compile time in the test runner - Check namespace continuity: if the file moves but the namespace stays, callers might still resolve — but if BOTH change, every
using X.Y;is now broken - Run
read_consolevia Unity MCP before pushing the rename commit — zero new errors required
See refactor-rename-checklist.md for the full checklist (file moves, partial classes, attribute duplication on partials, asmdef visibility, submodule pointer drift downstream).
Gotchas
Section titled “Gotchas”-
System update order is set per-world, not per-group — moving a system between groups changes its phase silently if the group’s update order isn’t documented.
-
SystemBase vs ISystem performance differs in Burst — ISystem is fully Burst-compilable; SystemBase has managed glue. Use ISystem when latency matters.
-
EntityCommandBuffer playback at the END of system update — not where it’s recorded — race conditions appear if you assume immediate effect.
-
World destruction order matters — destroying child worlds before parent leaks system instances.
-
Bake-time
GetEntity(prefab)vs runtimeAssetReferenceGameObject— pick the right tier. Both produce anEntityyou canInstantiate, but the failure modes differ sharply. Decision tree:Question If YES → bake-time If NO → addressable Is the prefab guaranteed to exist when the SubScene opens? (always in build, no DLC, no late content) ✓ baker.GetEntity(prefab, TransformUsageFlags.Dynamic)✗ switch to addressable Loaded exactly once per session (boot-time content)? ✓ bake-time ✗ switch Total prefab memory < 50 MB for the owning SubScene? ✓ bake-time ✗ addressable streams + unloads Need to instantiate from systems running BEFORE the owning SubScene finishes streaming? ✗ — bake-time Entity doesn’t exist yet ✓ addressable + per-system RequireForUpdate<MyPrefabSingleton>()Need to swap variants at runtime (different model per region/event)? ✗ — bake-time bakes ONE variant ✓ addressable keyed by region/event Cross-SubScene reference (system in SubScene A spawns SubScene B prefab)? ✗ — GetEntity(prefab)from baker in A only sees A’s content✓ addressable + AssetReferenceBakerExtensions.BakeAssetReferenceBake-time tell: consumers
RequireForUpdate<MyPrefabRef>()whereMyPrefabRef : IComponentData { public Entity Value; }is baked once viaAddComponent(new MyPrefabRef { Value = baker.GetEntity(prefab, TransformUsageFlags.Dynamic) }). No runtime indirection. Addressable tell: prefab lives behind[SerializeField] AssetReferenceGameObject prefab;in authoring; baker callsAssetReferenceBakerExtensions.BakeAssetReference(this, prefab)→ produces a runtimeBakedAssetReference<Entity>systems resolve viaResolveBakedEntityRequired(...)after scene load. Anti-pattern (silent failure):Resources.Load<GameObject>(...)in Runtime asmdef — banned post library v2.0.0 (gate:DOTSCore.Tests.Addressables.AddressablesEnforcementTests). If you must support both modes, gate behind aUseAddressablesbaker config; never sprinkle#ifbetween the two patterns.Field reports: see references/incidents.md.
-
Package layering — foundation never references a mode/genre layer. The library DAG is
dots-core(foundation) →dots-bridges(infrastructure) → mode/genre packages (dots-gamemodes,dots-puzzlemodes). Dependencies flow DOWN only. A foundation type reaching UP into a mode package is an inversion — even if it compiles, because Unity asmdefs only catch literal cycles, not conceptual layering. Real incident (W6.3):DOTSCore.UI.Combat.FloatingDamageNumberRendereronce drainedDOTSGamemodes.Survivor.DamageNumberEvent, making theDOTSCore.UIasmdef referenceDOTSGamemodes.Runtime(foundation → mode = wrong direction). Fix: define the shared contract in the infrastructure layer and have both sides depend on it. The renderer was re-targeted to drainDOTSBridges.UI.FloatingText.FloatingTextRequestfrom theFloatingTextHubsingleton, soDOTSCore.UInow referencesdots-bridges(down) instead ofdots-gamemodes(up). Detection heuristic: ausingin aDOTSCore.*/DOTSAI.*file naming aDOTSGamemodes.*/DOTSPuzzleModes.*namespace is almost always an inversion — route the data throughdots-bridgesor adots-coreevent type instead.
Architecture References (W4.5)
Section titled “Architecture References (W4.5)”→ Archetype layout, chunk fill rate, structural-change cost model: references/archetype-design.md
→ System group hierarchy, UpdateInGroup, ECB system selection: references/system-group-ordering.md
→ Baker patterns: TransformUsageFlags, singletons, buffers, DependsOn: references/baker-patterns.md
→ World bootstrap, VContainer DI bridge, ManagedEventQueue: references/world-bootstrap-di.md
Related rules
Section titled “Related rules”rules/library-feature-discovery-protocol.md— research the library 3× before implementing new functionality; always update this skill with what you find (or with the new code if you implement it)rules/manual-correction-implies-skill-gap-unity.md— if you manually inject library/API knowledge into a teammate brief, that knowledge belongs in this skill, not the brief