Making a game
Smooth gameplay
Drawing between steps, teleports and camera cuts, and measuring a judder.
The rules step at a fixed rate, prefabs.engine.dt, 1/60 of a second in a new game, and the screen draws at its own. This page covers drawing smoothly between the steps, and marking a jump, which the drawing cannot tell from fast travel.
What each frame draws
- Under render3d, every object and the camera are drawn a step behind the clock, between where the last two steps left them. A look writes its object from its row as the row is, every frame, and never eases, lerps or extrapolates it: a second blend on top lags and wobbles.
- An InstancedMesh's copies are drawn where they are set, not between.
- A thing moved on a frame that took no step, such as an animation, a particle or a camera turned on the page's clock, is drawn where it is set for the rest of that step and all of the next. It is drawn between steps again once a whole step has gone by with only the steps moving it.
- A thing new on the newest step is not drawn until the clock reaches its step, and is then drawn where it was set.
Marking a jump
- In the rule, add 1 to a field such as
jumpson the step that puts the row somewhere new: a door to elsewhere, a respawn, a car put back on the road. A field set for one step and cleared is missed when two packets land in one frame; a count is not. - In the look, set render3d's
userData.teleportto true on the object on the frame the count changes, and back to false on a later frame. - Flag every other object that jumps with it the same way, off the same count: the camera for a first-person jump, the player's body for the other seats, a held thing. A child moved with its parent needs no flag; a child moved by itself needs its own.
- For a thing that jumps every step, such as a light moved from lamp to lamp, leave the flag up.
// player.client.ts, for the row that is mine
let jumps = 0
export function setup(row: Row) { jumps = row.data.jumps }
export function tick(row: Row, plugins: Plugins) {
const camera = plugins.render3d.camera
if (!camera) return
camera.userData.teleport = row.data.jumps !== jumps
jumps = row.data.jumps
camera.position.set(row.data.pos[0], row.data.pos[1] + eye, row.data.pos[2])
camera.quaternion.setFromAxisAngle(up, row.data.yaw)
}
render2d has no such flag.
Keeping it smooth
- Set a turn whole, as a quaternion:
quaternion.setFromAxisAngleorquaternion.fromArray. Never set one Euler angle alone, asrotation.y = yaw: past 90° three.js hands back a mirrored set of angles, and the blend turns the long way round every frame. - Solve each motion exactly over
dt, so it moves the same at every step rate (Tuning feel). The manager'sprefabs.engine.dt, itsprefabs.scenemanager.dtand each scene'sprefabs.engine.dtchange together. - Put what the seat's press moves on its
playerrow, soclient.htmldraws it from the page's own copy on the frame after the key (Designing a seat). A camera follows that row. - Make a one-step event a ledger entry, not a field set and cleared (Input and seats).
- Compute rules with math, never
Math.sinand the rest: node and Chrome differ in the last bit, and the page's copy drifts from the server's. - Give predict's
world.predict.snapin the rows' units, well under the shortest jump. A correction no bigger thansnapis eased out overhalflifemilliseconds; a bigger one, such as a correction across a jump, is drawn at once.
What goes wrong
| Seen | Cause | Fix |
|---|---|---|
| The camera slides across the level for a frame on a jump or a respawn | Not flagged | userData.teleport on the frame it is put there |
| It still slides for part of the step | The flag raised and lowered in the same tick | Lower it on a later frame |
| A thing that jumps every step streaks | It is blended | Leave its flag up |
| A turn spins the long way | One Euler angle set alone | Set the whole quaternion |
| A look's own smoothing lags and wobbles | A second blend over render3d's | Write the object from the row as it is |
| The flag is on a group, and a child still slides | The child moved by itself | Flag the child |
| A jump is missed on some frames | A one-step true or false on the row | A count |
Checking it
- Record what is drawn every frame with render3d's
drew(f): where the camera and each mesh stand, between the steps. The frame where a drawn value leaves the row's is the bug. - Read the simulator's
standoff: how much the camera's distance to the seat's row changes from frame to frame. - Play the game in Chrome at the screen's own rate, walk through the door, and watch for any frame between the two places.