Docs: DR-022 animation pipeline + /dots-dev commit phase

DR-022 + session log for the Rukhanka/Synty player-animation slice; CLAUDE.md stack row + Animation (Rukhanka) build gotchas + client asmdef refs; add Phase 10 (operator-approved commit) to the dots-dev skill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 18:18:14 -07:00
parent 951b7ec273
commit 1e23246568
4 changed files with 126 additions and 3 deletions
@@ -0,0 +1,48 @@
---
date: 2026-06-06
type: session
tags: [session, animation, rukhanka, synty, netcode, presentation, dots, slice]
---
# Session 2026-06-06 — Animation pipeline (Rukhanka + Synty), Slice 1: the player is alive
## Goal
Operator (via `/dots-dev`, ultracode): *"Explore animations for this DOTS project — Rukhanka 2 (netcode-aware), alternatives, how much I can wire via MCP. Build something scalable/stable/sustainable for a solo dev. The game feels dead — the player is a capsule with no movement. Make it feel alive."*
## Process
- **Research (adversarial Workflow):** verify exact Rukhanka 2.9 / Entities 6.4 APIs against the installed source + docs + context7 → synthesize a code-ready spec → 2 adversarial critics → finalize. Landscape: **Rukhanka is the only maintained Entities-native option on the 6.4 stack** (Latios/Kinemation explicitly not 6.4-compatible; Unity official = vaporware; DMotion dead). 3 parallel research agents up front (Rukhanka capability+netcode, alternatives, Synty rig/anim-pack compat).
- **Clarifying gate (AskUserQuestion):** clip source = **Synty anim pack** (already imported: `AnimationBaseLocomotion`), first slice = **player only**, netcode = **advise-me** → chose **client-derived**, look = **SciFiSpace soldier**.
- **Plan → approval → execution.** A permissions misfire (my subagent guardrail text "don't enter Play / use UnityMCP" was read by the auto-approver as the *user's* rule) blocked Play; operator reconnected MCP and cleared it.
- **Execution serialized through the one live editor** (research fanned out in parallel; all Unity mutations done by me, validating each).
## Done
- **De-risk (task 1):** entered Play → Rukhanka 2.9 initializes clean on Entities 6.4 (zero Rukhanka errors). GO.
- **FBX (task 2):** SciFiSpace `Characters.fbx` already Generic + Optimize-GO-OFF + Mikk tangents → verify-only.
- **Controller (task 3):** authored `AC_PlayerTopDown.controller` via the AnimatorController API (robust vs the enum/Vector-drop MCP traps) — 4 params, Idle / 2D-Freeform-Directional strafe Locomotion (9 in-place Synty run clips) / Death; 0 missing clips.
- **Drive system (task 5, done before assets to compile-validate the Rukhanka API):** `AnimParamMath` (pure, **10 EditMode tests**) + client-only `PlayerAnimationDriveSystem` (Local CC-velocity + Remote position-delta jobs; `[WithAll(GhostOwnerIsLocal)]` / `[WithDisabled]` / `[WithPresent(Dead)]`). Asmdef refs added; **also `Unity.Physics`** (source-gen needs it directly — `KinematicCharacterBody` nests `ColliderKey`; the verified spec missed it). EditMode **204/204**.
- **Material + prefab (task 4):** imported Rukhanka samples for `AnimatedLitShader` (multi-target ShaderGraph w/ a UniversalTarget → URP-valid), built `M_SpaceSoldier_Animated` (Synty atlas → `_BaseColorMap`). Assembled `Player.prefab`: flattened the soldier skeleton + body/head SMRs onto the **player root** (so the rig co-locates with gameplay components — required for the drive query), `Animator`+`RigDefinitionAuthoring`(CPU, root-motion off) on the root, capsule visual removed / collider kept. **Recovered a broken first attempt** (used `rootBone`=Spine_03 → destroyed the lower skeleton) via `git checkout` + correct walk-up-to-skeleton-root detection.
- **Samples cleanup:** moved the 3 deformation ShaderGraphs to `_Project/Shaders/RukhankaSampleShaders` (GUID-preserving) and **deleted the whole samples tree** — it had dragged in 26 sample subscenes (one NRE'd Rukhanka's unguarded clip baker), sample systems that run in our worlds, and a conflicting TextMesh Pro folder.
- **Ground offset:** skeleton `Root` local Y = 0.9 (entity origin = capsule center ~1 m up). Clips key no Root/Hips position → Rukhanka bakes it as a constant → persists. Feet 0.90 → ≈0.00.
## Validation (runtime, focused editor, real Server+Client)
- ClientWorld player entity: `PlayerTag` + rig + 4 params + facing/stats/CC/`Dead` + **`GhostOwnerIsLocal` enabled** → drive job matches. **Idle and a forced run pose both deform the mesh** (distinct silhouettes, screenshots); **feet at y≈0**; capsule replaced by the textured soldier. Console clean (no NRE, no skinning warning).
- ServerWorld: our drive system correctly **absent**.
- **EditMode 204/204** (+10 `AnimParamMath`).
## Decisions
[[DR-022_Animation_Pipeline_Rukhanka_Synty]] — Rukhanka client-derived; Generic-rig + Synty clips; CPU engine + deformation material; slim top-down controller; **rig-on-root** for entity co-location; client-only two-path drive (local CC-velocity / remote position-delta).
**Runtime finding the adversarial spec got wrong:** `RukhankaAnimationSystemGroup` **is** created in `ServerWorld` (the spec read a `WorldFlags` gate and concluded otherwise). Harmless (drive is client-only; bones unreplicated; animation never feeds the sim) but wastes server CPU — only Play-validation caught it. Logged as a follow-up (server-strip).
## Next-session intent
- ~~Server-strip Rukhanka~~ **DONE (same session):** `ServerStripAnimationSystem` (server-only one-shot) disables all `Rukhanka.Runtime` systems on the server (validated: server enabled=0 / 4 disabled, client 8 running, console clean). Also added a **Phase 10 (Commit)** to the `/dots-dev` skill — offer + commit only on explicit operator approval, logical groups, proper messages, no push.
- Template the pipeline to **enemies** (Husk/Brute/Swarmer) — same flow, swap mesh + reuse `AC_PlayerTopDown` (or per-enemy clips).
- Source a **combat/hit-react/death** anim pack (Base Locomotion is locomotion-only; Death is a crouch placeholder; Fire has no clip yet).
- **Aim-IK** upper body toward the cursor (twin-stick), then ragdoll-on-death, bone-socket weapons, GPU engine + VAT for crowds.
- Operator live play-through to tune anim-speed scaling / blend thresholds / the 0.9 offset, and a 2-client MPPM check that remote players animate (the position-delta path).
@@ -0,0 +1,52 @@
---
id: DR-022
title: Player skeletal animation via Rukhanka (client-derived) on a Synty SciFiSpace soldier — Slice 1
status: accepted
date: 2026-06-06
tags:
- decision
- animation
- rukhanka
- synty
- netcode
- presentation
- dots
permalink: gamevault/07-sessions/decisions/dr-022-animation-pipeline-rukhanka-synty
---
# DR-022 — Skeletal animation pipeline (Rukhanka + Synty), client-derived — Slice 1 (player)
## Context
The player was a built-in **capsule** (`Player.prefab` = primitive mesh + CC + ghost); enemies/structures too. Operator: *"explore animations … build something scalable, stable, sustainable for a single developer … I want the game to feel alive. The current player capsule … feels really dead."* Owns **Rukhanka Animation System 2** (Entities-native, Burst/GPU skinning, netcode-aware) and a large **Synty Polygon** character library; **Synty Animation Base Locomotion** was imported this session (721 clips + `AC_Polygon_*` controllers, the unified Synty skeleton).
An adversarial research **Workflow** (verify exact Rukhanka 2.9 / Entities 6.4 APIs → synthesize → 2 critics → finalize) produced a code-ready spec. Landscape verdict: **Rukhanka is the right (only) primary** on this stack — **Latios/Kinemation explicitly does not support Entities 6.4** today; Unity's official ECS-animation path is vaporware; DMotion is dead. VAT (crowds) + procedural (secondary motion) are complementary, later. Clarifying answers: **Synty anim pack** (already in project), **player-only** first slice, **advise-me** on netcode, **SciFiSpace soldier**.
## Decision
1. **Rukhanka 2.9.0; netcode replication OFF** (`RUKHANKA_WITH_NETCODE` undefined). Animation is **client-derived presentation**, never replicated — matches the project doctrine ("derive instead of replicate"; "all juice = client-only observe", see [[DR-017_Persistent_Base_Player_Driven_Pacing]] / [[DR-021_HUD_UITK_BuildPalette]]). Clients compute locomotion params from **already-replicated state** (velocity/facing/`Dead`). Zero added bandwidth, **no new `[GhostField]`s**. Flip netcode on only if the server must arbitrate frame-exact animation state.
2. **Generic rig + the existing Synty `AC_Polygon` clips drive the soldier by bone-path** (one unified Synty skeleton spans the Polygon packs → no Mecanim Humanoid Avatar UI). The SciFiSpace `Characters.fbx` was already `animationType:3` (Generic) + **Optimize Game Objects OFF** (Rukhanka's hard requirement) + Mikk tangents → verify-only, no reimport.
3. **CPU animation engine + a deformation-aware material.** CPU sampling still skins via Entities-Graphics GPU deformation, which needs a material exposing `_DeformedMeshIndex``AnimatedLitShader` (a **multi-target** ShaderGraph that includes a `UniversalTarget` → renders under URP 17.4; stock URP/Lit would render **unskinned static**, not magenta). Synty atlas → its `_BaseColorMap`.
4. **Slim custom controller `AC_PlayerTopDown`** (not the 700-clip Synty graph): `Idle` / **2D Freeform-Directional** `Locomotion` (params `MoveX`/`MoveZ`, 9 nodes = idle-center + 8 in-place run strafes) / `Death`; params `MoveX,MoveZ,Speed,IsDead`. **Root motion OFF** (the DOTS Character Controller owns the transform; the blend tree is velocity-driven).
5. **Rig on the PLAYER ROOT, not a child.** Rukhanka puts the animator param components (`AnimatorControllerParameterComponent` buffer + index table) on the GameObject holding `RigDefinitionAuthoring`. The client-only drive job needs those **and** the gameplay components (`PlayerFacing`/`EffectiveCharacterStats`/`KinematicCharacterBody`/`Dead`) on **one entity**, so the rig must bake onto the ghost entity itself — `Animator` + `RigDefinitionAuthoring` go on the player root; the soldier skeleton (`Root` + 49 bones) + body/head `SkinnedMeshRenderer`s are flattened as children; the capsule MeshRenderer/Filter are removed, the **CapsuleCollider kept**. (The first assembly attempt used `SkinnedMeshRenderer.rootBone` as the skeleton top — that's the *bounds* root (`Spine_03` for the head) and destroyed the lower skeleton; correct detection = walk up from any bone to the direct child of the soldier instance.)
6. **`PlayerAnimationDriveSystem`** (client `SystemBase`, `[WorldSystemFilter(LocalSimulation|ClientSimulation)]`, `[UpdateBefore(RukhankaAnimationSystemGroup)]` → runs in `SimulationSystemGroup` before Rukhanka's same-frame controller eval — a documented exception to "all juice = PresentationSystemGroup", to avoid a 1-frame lag; still observe-only, never in the predicted loop). Two Bursted `IJobEntity` paths:
- **Local** `[WithAll(GhostOwnerIsLocal)]` (enableable → only the owned player) → `KinematicCharacterBody.RelativeVelocity`.
- **Remote** `[WithDisabled(GhostOwnerIsLocal)]``LocalTransform.Position` frame-delta (KinematicCharacterBody is baked **zero** on remotes — not a `[GhostField]`, owner-only-written), cached per-Entity + pruned each frame.
- Both `[WithPresent(Dead)]` (`Dead` is baked **disabled** → without `WithPresent` an `EnabledRefRO<Dead>` query silently visits zero alive players). Pure mapping is `AnimParamMath.LocomotionParams` (world-vel → facing-frame strafe + normalized speed), **10 EditMode tests**.
7. **Visual ground offset:** the entity origin is the capsule **center** (~1 m up), so feet-at-skeleton-origin float ~0.9 m. Fix = skeleton `Root` local **Y = 0.9**. The in-place clips key **no** `Root`/`Hips` position curves, so Rukhanka bakes the un-keyed `Root` at its authored value → the offset persists through animation. Verified: bodyFeetY went 0.90 → ≈0.00.
## Consequences (validated at runtime, Unity 6.4.7, real Server+Client worlds)
- ClientWorld player entity: `PlayerTag` + rig + **4 params** + facing/stats/CC/dead + **`GhostOwnerIsLocal` enabled** → the drive job matches. Idle pose **and** a forced run pose both deform the mesh (distinct silhouettes); feet on the ground; capsule gone. The soldier replaces the capsule in-game (screenshots `_anim_slice1_check`/`_run`, since deleted). **EditMode 194 → 204** (+10 `AnimParamMath`). Console clean (no NRE, no "does not support skinning").
- **Asmdef:** `ProjectM.Client` += `Rukhanka.Runtime`, `Rukhanka.Toolbox`, `Unity.CharacterController`, **`Unity.Physics`**. The last is a source-gen requirement the verified spec missed: `KinematicCharacterBody` nests `Unity.Physics.ColliderKey`, so the generated `*.g.cs` needs `Unity.Physics` as a **direct** ref (CS8377/CS0012) — same class as the `Unity.Transforms` direct-ref rule.
- **No new asmdef; no netcode surface change** — with the define off, no Rukhanka component is a ghost component (all its `[GhostField]`/variants are `#if RUKHANKA_WITH_NETCODE`), so the ghost hash + snapshot layout are unchanged and **no `DefaultVariantSystemBase` stripping** is needed. New files: `AnimParamMath`, `PlayerAnimationDriveSystem`, `AnimParamMathTests`, `AC_PlayerTopDown.controller`, `M_SpaceSoldier_Animated.mat`, `Shaders/RukhankaSampleShaders/*`, `ServerStripAnimationSystem` (server-only Rukhanka strip); modified `Player.prefab` + `ProjectM.Client.asmdef`.
## Findings / open / deferred
- **Server-side Rukhanka — STRIPPED (shipped this session).** Runtime showed Rukhanka systems on the `ServerWorld` (the adversarial spec concluded otherwise from a `WorldFlags` read — wrong; only Play-validation caught it). Real mechanism: the bootstrap creates `RukhankaAnimationSystemGroup` on the server but leaves it **empty** (it fills the update list only `if (isClient)`); the actual waste is the **deformation systems** — they use `[WorldSystemFilter(Default)]` and `Default` includes `ServerSimulation`, so a headless server runs skinned-mesh prep + mesh deformation on the player's baked bones/meshes nobody renders. **Fix:** `ServerStripAnimationSystem` (server-only one-shot, `[WorldSystemFilter(ServerSimulation)]`) disables every `Rukhanka.Runtime` system in the server world (disabling a group cascades to its managed + unmanaged children); matched by assembly name (no Rukhanka type ref → no asmdef change). **Validated:** server Rukhanka enabled **0** (4 disabled), client unaffected (8 running), console clean.
- **Samples-pollution gotcha:** importing the Rukhanka "Animation Samples" (the only source of `AnimatedLitShader`) drags in **26 sample subscenes** (one NRE'd Rukhanka's **unguarded clip baker**`AnimationClipBaker.ReadCurvesFromTransform` reads a null bone Transform when a clip references a missing bone), sample **systems that run in your worlds**, and a conflicting **TextMesh Pro** folder. Fix applied: move the 3 deformation ShaderGraphs to `Assets/_Project/Shaders/RukhankaSampleShaders` (GUID-preserving → material ref intact), then delete the entire samples tree.
- **Rukhanka's first bake is heavy** (~60 s, synchronous on the main thread → editor telemetry freezes — looks like a hang, isn't) while it builds the animation **blob** for every clip; **cached afterwards** (re-plays are fast). Budget for it on first Play / clip changes.
- **Death = crouch-pose placeholder** (Base Locomotion has no death/hit clips). Needs a combat/hit-react anim pack for real Fire/Death.
- **Roadmap (reuses this exact template):** enemies (Husk/Brute/Swarmer) → **Aim-IK** upper body toward cursor → ragdoll on death → bone-socket weapons → GPU engine + **VAT** for large Husk crowds → procedural secondary motion.
Builds on [[DR-007_M5b_Character_Controller_Package]] (the CC the visual rides), [[DR-012_Aim_Controls_Cursor_Gamepad]] (`PlayerFacing`), [[DR-021_HUD_UITK_BuildPalette]] / [[DR-017_Persistent_Base_Player_Driven_Pacing]] (observe-replicated-state presentation doctrine). Serves the "feel alive" goal.