15 KiB
Project M — CLAUDE.md
Multiplayer game on Unity DOTS (Entities) + Netcode for Entities — server-authoritative, input-only clients, client prediction. This file is committed and is the authoritative, cross-machine source of conventions. The /dots-dev skill drives feature work; the one-time stack setup lives in Docs/dots-setup-task.md.
Stack — reverting to Unity 6.4.7 (stable) as of 2026-05-30
| Package | Version | Notes |
|---|---|---|
com.unity.entities |
6.4.0 | Entities/Collections/Graphics track the Editor version (6.x). |
com.unity.entities.graphics |
6.4.0 | Renders entities under URP 17.4. |
com.unity.collections |
6.4.0 | (transitive) |
com.unity.netcode |
1.13.2 | Netcode for Entities (ECS). NOT com.unity.netcode.gameobjects. Independent 1.x line on Unity 6.4. |
com.unity.physics |
1.4.6 | Unity Physics (DOTS). Independent 1.x line on Unity 6.4. |
com.unity.transport |
2.7.2 | (transitive) |
com.unity.burst |
1.8.29 | (transitive) |
com.unity.mathematics |
1.3.3 | (transitive) |
⚠️ The values above are the Unity 6.4.7 set being reverted to — verify against
packages-lock.jsonafter the editor downgrade re-resolves, and reconcilemanifest.jsonif the brief 6.6 upgrade left explicit version pins.
Version history & status (2026-05-30): built on 6.4.7 (
6000.4.7f1; Netcode 1.13.2 / Physics 1.4.6 / Entities 6.4.0). Briefly upgraded to 6.6.0a6, where Netcode→6.6.0, Physics→6.5.0, Entities→6.5.0 all renumbered into the editor line — BUT the alpha's Netcode/Transport runtime is broken (all in-editor connections fail with "invalid wrapped network interface"; confirmed engine bug via a zero-gameplay repro — seeDocs/VaultDR-002 andDocs/UnityBugReport-Netcode-Transport-6.6.0a6.md). → Reverting to Unity 6.4.7 for stable netcode runtime. If returning to 6.6 later, expect the renumber and re-test the runtime. The M1 player slice should port to 6.4 / Netcode 1.13.2 with no or minimal changes — recompile andread_consoleafter the downgrade.
Namespaces & assembly split
Root namespace: ProjectM. Code lives under Assets/_Project/Scripts/ in four asmdefs (never create/edit .csproj/.sln; only .asmdef):
| Assembly | Namespace | Runs in | References |
|---|---|---|---|
ProjectM.Simulation |
ProjectM.Simulation |
client + server worlds | Entities, Unity.Transforms, Collections, Mathematics, Burst, Unity.Physics, Unity.NetCode |
ProjectM.Client |
ProjectM.Client |
client world only | + Simulation, Unity.Entities.Graphics, Unity.InputSystem |
ProjectM.Server |
ProjectM.Server |
server world only | + Simulation, Unity.Transforms, Unity.NetCode |
ProjectM.Authoring |
ProjectM.Authoring |
bake time (+ scene runtime) | Simulation, Entities, Unity.Entities.Hybrid, Collections, Mathematics, Unity.NetCode |
- Simulation = components + systems shared by both worlds (most gameplay). Client/Server = world-specific. Authoring =
…AuthoringMonoBehaviours +Baker<T>. - Other folders:
Assets/_Project/Subscenes/(baked entity subscenes),Assets/_Project/Prefabs/,Assets/_Project/Tests/EditMode/.
Build gotchas (learned — M1, 2026-05-30)
Unity.Transformsmust be a DIRECT asmdef reference for any assembly whose source-gen'd systems useLocalTransform/LocalToWorld. It is its own assembly; transitive visibility compiles your hand-written code but the Entities generator emits CS0246 inside the*.g.cs.- Authoring asmdefs need
Unity.Entities.Hybrid(definesBaker<T>) andUnity.Collections(baking source-gen). A nested baker class must not be namedBaker(it shadowsBaker<T>→ CS0308/CS0246) — name itFooBaker. - Never name an
IComponentDataPlayerInput, and don'tusing UnityEngine.InputSystem;in a file that references such a component: it collides withUnityEngine.InputSystem.PlayerInput, and the Entities generator bindsRefRW<…>to the managed class → a misleading CS8377 "must be a non-nullable value type". Fully-qualify Input System types (UnityEngine.InputSystem.Keyboard.current) instead. IInputComponentDatarequires implementingFixedString512Bytes ToFixedString().- An input-gather system that reads the managed Input System belongs in
GhostInputSystemGroupas a non-BurstISystem(orSystemBase), never inside the prediction loop.
Build gotchas (learned — M2 combat, 2026-05-31)
- The generated Input Actions C# wrapper must live inside an asmdef any system needs to reference. By default it generates next to the
.inputactions(e.g.Assets/Settings/), which has no asmdef → it compiles intoAssembly-CSharp, and asmdef assemblies (ProjectM.Client) cannot reference that. Fix: set the importer'swrapperCodePath(in the.inputactions.meta) to a path inside the consuming asmdef, e.g.Assets/_Project/Scripts/Client/Input/ProjectMInput.cs, and delete the old generated file. Read the action map via a managedSystemBaseholding the wrapper; gatherFireas a netcodeInputEvent(reset the field each frame,.Set()on the press edge — netcode latches the absoluteCountinto the command buffer; the live component value is only the per-tick delta). - Predicted-spawn classification cannot be
[BurstCompile]d (Netcode 1.13.2). The cross-assembly genericUnity.NetCode.LowLevel.SnapshotDataBufferComponentLookup.TryGetComponentDataFromSnapshotHistory<T>()trips a Burst internal compiler error (type-hash resolution). Make the classifier a plain non-BurstISystem(it only runs when spawns are received — cold path). In 1.13.2 that method takesref DynamicBuffer<SnapshotDataBuffer>(the public HelloNetcode sample's by-valuedatais from an older version). - A Burst internal compiler error corrupts the editor's Burst incremental cache. After the error is fixed in code, newly-added
[BurstCompile]entry points (systems and generated ghost-component serializers) keep logging"... is not a known Burst entry point"and run managed-fallback (slow → server tick-batching, ~30–40s play-enter). A clean compile + green tests + working runtime confirm the code is fine. Clear it with an editor restart (or deleteLibrary/BurstCachewhile closed) — a domain reload alone does not. - Projectile/area hit tests must be swept, not point checks. A point distance check tunnels straight through a target when the per-tick step exceeds the target radius — at high projectile speed or whenever the server tick-batches under load. Test the segment the projectile traversed this tick (
[curPos - dir*speed*dt, curPos]) against each target; order the damage system[UpdateAfter(MoveSystem)]. (Caught at runtime, not by a point-based unit test — cover hit detection with a tunnelling regression test.) - In-editor input injection needs a focused Game view — unless you change two settings. By default the Input System ignores injected/real device input while the Game view is unfocused, so headless (MCP
execute_code) keypress simulation won't driveIInputComponentData. Fix (both now set in this project):InputSettings.editorInputBehaviorInPlayMode = AllDeviceInputAlwaysGoesToGameView+Application.runInBackground = true. For deterministic, device-independent validation prefer the editor-onlyDebugInputInjectionSystem(ProjectM.Client,#if UNITY_EDITOR): poke its statics fromexecute_code—DebugInputInjectionSystem.Fire()/.SetMove(x,z)/.SetAim(x,z)/.Stop()— to drive the local player'sPlayerInputthrough the authentic command→prediction pipeline. (Validated:SetMovedrives + replicates movement. One-shotFirepropagation needs a healthy editor — tick-batching under a degraded/corrupt-Burst editor drops one-shotInputEvents while continuous values survive.) - Prototype presentation glue lives in
ProjectM.Clientas MonoBehaviours.PrototypeCameraRig(on the Main Camera) is a tunable player-following ARPG cam (default mid 3/4 ~45° perspective) that reads the local player ghost'sLocalTransformeach LateUpdate. Bright prototype URP-Lit materials are inAssets/_Project/Materials/(player cyan, dummy red, projectile yellow, ground grey).ProjectM.Clientnow referencesUnity.Transformsdirectly (the rig readsLocalTransform).
Bootstrap & worlds
ProjectM.Simulation.GameBootstrap : ClientServerBootstrap→ overridesInitialize, setsAutoConnectPort = 7979(in-editor auto-connect over IPC; set in M1 — was 0), callsCreateDefaultClientServerWorlds(). Entering Play Mode creates separateServerWorld(WorldFlags.GameServer) andClientWorld(WorldFlags.GameClient) — verified.Assets/_Project/Subscenes/Gameplay.unityis wired intoSampleScene(GameObjectGameplaySubScene) as a baking target. ReplaceSampleScenewith a dedicated bootstrap scene when building for real.
DOTS / ECS conventions (authoritative summary)
Full rules: ~/.claude/skills/dots-dev/references/dots-conventions.md (Windows: %USERPROFILE%\.claude\skills\dots-dev\references\). These replace classic MonoBehaviour/GameObject patterns.
struct : IComponentDatais the default (unmanaged, Burst/job-friendly).class : IComponentDataonly for genuine managed refs (main-thread, no Burst).IBufferElementDatafor per-entity arrays.IEnableableComponentto toggle state without a structural change.- Systems:
ISystem(struct) +[BurstCompile]is the default;SystemBaseonly when touching managed objects.SystemAPI.Query<…>()to iterate. Aspects (IAspect) are DEPRECATED (Entities 1.4+) — do not author new ones.Entities.ForEachis legacy. - Jobs:
IJobEntity/IJobChunk; threadJobHandlethroughstate.Dependency; mark inputs[ReadOnly]. Allocators:Temp(frame),TempJob(one job),Persistent(must dispose). Burst breaks on managed types/exceptions/reflection/strings. - Structural changes (add/remove component, create/destroy entity) invalidate handles + cause sync points → batch via
EntityCommandBuffer(Begin/EndSimulationEntityCommandBufferSystem;.AsParallelWriter()in parallel jobs). - Baking:
…AuthoringMonoBehaviour +class FooBaker : Baker<FooAuthoring>→GetEntity(authoring, TransformUsageFlags.…)thenAddComponent. Subscenes stream async — entities aren't present the instant a reference exists. - Netcode: ghosts = replicated entities (
GhostAuthoringComponent+[GhostField]); predicted (player-controlled, rolled back) vs interpolated. Core sim runs inPredictedSimulationSystemGroup(fixed step, runs multiple times per frame on rollback → must be deterministic/idempotent; filter with.WithAll<Simulate>()). Server-authoritative: clients send input (IInputComponentData), not state. RPCs (IRpcCommand) for one-off events. No wall-clock/Time.deltaTime/System.Randomin predicted sim. - Always verify volatile DOTS/Netcode API shape via context7 at code-time — do not trust memory. See
context7-libraries.md. Pinned IDs for our versions: Entities →/websites/unity3d_packages_com_unity_entities_6_5_manual; Netcode →/websites/unity3d_packages_com_unity_netcode_1_10_api(closest published; we run 1.13.2 — re-resolve if a 1.13 set appears); ECS samples →/unity-technologies/entitycomponentsystemsamples.
Testing
- Default pattern = plain-Entities EditMode test: create a
World, register the system inSimulationSystemGroup, tick, assert. Public API, always green, version-independent. Example:Assets/_Project/Tests/EditMode/HeartbeatSystemTests.cs. Run via Unity Test Runner or MCPrun_tests(mode="EditMode", assembly_names=["ProjectM.Tests.EditMode"]). NetCodeTestWorldisinternalin netcode 1.13.2 (Unity.NetCode.Tests, assemblyUnity.NetCode.TestsUtils.Runtime.Tests), exposed only via a fixed[InternalsVisibleTo]allow-list of Unity assemblies. To use it you must name a test asmdef to match an entry (e.g.Unity.NetcodeSamples.EditModeTests) — or vendor the test utils. SeeDocs/Vault/07_Sessions/_Decisions/DR-001_Netcode_Test_Harness.md. This does not change on Unity 6.6. Netcode world boot is covered by the Play Mode check, not a NetCodeTestWorld test.- Burst/source-gen errors surface at editor compile, not a plain build — always check
read_consoleafter script changes, and run a play/tick test, not just a compile.
Guardrails
- Never edit a
.metaindependently of its asset; delete an asset and its.metatogether. - Never read/write
Library/,Temp/,obj/,Logs/,UserSettings/(generated/cache). Use MCP resources for editor state instead. - Never create/edit/commit
.csproj/.sln— only.asmdef. - No asset/scene edits during Play Mode. Check
editor_state.advice.ready_for_toolsbefore mutating; package adds/refreshes trigger domain reloads — wait foris_compiling=false.
Memory — four layers (which tool when)
| Layer | Use for | Crosses machines? |
|---|---|---|
In-repo vault Docs/Vault/ |
Design docs, decision records (DR-###), session logs, roadmap — human-facing truth | Yes (git) |
| basic-memory MCP | Semantic/wikilink recall over those same vault files | Yes (indexes the vault) |
| serena MCP | C# symbol nav (find_symbol, references) of Assets/_Project/ |
N/A (from code) |
Native Claude memory (memory/, MEMORY.md) |
Machine-local facts, working-style, preferences | No |
- Where is X defined / who calls it → serena (fallback
Grep/Glob). What did we decide / how does Z work → basic-memory → read the vault note. Literal string / asset GUID → Grep/Glob. Current DOTS API → context7. Conventions → this file. - Cross-machine rule: durable truth goes in the vault or this file (both committed). Native
memory/is local-only and does NOT sync — never the sole home of a decision. - serena C# caveat: its language server is flaky on Unity (can auto-install the wrong .NET,
.slnload timeouts). Iffind_symbolerrors/stalls, fall back toGlob/Grep(or addclaude-contextwith local embeddings as a code-search index). serena live-verification was deferred at setup; confirm on first use.
Per-machine setup (NOT in git — redo on each machine)
.mcp.json is committed and portable (${CLAUDE_PROJECT_DIR} only). But each machine still needs:
uv/uvx, the Obsidian app +obsidian-cli, and thedots-devskill in~/.claude/skills/.- basic-memory project registration (machine-local config):
uvx basic-memory project add gamevault "<repo>/Docs/Vault" --default, thenuvx basic-memory reindex --full --search --embeddings --project gamevault. - Unity 6.4 opens the project and the CoplayDev Unity-MCP bridge connects (
mcpforunity://editor/state→ready_for_tools).