4ac1ae5a2e
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>
8.9 KiB
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 (aGameObject,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) vsSystemBase(class, managed, main-thread, no Burst — only when you must touch managed objects). Tag system methods[BurstCompile]. SystemAPIis the entry point inside systems:SystemAPI.Query<...>(),.GetComponent,.Time,.GetSingleton. Source-generated; valid only insideISystem/SystemBase.- Aspects (
IAspect) are DEPRECATED as of Entities 1.4 — do not author new ones. Use components +SystemAPI.Query/IJobEntitydirectly.Entities.ForEachis legacy; avoid. [verify] the current recommended replacement. - Naming: components are nouns/adjectives (
Health,Velocity,MoveSpeed); tags suffix-free or…Tag; authoring MonoBehaviours suffixAuthoring; systems suffixSystem; buffer elements often…Element/…BufferElement.
2. Jobs & Burst
IJobEntity— preferred per-entity job (source-generatedExecuteover a query).IJobChunk— lower-level, chunk-wide/manual iteration.SystemAPI.Query— main-thread iteration.- Scheduling —
Schedule()/ScheduleParallel(); thread the returnedJobHandlethroughstate.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,stringops, GC allocation, most ofSystem.*. Keep job codeunmanaged. - Collections —
NativeArray/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
MonoBehaviournamed…Authoring+class FooBaker : Baker<FooAuthoring>whoseBake()callsGetEntity(...)thenAddComponent(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 declaringDependsOn(so bake doesn't re-run on asset change); doing runtime-only work inBake(); forgettingTransformUsageFlags(→ missingLocalTransform).
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 viaIGhostComponentVariation. [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 toInputBufferData<T>);ICommandDatais the lower-level command-buffer form. Don't read rawUnityEngine.Inputin sim systems. [verify] helper signatures. - RPCs:
struct : IRpcCommandfor 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; subclassClientServerBootstrap(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-inBegin/EndSimulationEntityCommandBufferSystem(andBeginInitialization…). 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 addsPredictedSimulationSystemGroup,GhostSimulationSystemGroup. [verify] netcode group set. - Main-thread-only: managed components,
EntityManagerstructural ops, mostWorld/managed access. Keep out of Burst jobs. - No managed refs in unmanaged components — won't compile / breaks Burst. Use a managed
IComponentData, aBlobAssetReference, or anEntityreference 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 inSimulationSystemGroup→SortSystems→Update()→ assert. Public API, version-independent. This is the project's actual harness. - Extract pure logic to a
*Math.cshelper 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 byworld.Name == "ServerWorld"/"ClientWorld").NetCodeTestWorldisinternalin 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 (
IInputComponentDatavsICommandData) helper signatures. - Ghost attribute options (
[GhostField]/[GhostComponent]parameters, variants). - Bootstrap/world-creation method names and
WorldSystemFilterFlags. - ECB system names + allocator/Collections API surface.