Skip to content

[FEATURE] SDK remediation Phase 3e: runtime — extensible sim loop, incremental spatial index, unified physics, animation depth #1561

Description

@Noisemaker111

Tracked slice of docs/plans/sdk-remediation.md Phase 3e. Filed per the plan's PR-sequence item 6. The plan flags this one as "expensive, calcifies if deferred" — the longer it waits, the more code copies the patterns.

1. Games cannot extend the sim loop

A genuinely good scheduler exists (game/systemSchedule.ts — topological before/after, order independent of imports), but stepPlayerMovement and advanceBehaviors are hardwired around onTick (runtime/headlessRunner.ts:135-145), outside the stage system and unreorderable.

  • The fixed accumulator (game/systemRuntime.ts:96-104) has no substep cap — spiral of death — while PhysicsWorld right beside it has maxSubsteps.
  • No interpolation exists anywhere, so a 30Hz system renders as stutter with no fix short of a core change.

2. The entity store defeats its own spatial index

Map<string, SceneEntity>, closed struct (role: "player"|"npc"|"prop"), no components, extension only via meta: unknown.

  • Every write fans out synchronously to every subscriber and bumps spatialGeneration, rebuilding the entire grid (scene/spatial.ts:114-149). Move-entity-then-query-neighbours is O(N²) per frame.
  • setPose allocates a fresh 3-tuple per call.
  • Fix: dirty sets + incremental grid update. This one violates CLAUDE.md's scale-by-default invariant directly.

3. Physics has no rotation, and the good solver is bypassed

  • Zero hits for angular|quat|torque|inertia in physicsWorld.ts. AABB forever — no tumbling vehicle, ragdoll, or toppling crate.
  • The solver itself is excellent (SoA Float32Array, grid broadphase, sequential impulses, sleeping, joints) and is almost entirely bypassed: 7 separate collision implementations, 5 independent spatial hash grids, 2 gravity integrators (movement/movementModel.ts:278 vs physicsWorld.ts:731), and the same ray-sphere quadratic written twice (scene/sceneRaycast.ts:104, multiplayer/lagCompensation.ts:127).
  • ballisticSweep.ts:33 and forceVolume.ts:180 do full O(n) scans past the grid sitting next to them.
  • PhysicsWorld has no snapshot/hydrate — physics sits outside both save and replication.

4. Replication serializes the whole world every commit

SnapshotModule.version() is module-granularity, so entities are dirty the instant anything moves, and area-of-interest filtering runs after the full snapshot is materialized. No dirty bits, deltas, or quantization.

5. Flat React scene tree

Every entity is its own element with its own useFrame and GLB clone; AuthoredScene.tsx:297-315 mounts the entire document at once; culling keeps components mounted and ticking. Editor-placed props are never instanced — 10k props = 10k draw calls. Instancing exists, but only for procedural scatter/city.

6. Animation is a 3-state FSM

idle|walk|run on scalar speed. No IK, blend trees, morphs, root motion, or animation notifies (so no hit frames or foot plants). Skeletal animation exists only on the per-entity path; the instanced path is un-skinned boxes — no route to hundreds of animated humanoids. A third disjoint system (PartMotion.tsx) cannot blend with the mixer path at all.

7. Feel constants are module globals

MOVEMENT_TUNING (accel 26, friction 18, gravity 24) is shared by every game — a heavy mech and a sprinter move identically. Only backpedalSpeedMultiplier is overridable. Same class as the Phase 2.3 module-global cleanup.

8. Audio

  • Distance attenuation only, no PannerNode/HRTF — and setListenerPose(position: Vec3) carries no orientation, so L/R cannot be derived. Widen that signature first; nothing positional works until it is.
  • No ducking/sidechain. crossfadeTo drives all other layers to 0, foreclosing vertical music stems. No streaming (everything is a decoded AudioBuffer).
  • Zero audio assets in the repo and no audio catalog in packages/assets. 2 of 11 games have any audio; none use the positional path.

9. ~10% of core is unreachable

2 fully dead modules and ~47 test-only dead (written, tested, barrel-exported, imported by nothing). world/lod.ts is the archetype — a correct distance-banded LOD scheduler with 8 tests, exported, wired into zero systems. Same for visibility/simulationCulling.ts, ai/interestScheduler.ts, and the whole ai/{flock,crowd,populationDirector,laneSelect} cluster.

Caveat carried from the audit: that reachability was measured with a static in-repo graph, so some modules may be reachable by external deep import. Verify before deleting anything.

Suggested split

Items 1, 2, and 8's signature change are the ones that calcify — schedule them first. Items 3–6 are large enough to be their own PRs. Item 9 is a cleanup pass, not a blocker, and should follow the Phase 1.1 export curation (#1547) so it prunes against the curated surface rather than the wildcard one.

Related: #1139, #1146.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions