Files
kronic 4ac1ae5a2e Rewrite /dots-dev skill: Workflow-first, two-review sandwich, 6.5 pins
Realigns the skill with how sessions actually run now — it predated the
Workflow tool and the ultracode two-review practice. Driven by an analysis
of the full session history + the 26-file memory corpus + skill-authoring
research, adversarially reviewed.

Key changes:
- Workflow-first orchestration: drop the dead manual "swarm (<=N agents)"
  model and the impossible 3-5-agent impl swarm; implementation is serial
  orchestrator MCP edits. New references/workflow-patterns.md (ground
  fan-out + design-review lens/critic) replaces agent-briefs.md.
- Pre-code design-review + post-impl diff-review are now first-class gated
  phases (the spine that catches what green tests + a clean Play miss).
- Size by blast-radius / netcode-heaviness, not time estimates.
- Lean SKILL.md (222 -> 131 lines, -25% KB): leans on CLAUDE.md instead of
  duplicating its MCP cheat-sheet / anti-patterns / error-recovery.
- ctx7 CLI / find-docs mechanism (the MCP verbs are gone); live-verified
  6.5-era library pins; kill the dead NetCodeTestWorld test path.
- Encode operator gates (no-time-estimates, present-forks, never-defer,
  tuning-autonomy) + the highest-recurrence MCP-edit / Workflow gotchas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 21:19:49 -07:00

8.9 KiB

DOTS / Netcode for Entities — Conventions & Gotchas

Reusable rule set the /dots-dev skill enforces. Targets Unity 6.5, Entities 6.5, Netcode for Entities 6.5 (≡ the old ~1.12/1.13 API lineage), Unity Physics 6.x — the unified 6.x DOTS line. These APIs move fast — anything flagged [verify] must be confirmed via ctx7/find-docs 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)

  • Default = plain-Entities EditMode test: new World → register the system in SimulationSystemGroupSortSystemsUpdate() → assert. Public API, version-independent. This is the project's actual harness.
  • Extract pure logic to a *Math.cs helper and unit-test it — deterministic, fast, version-proof. Cover swept hit-detection with a tunnelling regression (a point-based test won't catch the tunnel).
  • Netcode coverage = one live Play smoke: boot client+server (execute_code), step ticks, assert server == client for the replicated surface (identify worlds by world.Name == "ServerWorld"/"ClientWorld"). NetCodeTestWorld is internal in this line (exposed only to a fixed allow-list) and 0 of the project's test files use it — do not prescribe it as the primary path. (Thin clients via Multiplayer PlayMode Tools exist for soak/load testing if ever needed — not in current practice.)
  • 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.