iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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.

Example

Example
// Pong → Brick breaker → Top-down shooter → Platformer. Ship each one.
Try it Yourself »

Discussion

Loading…