Files
Project-M/.claude/skills/dots-dev/references/dots-conventions.md
T
Luis Gonzalez a5af81c8a8 Commit dots-dev skill into repo for cross-machine portability
Move the dots-dev skill from machine-local ~/.claude/skills/ into the
repo at .claude/skills/dots-dev/ so it travels with a clone. Update the
CLAUDE.md per-machine setup note to reflect that the skill no longer
needs manual placement; unity-mcp-skill and native memory/ stay local.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 11:58:37 -07:00

8.3 KiB

DOTS / Netcode for Entities — Conventions & Gotchas

Reusable rule set the /dots-dev skill enforces. Targets Unity 6.x, Entities 1.3+/1.4+, Netcode for Entities 1.x. These APIs move fast — anything flagged [verify] must be confirmed via context7 at code-time (see context7-libraries.md), not trusted from memory. The project's CLAUDE.md is authoritative where it differs.

These conventions replace classic MonoBehaviour/GameObject conventions. If a design reaches for a MonoBehaviour singleton, a ScriptableObject "service" for runtime simulation, [SerializeField] private data carriers, or coroutines for sim logic, it is almost certainly wrong for DOTS.

1. Code structure

  • struct : IComponentData — the default. Unmanaged, blittable, Burst- and job-friendly. Use for nearly all data.
  • class : IComponentData — managed component. Only for genuine managed refs (a GameObject, Material). Main-thread only, no Burst, no jobs. Keep out of hot paths.
  • IBufferElementData — per-entity dynamic array (DynamicBuffer<T>): inventory, path points, command history.
  • ISharedComponentData — groups entities into chunks by shared value; changing it is a structural change. Use sparingly; prefer unmanaged shared components.
  • IEnableableComponent — toggle a component on/off without a structural change. Prefer over add/remove tag components for frequently-flipped state. Enabled by default.
  • Systems: ISystem (struct, Burst-compatible — default) vs SystemBase (class, managed, main-thread, no Burst — only when you must touch managed objects). Tag system methods [BurstCompile].
  • SystemAPI is the entry point inside systems: SystemAPI.Query<...>(), .GetComponent, .Time, .GetSingleton. Source-generated; valid only inside ISystem/SystemBase.
  • Aspects (IAspect) are DEPRECATED as of Entities 1.4 — do not author new ones. Use components + SystemAPI.Query/IJobEntity directly. Entities.ForEach is legacy; avoid. [verify] the current recommended replacement.
  • Naming: components are nouns/adjectives (Health, Velocity, MoveSpeed); tags suffix-free or …Tag; authoring MonoBehaviours suffix Authoring; systems suffix System; buffer elements often …Element/…BufferElement.

2. Jobs & Burst

  • IJobEntity — preferred per-entity job (source-generated Execute over a query). IJobChunk — lower-level, chunk-wide/manual iteration. SystemAPI.Query — main-thread iteration.
  • SchedulingSchedule() / ScheduleParallel(); thread the returned JobHandle through state.Dependency. Avoid manual .Complete() unless you need results now (it's a sync point).
  • Burst breaks on — managed types, try/catch/exceptions, typeof/reflection, virtual calls, string ops, GC allocation, most of System.*. Keep job code unmanaged.
  • CollectionsNativeArray/List/HashMap, NativeQueue, etc. Allocators: Allocator.Temp (frame/scope, auto-freed, not across job boundaries), Allocator.TempJob (one job, must dispose), Allocator.Persistent (long-lived, must dispose). Always dispose non-Temp, or .Dispose(jobHandle).
  • Mark inputs [ReadOnly] to allow parallel access; write-aliasing across parallel jobs is a safety error.

3. Baking (authoring → entities)

  • Why: authoring data is converted offline at edit/import time into entities — no runtime conversion cost, deterministic, streamable.
  • Pattern: a MonoBehaviour named …Authoring + class FooBaker : Baker<FooAuthoring> whose Bake() calls GetEntity(...) then AddComponent(entity, new Foo{...}).
  • Subscenes hold baked entities and stream in/out asynchronously — entities are not present the instant a subscene reference exists; gate logic on load state.
  • Mistakes: referencing other entities without GetEntity(authoring, TransformUsageFlags.…); not declaring DependsOn (so bake doesn't re-run on asset change); doing runtime-only work in Bake(); forgetting TransformUsageFlags (→ missing LocalTransform).

4. Netcode for Entities

  • Ghosts = replicated entities. Mark a prefab with GhostAuthoringComponent; mark replicated fields with [GhostField]; tune per-component replication with [GhostComponent] (e.g. SendToOwner). Variants via IGhostComponentVariation. [verify] attribute parameters.
  • Predicted vs interpolated ghosts: predicted = simulated locally + rolled back (player-controlled, fast reaction). Interpolated = smoothed display of past server state (remote/non-critical).
  • Prediction & rollback: core sim runs in PredictedSimulationSystemGroup, a fixed-step group on both client and server. On each new snapshot the client re-simulates from the oldest received tick → systems run multiple times per frame; sim code must be idempotent/deterministic. Filter queries with .WithAll<Simulate>() so only currently-simulated entities run. [verify] group internals/names.
  • Server-authoritative: server is truth; clients send input, not state.
  • Input: prefer IInputComponentData (auto-buffered to InputBufferData<T>); ICommandData is the lower-level command-buffer form. Don't read raw UnityEngine.Input in sim systems. [verify] helper signatures.
  • RPCs: struct : IRpcCommand for one-off events (connect, requests). Not for per-tick state.
  • Determinism: no Time.deltaTime/wall-clock in sim — use the netcode fixed tick. Avoid non-deterministic float sources, System.Random, ordering by hash. Cross-machine float determinism is fragile — keep predicted sim simple and consistent.
  • Worlds & bootstrap: separate client and server Worlds; subclass ClientServerBootstrap (CreateClientWorld/CreateServerWorld) to customize. Annotate systems with [WorldSystemFilter(...)] to target client/server/thin. [verify] bootstrap method names + WorldSystemFilterFlags.

5. Common footguns

  • Structural changes (add/remove component, create/destroy entity, set shared component) invalidate Entity/component handles & references and cause sync points. Batch via ECB.
  • ECB (EntityCommandBuffer): record now, play back later. Use built-in Begin/EndSimulationEntityCommandBufferSystem (and BeginInitialization…). One ECB per job; in parallel jobs use .AsParallelWriter(). Don't read back values you just recorded.
  • System order: [UpdateInGroup], [UpdateBefore/After]. Key groups: InitializationSystemGroup, SimulationSystemGroup, PresentationSystemGroup; netcode adds PredictedSimulationSystemGroup, GhostSimulationSystemGroup. [verify] netcode group set.
  • Main-thread-only: managed components, EntityManager structural ops, most World/managed access. Keep out of Burst jobs.
  • No managed refs in unmanaged components — won't compile / breaks Burst. Use a managed IComponentData, a BlobAssetReference, or an Entity reference instead.
  • Subscene timing & GO↔entity mixing: entities may not exist yet; bridging GameObjects↔entities is manual and a known pain point — design around it, don't sprinkle hybrid links.

6. Testing (what "compiles clean" misses)

  • NetCodeTestWorld — spins up in-process client+server worlds for deterministic tick-by-tick netcode tests (connect, tick, assert on ghosts/RPCs).
  • Thin clients — stripped dummy clients for soak/load testing in-editor (no rendering/full sim); spawn via Multiplayer PlayMode Tools.
  • Entities logic — standard test world + World.Update() stepping (EntitiesTestFixture-style).
  • Burst/source-gen failures surface at editor compile/play, not a plain C# build. Determinism desync, rollback bugs, sync-point stalls, and structural-change invalidation only show at runtime under prediction — always run a play/tick test, not just a build.

7. Verify via context7 at code-time (volatile — do NOT hardcode)

  • Aspect deprecation status & the recommended replacement.
  • Exact system group names/order and netcode prediction-group internals.
  • Input API (IInputComponentData vs ICommandData) helper signatures.
  • Ghost attribute options ([GhostField]/[GhostComponent] parameters, variants).
  • Bootstrap/world-creation method names and WorldSystemFilterFlags.
  • ECB system names + allocator/Collections API surface.