Skip to content

[FEATURE] Physics backend seam with capsule character controller and a Rapier adapter #1682

Description

@Noisemaker111

Problem: physics was translation-only, box/sphere only, no shapecast, and a concrete class. Done in #1693: packages/core/src/physics/physicsBackend.ts (the PhysicsBackend interface), physicsWorldBackend.ts (PhysicsWorld adapter), physicsBackendConformance.ts (runPhysicsBackendConformance(create, { test, expect })), and packages/core/src/movement/characterController.ts (createCharacterController). Read those four files before starting; the interface is the contract.

Tasks, one PR each, in order:

  1. @jgengine/rapier package. Create packages/rapier/ mirroring packages/sql/ layout (package.json with "./*" wildcard export, tsconfig, tsconfig.build.json, build script tsgo -p tsconfig.build.json && bun ../../scripts/fix-extensions.ts dist). Dependencies: @jgengine/core (workspace) and @dimforge/rapier3d-compat (pin latest 0.x). src/createRapierBackend.ts: export async function createRapierBackend(options: PhysicsBackendConfig & { warn?: boolean }): Promise<PhysicsBackend> that awaits RAPIER.init(), then implements every method: shapes map to ColliderDesc.cuboid/ball/capsule/convexHull/trimesh/heightfield; kind maps to RigidBodyDesc.dynamic/kinematicPositionBased/fixed; layers and mask to collisionGroups; ccd to setCcdEnabled; joints to JointData.fixed/revolute/spherical/rope/spring with limits on revolute; raycast via world.castRay with solid=true and normal from castRayAndGetNormal; shapecast via world.castShape; overlap via intersectionsWithShape; contacts from EventQueue.drainCollisionEvents plus contactPair for the normal; setKinematicTarget via setNextKinematicTranslation; snapshot = world.takeSnapshot() bytes plus the handle map; restore = World.restoreSnapshot. Handles are your own counters mapped to Rapier handles, as the PhysicsWorld adapter does. capabilities: rotation true, all shapes, ccd true, all joints. Test file src/createRapierBackend.test.ts runs runPhysicsBackendConformance and the character controller test scenarios (copy characterController.test.ts with the backend swapped). Register the package: root package.json build chain, scripts/export-manifest.json via bun run gen, README package table and lockstep version line, scripts/tarballInstall.test.ts list, CHANGELOG Added. Update packages/core/src/physics/README.md "Update 2026-09" to say the adapter exists.

  2. Controller adoption in the shell. In packages/core/src/movement/playerMovement.ts add an optional controller?: { backend: PhysicsBackend; capsule: CharacterControllerConfig } on PlayerMovementTuning; when present, stepPlayerMovement computes the horizontal motion from input as today, then calls createCharacterController(...).move(backend, { motion, dt, gravity, jumpVelocity, crouch }) and writes the resulting position through setPose, instead of the terrain-height and AABB path. Store the controller per user in the same per-context map the function already uses for motion state. Expose physics?: { backend: PhysicsBackend } on the game definition so resolvePlayerMovementTuning can pick it up, and register backend.step(dt) as ctx.sim.addStage({ id: "physics", phase: "afterMovement", run: (dt) => backend.step(dt) }) in packages/core/src/game/defineGame.ts when a backend is configured. Headless test: a createHeadlessRunner game with a PhysicsWorld backend, a floor and a step box, walks and steps up. CHANGELOG Added.

  3. Static world into the backend. New packages/core/src/physics/worldColliders.ts: syncWorldColliders(backend, ctx) that creates static bodies for every scene object collider (ctx.scene.object colliders and packages/core/src/scene/colliders.ts), and for terrain a heightfield from ctx.world.ground samples; keeps a map from object id to handle and updates on spawn/despawn via the entity/object subscribe hooks. Test: a spawned object becomes a raycast hit. This makes [FEATURE] World features generate no collision — buildings, city volumes, and walls are solid to nothing #1627 (world features generate no collision) fixable through the backend later.

  4. Probe and clip. In apps/dev or examples/, a scene with the Rapier backend: a crate tumbling down a slope, a ragdoll (use packages/core/src/physics/ragdoll.ts shapes through addBody/addJoint with ball joints), and a box vehicle rolling. Record with bun run pr-video (video or stills, never a GIF) and embed. Closes #1682.

Rules: wait for bun run agent:bootstrap --check; branch claude/<slug> off origin/main; claim comment first; one task per PR with Refs #1682; bun run gen after export changes (JSDoc on every export; create* factories need snapshot/restore); CHANGELOG bullet; check-types and test on every touched package plus check-stateful-ratchet, check-doc-symbols, check-orphan-ratchet; merge origin/main before pushing; squash auto-merge; fix CI on the same branch. Rapier's WASM must not be imported by @jgengine/core; only the new package depends on it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions