CC Package and Physics

This commit is contained in:
2026-06-02 08:56:26 -07:00
parent a5af81c8a8
commit 2ee30c01fd
37 changed files with 1295 additions and 142 deletions
+11 -1
View File
@@ -28,4 +28,14 @@ Unordered pool of candidate work. Promote to a [[Milestones|milestone]] when com
- [ ] **M3 follow-up — multi-prefab abilities** (a per-ability *different* projectile ghost) needs `ProjectileClassificationSystem` generalized beyond the single shared prefab.
- [ ] **M3 follow-up — standalone-server debug modifier path** via `IRpcCommand` (current `DebugModifierInjectionSystem` is in-editor single-process only).
- [ ] **M3 follow-up — rate-limited turning** (`PlayerAimSystem` still snaps rotation; `EffectiveCharacterStats.TurnRate` is wired but unused).
- [ ] **M3 polish — pickup visuals** (primitive sphere/default material currently); pickup auto-grant feel (continuous overlap).
- [ ] **M3 polish — pickup visuals** (primitive sphere/default material currently); pickup auto-grant feel (continuous overlap).
- [ ] **M5 — base subscene streaming** (the other half of M5): persistent home-base subscene that streams in/out around players. Physics-in-prediction slice is done ([[DR-006_M5_Physics_In_Prediction]]).
- [ ] ~~**M5 follow-up — same-tick movement**~~ — moot after M5b (the CC character runs in the predicted fixed-step group; M5's `PlayerMoveSystem` is deleted). See [[DR-007_M5b_Character_Controller_Package]].
- [ ] **M5b follow-up — multi-client CC interpolation validation**: live two-build / thin-client run to confirm remote-peer interpolation smoothness (predicted-only `CharacterInterpolation` variant is in; only single-client validated). Pairs with the deferred M4 real-LAN two-build test.
- [ ] **M5b follow-up — player-vs-player collision**: currently non-physical (`SimulateDynamicBody=false`); enable + handle masses if mutual push is wanted.
- [ ] **M5b follow-up — KinematicCharacterBody velocity ghost variant** (replicate RelativeVelocity/IsGrounded) if owner-prediction reconciliation looks jittery under latency.
- [ ] **M5b follow-up — gravity/verticality**: CC character is gravity-free + planar (no floor collider). Add gravity in the processor + a ground collider + reconsider `SnapToGround` if terrain/verticality is introduced.
- [ ] **M5b follow-up — CC package version**: 1.4.2 declares `entities/physics@1.3.x` (resolves via SemVer floor on our 6.4.0/1.4.6). Move to a CC build explicitly targeting Entities 6.x when published.
- [ ] **M5 follow-up — physics lag compensation** (`NetCodePhysicsConfig.EnableLagCompensation` + `PhysicsWorldHistorySingleton`) if/when physics-based hit detection replaces the swept-segment server check.
- [ ] **M5 follow-up — projectiles as physics bodies** (currently kinematic + swept hit); convert for projectile-vs-world collision.
- [ ] **M5 follow-up — hard rotation lock** (`PhysicsMass.InverseInertia=0`); `Rigidbody.FreezeRotation` is not honored by the DOTS baker, so rotation is held by per-tick angular-zero + aim write.
+1 -1
View File
@@ -15,7 +15,7 @@ permalink: gamevault/06-roadmap/milestones
| **M2 — Combat** | Directional ability fire + deterministic soft auto-target; server-authoritative damage/health | ✅ Done 2026-05-31 — runtime-validated on 6.4.7: input→fire→**predicted projectile**→**swept hit**→server damage→`Health` `[GhostField]` replicated server→client; movement + fire confirmed live; EditMode 22/22. Predicted-projectile + server auto-target + non-Burst classifier — [[DR-003_M2_Combat_Netcode_Architecture]], [[2026-05-31_M2_Combat]]. (Projectile ghost-map errors were later root-caused to a `[ReadOnly]`-write in `ProjectileClassificationSystem` — fixed 2026-06-01, see [[2026-06-01_M4_LAN_CoOp_And_Classification_Fix]] — NOT two-editor tick-batching as first thought.) |
| **M3 — Data-driven abilities & modifiers** | Ability **and** character stats authored in ScriptableObjects, baked to DOTS **blob assets**; runtime **flat + % modifier** stacks (upgrades/buffs) → effective stats, server-authoritative + prediction-correct. Pattern slice: refactor the current projectile ability + 12 sample abilities onto the data model. | ✅ Done 2026-05-31 — runtime-validated on 6.4.7: blob DB baked into both worlds; data-driven base + replicated `StatModifier` ghost buffer → **identical effective stats on server & owner-predicted client** (held under tick-batching); data-only ability swap; real pickup grant; EditMode 38/38. Blob DB + replicated modifier buffer + every-tick effective recompute — [[DR-004_M3_DataDriven_Abilities_Modifiers]], [[2026-05-31_M3_Data_Driven_Abilities]]. |
| **M4 — Co-op** | 24 players; client-hosted listen-server (Direct IP/LAN now, Unity Relay later) | 🚧 In progress 2026-06-01 — **LAN slice done + runtime-validated**: no-auto-connect `ConnectionConfig` + request-component host/join, editor auto-host + thin clients, deterministic ring spawn; 3 clients (1 real + 2 thin) connect→spawn (distinct slots)→replicate→clean disconnect; `ConnectionUI` for builds; EditMode 45/45. **Unity Relay + real two-build LAN join deferred** — [[DR-005_M4_Connection_Model_Direct_IP]], [[2026-06-01_M4_LAN_CoOp_And_Classification_Fix]]. |
| **M5 — Home base + physics** | Persistent base subscene streaming + Unity Physics in the predicted loop | |
| **M5 — Home base + physics** | Persistent base subscene streaming + Unity Physics in the predicted loop | 🚧 In progress 2026-06-01 — **physics-in-prediction slice done + runtime-validated** on 6.4.7: player is a velocity-driven dynamic Unity Physics body in the predicted loop (built-in `CapsuleCollider`+`Rigidbody` bake; `PhysicsVelocity` auto-replicated), collides with baked static walls (stops at the surface, no tunnel/climb-over), planar-pinned, **server == client** with no desync; EditMode 51/51. **Base subscene streaming deferred** to a later pass — [[DR-006_M5_Physics_In_Prediction]], [[2026-06-01_M5_Physics_In_Prediction]]. **M5b (same day): player movement re-founded on the Unity Character Controller package** (`com.unity.charactercontroller` 1.4.2) — kinematic collide-and-slide, owner-predicted, data-driven speed; replaces the dynamic-Rigidbody mover (keeps the DR-006 predicted-physics infra). Runtime-validated (collide-and-slide, planar, server==client, CharacterInterpolation predicted-only); EditMode 47/47 — [[DR-007_M5b_Character_Controller_Package]], [[2026-06-01_M5b_Character_Controller]]. |
| **M6 — Build/placement** | Server-authoritative grid build placement via RPC | ⬜ |
| **M7 — Automation** | Self-running tick-based production chains (deterministic offline catch-up) | ⬜ |
@@ -0,0 +1,44 @@
---
date: 2026-06-01
type: session
tags: [session, dots, netcode, physics, prediction, m5]
permalink: gamevault/07-sessions/2026/2026-06-01-m5-physics-in-prediction
---
# Session 2026-06-01 — M5 physics-in-prediction (velocity-driven character)
## Goal
Start **M5 — Home base + physics** ([[Milestones]]). Operator scoped this pass to **physics-in-prediction first**: bring Unity Physics (1.4.6) into Netcode's predicted loop as a deterministic, rollback-safe foundation before base-streaming. Deliverable: the player becomes a dynamic physics body driven by input velocity, colliding with static world geometry + other players, replicating server→client with no desync. Projectiles stay kinematic. Architecture locked in [[DR-006_M5_Physics_In_Prediction]].
## Done
- **Velocity-driven movement.** `PlayerMoveSystem` rewritten from teleporting `LocalTransform.Position` → writing `PhysicsVelocity.Linear = dir * EffectiveCharacterStats.MoveSpeed` (Y zeroed, angular zeroed); Unity Physics integrates + resolves contacts. Kept in `PredictedSimulationSystemGroup` (the predicted physics group is OrderFirst, so velocity applies on the next step — a 1-tick offset that is consistent across server/client/rollback). `Unity.Transforms`/`Unity.Physics.Systems` usings dropped.
- **Player physics body (built-in authoring).** `Player.prefab` got a `CapsuleCollider` (r 0.5, h 2) + `Rigidbody` (`useGravity=false`, dynamic, `FreezeRotation`, `interpolation=Interpolate`) → bakes `PhysicsCollider`/`PhysicsVelocity`/`PhysicsMass`/`PhysicsGravityFactor`(0)/`PhysicsGraphicalSmoothing`. Unity Physics 1.x bakes built-in colliders + `Rigidbody`; the old `PhysicsShape/BodyAuthoring` are gone.
- **Planar pin.** New `PlayerPlanarConstraintSystem` (after the fixed-step physics group) clamps player Y to `PlayerSpawner.SpawnPoint.y` + zeroes vertical velocity — stops gravity-free capsules from riding up box edges and floating away (the actual cause of the first "tunnelling" symptom; see below).
- **Subscene content.** `Gameplay.unity` gained a `NetCodePhysicsConfig` singleton (`PhysicGroupRunMode = LagCompensationEnabledOrAnyPhysicsEntities`, `EnableLagCompensation = false`) and 3 static box-collider obstacles (`Pillar_Center`, `Wall_East`, `Wall_North`, red). Re-baked.
- **Tests.** `ProjectM.Tests.EditMode` asmdef gained `Unity.Physics`; `PlayerMoveSystemTests` rewritten from teleport-position asserts → velocity-mapping asserts (cardinal, diagonal unit-clamp, sub-unit proportional, determinism + angular-zero).
## Validation
- **EditMode 51/51 green** (47 prior suites + 4 rewritten PlayerMove tests); M1M4 suites unaffected.
- **Runtime (single in-editor client, 6.4.7):** player baked with all physics components on **server + client**; CollisionWorld holds 4 collider bodies (player + 3 static), raycast confirms solid walls. Driving +X into `Wall_East` (face x≈5.75) → capsule **stops at x=5.25 = face radius** on both worlds, **holds under continuous push** (no tunnel, no creep), **Y pinned at 1.00** (no climb-over), and **releases to free movement** on reverse. **Server == client exactly** (prediction in sync). Console clean of all five M2/M4 cascade signatures + Burst "not a known entry point"; only the known unfocused-editor `Server Tick Batching` artifact remains.
## Diagnosis notes (for future me)
- **First symptom looked like tunnelling; it was climb-over.** Colliders were solid (raycast hit), but with gravity off the capsule took a vertical contact impulse, rose above the short (y[0,1]) walls, and floated away at constant velocity. Fix = planar pin + taller walls. Not a collision-detection bug.
- **A whole detour was caused by `Write`-tool edits to `.cs` not triggering a Unity recompile** on this unfocused editor — tests/`execute_code` ran a **stale assembly** (velocity showed `MoveSpeed*dt`, angular not zeroed: code that existed in neither old nor new source). Switching to MCP `apply_text_edits` (Unity's own scripting pipeline) fixed it immediately. **Always edit Assets `.cs` via `apply_text_edits`/`create_script`, never the raw `Write` tool.**
## Decisions
- [[DR-006_M5_Physics_In_Prediction]] — implicit predicted physics (no toggle); built-in collider+Rigidbody authoring; velocity-driven move; `PhysicsVelocity` auto-replication; planar pin; baked static world. Physics-first ordering (1-tick offset); `FreezeRotation` not baked → angular-zero held in code.
## Open / deferred
- **Base subscene streaming** — the other half of M5 (persistent home base, stream in/out around players). Not started this pass.
- **Lag compensation** — `EnableLagCompensation=false`; revisit if/when physics-based hit detection replaces the swept-segment server check.
- **Same-tick movement** — currently a 1-tick velocity offset (physics group is OrderFirst). Move `PlayerMoveSystem` into the fixed-step group `[UpdateBefore(PhysicsSystemGroup)]` if responsiveness needs it (costs 2 cosmetic warnings).
- **Projectiles as physics bodies** — still kinematic + swept-hit; convert if projectile-vs-world collision is wanted.
- **Rotation lock** — relying on per-tick angular-zero + aim write; set `PhysicsMass.InverseInertia=0` if a hard lock is needed.
- **Player-vs-player collision** is server-authoritative (both dynamic on server); on clients others are interpolated, so peer collisions are approximate and server-corrected — fine for 24.
## Next
Either (a) the **base subscene streaming** half of M5, or (b) layer **Unity Relay** onto M4's `ConnectionConfig` to make co-op remote-playable. Recommend (a) to complete M5.
@@ -0,0 +1,46 @@
---
date: 2026-06-01
type: session
tags: [session, dots, netcode, physics, character-controller, prediction, m5]
permalink: gamevault/07-sessions/2026/2026-06-01-m5b-character-controller
---
# Session 2026-06-01 — M5b: adopt the Unity Character Controller package
## Goal
Operator decision: replace the M5 dynamic-Rigidbody player ([[DR-006_M5_Physics_In_Prediction]]) with Unity's **Character Controller package** (kinematic, collide-and-slide, DOTS, netcode-predicted) as the player's movement foundation. Run via `/dots-dev` with a multi-agent research workflow. Architecture locked in [[DR-007_M5b_Character_Controller_Package]].
## Process
- **Read-only research workflow** (6 parallel Explore agents → synthesis): compatibility (two angles), CC netcode setup, CC core API, codebase impact. Returned a unified plan + a hard compatibility verdict.
- **Compatibility gate (the make-or-break):** research said CC 1.4.2 declares `entities/physics@1.3.15` vs our Entities 6.4.0 (Unity-6 renumber) — *likely incompatible*. Resolved by an **empirical install probe** (operator-gated): added the package, confirmed the lock kept Entities 6.4.0 / Physics 1.4.6 / Netcode 1.13.2 (no downgrade) and compiled clean. The floors were satisfied → **it works**. Probe → pause → operator "go".
- **Port:** a sub-agent fetched the OnlineFPS netcode-character sample verbatim; I reconciled its API against the **installed** 1.4.2 via `unity_reflect` (the canonical path is `IKinematicCharacterProcessor<T>` + `KinematicCharacterDataAccess` + static `KinematicCharacterUtilities.Update_*`; the legacy `KinematicCharacterAspect` also exists but isn't what the samples use). Authored a minimal top-down adaptation.
## Done
- **New (`ProjectM.Simulation`):** `CharacterComponent` + `CharacterControl` (CharacterComponents.cs); `CharacterControlMath.DesiredMovement` (unit-test seam); `CharacterProcessor` (the IKinematicCharacterProcessor running the Update_* sequence, gravity-free, velocity-controlled); `CharacterPhysicsUpdateSystem` (`KinematicCharacterPhysicsUpdateGroup`); `PlayerControlSystem` (PlayerInput × MoveSpeed → CharacterControl); `CharacterGhostVariants` (CharacterInterpolation → PredictedClient-only).
- **New (`ProjectM.Authoring`):** `PlayerCharacterAuthoring` — baker calls `KinematicCharacterUtilities.BakeCharacter` (top-down props) + adds CharacterComponent/CharacterControl.
- **Modified:** `ProjectM.Simulation.asmdef` + `ProjectM.Authoring.asmdef` (+`Unity.CharacterController`, Authoring +`Unity.Physics`); `Player.prefab` (removed M5 Rigidbody, kept CapsuleCollider, added PlayerCharacterAuthoring, scale 1); `StatRecomputeSystem` (dropped the now-dangling `[UpdateBefore(PlayerMoveSystem)]`).
- **Deleted:** `PlayerMoveSystem`, `PlayerPlanarConstraintSystem` (CC + no-gravity stays planar — no constraint needed); `PlayerMoveSystemTests``CharacterControlMathTests`.
- **Kept:** PlayerInput + input gather + DebugInputInjectionSystem, EffectiveCharacterStats/StatRecompute, GhostOwner/spawn/co-op ring, PlayerAimSystem, combat/health, the `NetCodePhysicsConfig` + baked static walls from DR-006.
## Validation
- **EditMode 47/47 green** (4 new CharacterControlMath tests replace the 4 old PhysicsVelocity-mapping tests).
- **Runtime (single in-editor client, 6.4.7):** player bakes the full CC set (`KinematicCharacterBody/Properties`, `PhysicsCollider`, `CharacterControl`, `CharacterComponent`, `CharacterInterpolation`, the 4 CC buffers) on **both** worlds; spawns at the ring slot (2.5,1,0). Driving +X stops the capsule at **x=5.24** (= `Wall_East` face radius) on **server and client**; diagonal input **slides along** the wall and rounds its finite end (collide-and-slide, no tunnel); **Y holds 1.00 with no planar-pin system**; **server == client**. `CharacterInterpolation` confirmed on the client ghost, **absent on the server** (predicted-only variant works). Console clean of CC/Burst/cascade signatures — only the known unfocused-editor tick-batching artifact.
## Decisions
- [[DR-007_M5b_Character_Controller_Package]] — CC 1.4.2 resolves on our stack (SemVer floors, no downgrade); kinematic collide-and-slide owner-predicted character via the static-utilities pattern; data-driven velocity from existing PlayerInput/stats; CharacterInterpolation predicted-only; do NOT globally DontSerialize LocalTransform (protects non-character ghosts); supersedes the DR-006 dynamic-Rigidbody mover, keeps the DR-006 predicted-physics infra.
## Diagnosis notes (for future me)
- **The compatibility "no" was a false alarm** — a package declaring an older `entities@1.3.x` dependency still resolves against the renumbered Entities 6.4.0 (SemVer floor); always **probe** rather than trust the version-string mismatch. Verify the lock didn't downgrade + the package compiles.
- **Trust `unity_reflect` over sub-agent package-cache reads** for the installed API shape: reflect showed both the aspect (legacy) and the static-utilities path; the samples use the latter, which compiled first-try.
- Continued to edit Assets `.cs` exclusively via MCP `create_script`/`apply_text_edits` (see [[2026-06-01_M5_Physics_In_Prediction]] / the Write-tool stale-assembly trap).
## Open / deferred
- **Multi-client co-op interpolation** — validate remote-peer smoothness with a live two-build / thin-client run (predicted-only `CharacterInterpolation` variant is in place; single-client validated).
- **Player-vs-player non-physical** (`SimulateDynamicBody=false`); **gravity-free/no floor** (planar); **CC declares older deps** (revisit on CC bump); optional **KinematicCharacterBody velocity ghost variant** for tighter reconciliation under latency.
## Next
Recommend a **real two-build LAN co-op smoke test** (also closes the M4 deferral) to validate remote CC interpolation, then resume the **base subscene streaming** half of M5.
@@ -0,0 +1,37 @@
---
id: DR-006
title: M5 physics-in-prediction — velocity-driven character on Unity Physics in the predicted loop; planar pin
status: accepted
date: 2026-06-01
tags:
- decision
- netcode
- physics
- prediction
- m5
permalink: gamevault/07-sessions/decisions/dr-006-m5-physics-in-prediction
---
# DR-006 — M5 Physics-in-Prediction (velocity-driven character; planar pin)
## Context
M5 ([[Milestones]]) = "home base + physics". The operator scoped this first pass to **physics-in-prediction**: stand up Unity Physics (1.4.6) *inside* Netcode's predicted simulation loop before any base-streaming work, because deterministic, rollback-safe physics is the highest-risk netcode surface in the roadmap. Through M4 the player moved by **teleporting `LocalTransform.Position`** (`PlayerMoveSystem`, kinematic) — no collision, no bodies. `Unity.Physics` was already referenced by `ProjectM.Simulation`, and the netcode-physics integration assemblies (`Unity.NetCode.Physics`, `…Physics.Hybrid`) ship with our stack. Validated against Unity package docs + `unity_reflect` against the installed packages (context7 MCP was unavailable this session — fell back to official docs + reflection). Extends [[DR-005_M4_Connection_Model_Direct_IP]].
## Decision
1. **Predicted physics is implicit — no toggle.** With the netcode-physics package present and predicted ghosts carrying physics components, Netcode relocates `PhysicsSystemGroup` into the `PredictedFixedStepSimulationSystemGroup` (a child of `PredictedSimulationSystemGroup`, marked **OrderFirst**). `NetCodePhysicsConfig` has **no `PredictedPhysics` field** — it only tunes lag-comp / run-mode / history. A single `NetCodePhysicsConfig` lives in the `Gameplay` subscene with `PhysicGroupRunMode = LagCompensationEnabledOrAnyPhysicsEntities` (so the predicted physics group runs whenever physics entities exist) and `EnableLagCompensation = false` (our damage is swept-segment + server-side; lag comp deferred).
2. **The player is a velocity-driven dynamic body, authored with built-in components.** Unity Physics **1.x bakes built-in `UnityEngine` colliders + `Rigidbody`** — the old `PhysicsBodyAuthoring`/`PhysicsShapeAuthoring` (Physics 0.x) are gone. `Player.prefab` gets a `CapsuleCollider` (r 0.5, h 2) + a `Rigidbody` (`useGravity=false` → planar, `isKinematic=false`, `FreezeRotation`, `interpolation=Interpolate`). These bake to `PhysicsCollider` / `PhysicsVelocity` / `PhysicsMass` / `PhysicsGravityFactor`(0) / `PhysicsGraphicalSmoothing`. `PlayerMoveSystem` now writes `PhysicsVelocity.Linear = dir * MoveSpeed` (Y zeroed, angular zeroed) instead of teleporting; the solver integrates + resolves contacts.
3. **`PhysicsVelocity` auto-replicates.** Netcode ships `PhysicsVelocityDefaultVariant` + a generated serializer, so no hand-written `[GhostField]` is needed; `LocalTransform` is already replicated on the player ghost. Owner-predicted player (`DefaultGhostMode=OwnerPredicted`) → owner predicts physics, other clients interpolate (Simulate disabled), server is authoritative.
4. **Players are pinned to the movement plane after the step.** `PlayerPlanarConstraintSystem` (`PredictedSimulationSystemGroup`, `[UpdateAfter(PredictedFixedStepSimulationSystemGroup)]`) clamps each predicted player's Y to `PlayerSpawner.SpawnPoint.y` (single source) and zeroes vertical velocity. Required because with gravity off, any vertical contact impulse (a capsule riding a box edge) is **permanent** — without the pin the character climbs over obstacles and floats away.
5. **Static world geometry is baked, not ghosted.** Three static box colliders (`Pillar_Center`, `Wall_East`, `Wall_North`, red `M_Dummy`) added to the `Gameplay` subscene — present identically in server + client worlds (deterministic, no replication). Projectiles stay kinematic this pass.
## Consequences
- **Validated end to end on 6.4.7.** EditMode 51/51 (4 new `PlayerMoveSystemTests`: cardinal map, diagonal unit-clamp, sub-unit proportional, determinism+angular-zero). Runtime: player baked with all physics components on **both** worlds; 4 `PhysicsCollider` bodies; driving +X into `Wall_East` (face x≈5.75) stops the capsule at **x=5.25** (= face radius) on server **and** client, holds there under continuous push (no tunnel, no creep), Y pinned at 1.00 (no climb-over), and releases to free movement on reverse. **Server == client exactly** — prediction in sync, console clean of all five M2/M4 cascade signatures and Burst errors.
- **Ordering: physics runs *before* `PlayerMoveSystem` (1-tick velocity offset).** The fixed-step group is **OrderFirst**, so `[UpdateBefore]` against it is ignored (the attribute was removed to keep the console warning-free). The offset is identical on server + owning client + rollback, so prediction stays consistent and the lag is absorbed by prediction. If same-tick responsiveness is ever needed, move `PlayerMoveSystem` *into* `PredictedFixedStepSimulationSystemGroup` `[UpdateBefore(PhysicsSystemGroup)]` (verified to sort correctly) — at the cost of two cosmetic "invalid UpdateBefore" warnings from the netcode physics relocation.
- **`Rigidbody.FreezeRotation` is NOT honored by the DOTS baker** — baked `PhysicsMass.InverseInertia` stayed non-zero. Rotation is instead held by zeroing angular velocity each tick (`PlayerMoveSystem`) + `PlayerAimSystem` writing the facing directly. If precise rotation lock is needed later, set `PhysicsMass.InverseInertia = 0` in a baker/system.
- **The known unfocused-editor `Server Tick Batching` artifact persists** (1.251.75 ticks/frame) — clears when the Game view is focused / in a build (see [[2026-06-01_M4_LAN_CoOp_And_Classification_Fix]]). It does not desync the physics in-session.
- **Determinism caveat (deferred):** Unity Physics is same-binary deterministic, which is what client re-simulation needs; cross-platform float determinism is not guaranteed but the server is authoritative and corrects via snapshots + `PhysicsGraphicalSmoothing`. No lag compensation yet (`EnableLagCompensation=false`).
Mirrors the server-authoritative + client-prediction pillars from [[Pillars]]; unblocks the base-streaming half of M5.
@@ -0,0 +1,45 @@
---
id: DR-007
title: M5b — adopt Unity Character Controller package for the predicted player (replaces the dynamic-Rigidbody mover)
status: accepted
date: 2026-06-01
tags:
- decision
- netcode
- physics
- character-controller
- prediction
- m5
permalink: gamevault/07-sessions/decisions/dr-007-m5b-character-controller-package
---
# DR-007 — M5b Unity Character Controller (kinematic, owner-predicted)
## Context
The M5 player ([[DR-006_M5_Physics_In_Prediction]]) was a **dynamic Unity Physics Rigidbody** driven by setting `PhysicsVelocity` each tick. It worked, but only with workarounds the kinematic approach avoids by design: a planar-pin system (gravity-off bodies accumulate vertical contact impulses and climb over walls), per-tick angular-zeroing, and `Rigidbody.FreezeRotation` (which the DOTS baker ignores). The operator chose to adopt Unity's **Character Controller package** (`com.unity.charactercontroller`) — a DOTS, collide-and-slide kinematic controller with first-class Netcode-for-Entities prediction — as the player's movement foundation. Researched via a read-only workflow + verified against the installed API with `unity_reflect`. Supersedes the **movement** half of DR-006; the predicted-physics infrastructure DR-006 stood up (predicted physics group, `NetCodePhysicsConfig`, baked static walls) is **kept** and is exactly what the CC character sweeps against.
## Decision
1. **CC version: `com.unity.charactercontroller` 1.4.2 — verified to resolve on our stack.** It declares `com.unity.entities@1.3.15` / `com.unity.physics@1.3.15`, but Unity treats these as SemVer **floors**: the resolver kept our **Entities 6.4.0 / Physics 1.4.6 / Netcode 1.13.2 / Collections 6.4.0** with **no downgrade**, and CC 1.4.2 compiled clean against the renumbered Entities 6.x. (Empirical install probe was the gate; the docs' "installation will fail" prediction was wrong.)
2. **Kinematic collide-and-slide, owner-predicted.** The player is a `KinematicCharacterBody` (no Rigidbody). `BakeCharacter` adds the CC component/buffer set + bakes the prefab's `CapsuleCollider` into `PhysicsCollider`. Top-down config: `SnapToGround=false`, gravity handled in the processor (fed `float3.zero`), `InterpolateRotation=false` (rotation stays owned by `PlayerAimSystem`), `SimulateDynamicBody=false`.
3. **CC API pattern (1.4.2): `IKinematicCharacterProcessor<T>` + `KinematicCharacterDataAccess` + static `KinematicCharacterUtilities.Update_*`.** The legacy `KinematicCharacterAspect` (IAspect, instance `Update_*`) still exists but the static-utilities path is what the 1.4.x samples use — confirmed via reflect. The processor runs the canonical Update sequence (Initialize→ParentMovement→Grounding→[velocity control]→PreventGrounding→GroundPushing→MovementAndDecollisions→…) in `CharacterPhysicsUpdateSystem` (`[UpdateInGroup(KinematicCharacterPhysicsUpdateGroup)]`, which netcode relocates into the predicted fixed-step loop).
4. **Data-driven + reuse the existing input.** `PlayerControlSystem` (`PredictedSimulationSystemGroup`, `[UpdateAfter(StatRecomputeSystem)]`) maps the replicated `PlayerInput.Move` × `EffectiveCharacterStats.MoveSpeed` → a non-replicated `CharacterControl.MoveVelocity` (clamp via the unit-tested `CharacterControlMath.DesiredMovement`); the processor lerps `RelativeVelocity` toward it. No second input component; spawn / GhostOwner / co-op ring unchanged.
5. **Ghost variants: only `CharacterInterpolation` → PredictedClient-only.** `BakeCharacter` adds `CharacterInterpolation` to every prefab version; a `DefaultVariantSystemBase` + `[GhostComponentVariation(typeof(CharacterInterpolation))] [GhostComponent(PrefabType = GhostPrefabType.PredictedClient)]` strips it from server + interpolated-client prefabs (presentation-only; double-interp on remotes otherwise). **We deliberately do NOT register the CC sample's global `LocalTransform → DontSerializeVariant`** — that is project-wide and would break our non-character ghosts (projectiles/dummies/pickups) that rely on stock `LocalTransform` replication. The character replicates position via the normal owner-predicted `LocalTransform` path.
## Consequences
- **Validated on 6.4.7.** EditMode 47/47 (the 4 M5 PhysicsVelocity-mapping tests replaced by 4 `CharacterControlMath` tests). Runtime (single in-editor client): player bakes the full CC set on **both** worlds; driving into `Wall_East` (face x≈5.75) stops the capsule at **x=5.24** = face radius on server **and** client; diagonal input **slides along** the wall and rounds its finite end (true collide-and-slide, not tunnelling); **Y holds at 1.00 with no planar-pin system** (kinematic + no gravity stays on plane); **server == client**; `CharacterInterpolation` confirmed present on the client ghost and **absent on the server** (variant works). Console clean of CC/Burst/cascade errors — only the known unfocused-editor tick-batching artifact.
- **Net code simplification.** Deleted `PlayerMoveSystem` + `PlayerPlanarConstraintSystem` (and removed `StatRecomputeSystem`'s `[UpdateBefore(PlayerMoveSystem)]`). The climb-over / rotation-lock / planar-pin workarounds from DR-006 are gone — the kinematic controller handles it natively.
- **Asmdefs:** `ProjectM.Simulation` and `ProjectM.Authoring` now reference `Unity.CharacterController` (Authoring also `Unity.Physics` for `BakeCharacter`).
- **Stack:** new dependency `com.unity.charactercontroller@1.4.2` in `manifest.json` / lock; core DOTS versions unchanged.
## Open / deferred
- **Multi-client co-op interpolation not headlessly validated.** Single owner-predicted client is validated; remote interpolation smoothness (and the predicted-only `CharacterInterpolation` variant's effect on a *real* second client) needs a live two-build / thin-client check. The variant is in place per Unity's documented setup; CC 1.4.2 also guards `CharacterInterpolationSystem` to enabled-`Simulate` entities only.
- **Player-vs-player is non-physical** (`SimulateDynamicBody=false`): kinematic characters don't shove each other. Revisit if mutual push is wanted (set true + handle masses in `OverrideDynamicHitMasses`).
- **Gravity-free / no floor collider** — planar top-down; if verticality/terrain is added, give the character gravity in the processor + a ground collider and reconsider `SnapToGround`.
- **CC 1.4.2 declares older package deps** (entities/physics 1.3.15). Re-check on any CC bump; a CC build explicitly targeting Entities 6.x ("1.5+") would be preferable when available.
- **`KinematicCharacterBody` velocity ghost variant** (replicate `RelativeVelocity`/`IsGrounded` for tighter reconciliation) was left at defaults; add if owner-prediction reconciliation looks jittery under latency.
Mirrors the server-authoritative + client-prediction + small-co-op pillars from [[Pillars]]. Builds on [[DR-006_M5_Physics_In_Prediction]] (kept infra) and [[DR-005_M4_Connection_Model_Direct_IP]] (spawn/co-op).