Tune the follow camera
Goal
A camera that sits where you want it: a longer or shorter boom, a different shoulder height, a pitch range that suits your level, and an over-the-shoulder offset instead of a centred one.
Files you will edit
src/game.tssrc/systems/locomotion.ts
Steps
Know who owns what. The guest states an intent; the host owns the arm.
ctx.camera.follow(hero, { yaw, pitch, distance, height })writes onecamera-staterecord — the orbit pivot, the direction the player is looking, the boom length — and the engine builds a spring arm from it, probes from the pivot towards where the camera wants to sit, and pulls the boom in when a wall is in the way. You never write the collision.Change the numbers. Everything is a rule in
src/game.ts, and the declarativeplayerblock has to agree with it, because the engine re-states the rig after every user system:tsplayer: { prefab: HeroPrefab, spawn: HERO_SPAWN, camera: 'thirdPerson', distance: 3, // a tighter, more claustrophobic boom height: 1.35, // roughly shoulder height on this capsule sensitivity: 0.0018, }, rules: { cameraDistance: 3, cameraHeight: 1.35, cameraPitch: -0.3, // resting: a little above, looking down cameraMinPitch: -1.0, // how far the camera may rise cameraMaxPitch: 0.45, // how far it may drop and look up },Keep
distance/heightandcameraDistance/cameraHeightthe same number. They are the same boom stated twice, andtests/game.test.tsasserts the result.Offset the shoulder. The centred boom is the
aimCamerahelper insrc/systems/locomotion.ts. Swinging the pivot sideways is one extra term on the yaw:tsfunction aimCamera(ctx: GameContext, hero: number): void { const shoulder = Number(ctx.rules.cameraShoulder ?? 0.5); ctx.camera.follow(hero, { // A small yaw offset reads as an over-the-shoulder camera, and costs the // rig nothing: it is the same one arm, aimed slightly to one side. yaw: locomotionState.yaw + shoulder * 0.12, pitch: locomotionState.pitch, distance: Number(ctx.rules.cameraDistance ?? 4.5), height: Number(ctx.rules.cameraHeight ?? 1.5), }); }Keep the accumulator honest.
orbitclampsctx.camera.look.pitchin place before reading it. That is not tidiness: the declarativecamera: 'thirdPerson'block rebuilds the camera out of that same accumulator after your systems run, so a clamp that is not written back is a clamp the engine ignores.
Verify
sh
npm test -w templates/third-person
npm run dev -w templates/third-personWalk the hero into a corner and hold the mouse so the camera swings into the wall: the boom shortens instead of the wall filling the screen. The headless test rides a spring arm behind the hero asserts armLength and offset.y straight out of output.camera, so a mismatched pair fails before you look at it.
See also
- Build your first adventure — the template this tunes
- Add a locomotion state — the other half of the same system
- Read player input — where the mouse delta comes from
- The engine loop — why the camera is interpolated, not snapped