Build a physics puzzle
Goal
Push a lever past 25 degrees to raise a heavy gate. Dodge the swinging weight or use it to shove the lever; win when the gate has travelled 2 metres.
Files you will edit
src/prefabs.tssrc/game.ts
Steps
Append these prefabs to the third-person template's
src/prefabs.ts, using its existingprefabimport. Their default layer already meets the hero's mask. None has the template'sDoortag. The weight and lever can collide; each joint here connects to the world, so there is no connected body to hit.tsexport const PuzzleLeverPrefab = prefab({ name: 'puzzle-lever', body: { shape: 'box', dims: [0.12, 0.7, 0.12], kind: 'dynamic', mass: 5, layer: { defaultLayer: true }, mask: { defaultLayer: true, player: true, staticGeometry: true }, }, }); export const PuzzleGatePrefab = prefab({ name: 'puzzle-gate', body: { shape: 'box', dims: [1, 1.2, 0.15], kind: 'dynamic', mass: 100, layer: { defaultLayer: true }, mask: { player: true, staticGeometry: true }, }, }); export const PuzzleWeightPrefab = prefab({ name: 'puzzle-weight', body: { shape: 'sphere', dims: [0.35], kind: 'dynamic', mass: 20, layer: { defaultLayer: true }, mask: { defaultLayer: true, player: true, staticGeometry: true }, }, });In
src/game.ts, addphysicsto the SDK import and the three new prefabs to the./prefabsimport. Add this state and system abovedefineGame. These records are reused on every step; the motor command is only sent when the lever crosses the threshold.tslet leverJoint = 0; let gateJoint = 0; let gateOpening = false; let puzzleWon = false; const gateMotor = { mode: 'position', target: 0, maxForce: 10000 } as const; const gateTarget = { ...gateMotor, target: 0 }; function physicsPuzzle(ctx: GameContext): void { const angle = physics.jointValue(leverJoint); const opening = angle !== null && angle > 25; if (opening !== gateOpening) { gateOpening = opening; gateTarget.target = opening ? 2.4 : 0; physics.setMotor(gateJoint, gateTarget); } const travel = physics.jointValue(gateJoint); if (!puzzleWon && travel !== null && travel > 2) { puzzleWon = true; console.log('physics puzzle solved'); ctx.hud.invalidate(); } }Append this setup to the existing
init(ctx). Put these props in clear space, away from the arena walls. The lever's lower end is the hinge; its positive rotation about +Z tips its top toward -X. The slider's positive displacement is upward along +Y, relative to its initial position.tsgateOpening = false; puzzleWon = false; const lever = ctx.spawn(PuzzleLeverPrefab, { x: 2, y: 1, z: -3 }); tint(lever, CHEST_COLOUR); leverJoint = physics.hinge(lever, 0, { anchor: { x: 2, y: 0.3, z: -3 }, axis: { x: 0, y: 0, z: 1 }, minDeg: -40, maxDeg: 40, friction: 8, }); const gate = ctx.spawn(PuzzleGatePrefab, { x: 5, y: 1.3, z: -3 }); tint(gate, DOOR_COLOUR); gateJoint = physics.slider(gate, 0, { anchor: { x: 5, y: 1.3, z: -3 }, axis: { x: 0, y: 1, z: 0 }, min: 0, max: 2.4, }); gateTarget.target = 0; physics.setMotor(gateJoint, gateTarget); const weight = ctx.spawn(PuzzleWeightPrefab, { x: 2.8, y: 1.5, z: -3 }); tint(weight, CHEST_COLOUR); physics.distance(weight, 0, { anchor: { x: 2.8, y: 1.5, z: -3 }, anchorB: { x: 2.8, y: 4.5, z: -3 }, }); physics.applyImpulse(weight, -50, 0, 0);With no distance limits the connection is a rigid 3 m rod at its initial length, not a slack rope. A 100 kg gate weighs about 981 N: the default motor limit of 100 N cannot lift it. Here
maxForce: 10000supplies enough force to raise and hold it. Keep connected bodies apart in two-body joints: joining them does not disable their collisions.Add
physicsPuzzleafterlocomotionSystemin the existingsystemsarray.puzzleWonis the latched result for your HUD or subsequent game rules; the win test uses sliderjointValue, not the gate's visual pose.
Verify
sh
npm run devThe weight swings from its overhead anchor. Push the lever toward -X until physics.jointValue(leverJoint) exceeds 25 degrees: the gate rises. Its jointValue passes 2 metres, puzzleWon becomes true, and the console prints physics puzzle solved once. Push the lever back below the threshold and the gate closes; the win stays latched.