a5af81c8a8
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>
8.3 KiB
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 (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)
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 (
IInputComponentDatavsICommandData) helper signatures. - Ghost attribute options (
[GhostField]/[GhostComponent]parameters, variants). - Bootstrap/world-creation method names and
WorldSystemFilterFlags. - ECB system names + allocator/Collections API surface.