Examples
Five small game-dev recipes that come up in real projects: a fixed-timestep game loop, an entity-component system sketch, a tween library, a save-file with versioning, and a simple state machine for player movement.
Five game-dev recipes
EXAMPLE
// 1) Fixed-timestep game loop (engine-agnostic)
// Render as often as the browser/engine allows, but UPDATE physics at a
// constant rate. Smooths variable framerates, makes physics deterministic.
let last = performance.now();
let acc = 0;
const STEP = 1 / 60; // 60 Hz logic
function frame(now) {
const dt = Math.min((now - last) / 1000, 0.25); // cap big spikes (tab unfocused)
last = now;
acc += dt;
while (acc >= STEP) {
update(STEP);
acc -= STEP;
}
render(acc / STEP); // interpolation factor
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
function update(dt) { /* physics, AI, input */ }
function render(alpha) { /* draw using current + previous state via lerp(alpha) */ }
// 2) Tiny Entity-Component-System
class World {
constructor() {
this.next = 0;
this.components = new Map(); // name -> Map<entityId, component>
this.systems = [];
}
create() { return ++this.next; }
addComponent(eid, name, data) {
if (!this.components.has(name)) this.components.set(name, new Map());
this.components.get(name).set(eid, data);
}
query(...names) {
const first = this.components.get(names[0]);
if (!first) return [];
return [...first.keys()].filter((eid) =>
names.every((n) => this.components.get(n)?.has(eid))
);
}
addSystem(sys) { this.systems.push(sys); }
update(dt) { for (const s of this.systems) s(this, dt); }
}
const world = new World();
const player = world.create();
world.addComponent(player, 'pos', { x: 0, y: 0 });
world.addComponent(player, 'vel', { x: 1, y: 0 });
world.addSystem((w, dt) => {
for (const eid of w.query('pos', 'vel')) {
const p = w.components.get('pos').get(eid);
const v = w.components.get('vel').get(eid);
p.x += v.x * dt;
p.y += v.y * dt;
}
});
// 3) Tween — simple per-update interpolation
class Tween {
constructor(target, prop, from, to, duration, ease = (t) => t) {
this.target = target; this.prop = prop;
this.from = from; this.to = to; this.duration = duration; this.ease = ease;
this.elapsed = 0;
}
update(dt) {
this.elapsed = Math.min(this.elapsed + dt, this.duration);
const t = this.elapsed / this.duration;
this.target[this.prop] = this.from + (this.to - this.from) * this.ease(t);
return this.elapsed >= this.duration;
}
}
// Easing functions
const ease = {
linear: (t) => t,
outQuad: (t) => t * (2 - t),
outBack: (t) => { const c = 1.70158; return 1 + (c + 1) * (t - 1) ** 3 + c * (t - 1) ** 2; },
};
// 4) Versioned save file
const SAVE_VERSION = 3;
function save(state) {
const data = { version: SAVE_VERSION, state };
localStorage.setItem('save', JSON.stringify(data));
}
function load() {
const raw = localStorage.getItem('save');
if (!raw) return defaultState();
const data = JSON.parse(raw);
return migrate(data);
}
function migrate(data) {
if (data.version === 1) data = migrateV1ToV2(data);
if (data.version === 2) data = migrateV2ToV3(data);
if (data.version !== SAVE_VERSION) throw new Error('unknown save version');
return data.state;
}
function migrateV1ToV2(data) { return { ...data, version: 2 }; }
function migrateV2ToV3(data) { return { ...data, version: 3 }; }
function defaultState() { return { level: 1, hp: 10 }; }
// 5) Player state machine
const states = {
idle: { onUpdate: (p, dt, input) => input.move ? 'walking' : 'idle' },
walking: { onUpdate: (p, dt, input) => {
p.x += input.move * 80 * dt;
if (input.jump) return 'jumping';
if (!input.move) return 'idle';
return 'walking';
} },
jumping: { onEnter: (p) => { p.vy = -240; },
onUpdate: (p, dt) => {
p.vy += 600 * dt;
p.y += p.vy * dt;
return p.y >= 0 ? 'idle' : 'jumping';
} },
};
class Player {
constructor() { this.x = 0; this.y = 0; this.vy = 0; this.state = 'idle'; }
update(dt, input) {
const def = states[this.state];
const next = def.onUpdate?.(this, dt, input) ?? this.state;
if (next !== this.state) {
states[next].onEnter?.(this);
this.state = next;
}
}
}
// ===== Patterns to internalise =====
// - Fixed-timestep update + variable-rate render for stable physics
// - ECS scales better than 'one class per entity' for large worlds
// - Tweens for inbetweens; full animation libs (GSAP) for complex sequences
// - Save files are versioned the moment you ship; migration scripts forever
// - State machines for player / NPC behaviour; one source of truth
// ===== Pitfalls =====
// - Using requestAnimationFrame as the update tick -> framerate-dependent physics
// - Garbage-creating ECS queries -> reuse arrays
// - Save files without a version field -> nothing to migrate from
// - State machines that allow ANY transition -> spaghetti
Why it matters
Fixed-timestep updates separate "how the world advances" from "how often it draws". Pair that with a tiny state machine for the player and an ECS for the entities and you have the skeleton of an engine that scales — no engine framework required.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…