Exercises
Six game dev exercises: physics loop, AABB collision, FSM, pooling, A* pathfinding, save/load.
Game dev — exercises
EXAMPLE
// ===== Exercise 1: fixed-timestep loop =====
const STEP = 1 / 60;
let acc = 0, last = performance.now();
let ball = { x: 100, y: 100, vx: 200, vy: -150 };
function frame(now) {
const dt = Math.min(0.25, (now - last) / 1000); last = now;
acc += dt;
while (acc >= STEP) {
ball.x += ball.vx * STEP;
ball.y += ball.vy * STEP;
ball.vy += 600 * STEP; // gravity
if (ball.y > 500) { ball.y = 500; ball.vy *= -0.9; } // bounce
acc -= STEP;
}
// render(ball, acc / STEP)
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
// ===== Exercise 2: AABB collision =====
function aabbOverlap(a, b) {
return a.x < b.x + b.w && a.x + a.w > b.x &&
a.y < b.y + b.h && a.y + a.h > b.y;
}
const box1 = { x: 100, y: 100, w: 50, h: 50 };
const box2 = { x: 120, y: 120, w: 50, h: 50 };
console.log(aabbOverlap(box1, box2)); // true
// ===== Exercise 3: simple FSM (enemy AI) =====
class Enemy {
constructor() { this.state = 'patrol'; this.hp = 100; }
update(player) {
if (this.state === 'patrol') {
if (distance(this, player) < 100) this.state = 'chase';
} else if (this.state === 'chase') {
if (distance(this, player) < 30) this.state = 'attack';
if (this.hp < 20) this.state = 'flee';
} else if (this.state === 'attack') {
if (this.hp < 20) this.state = 'flee';
if (distance(this, player) > 50) this.state = 'chase';
} else if (this.state === 'flee') {
if (this.hp > 80) this.state = 'patrol';
}
}
}
// ===== Exercise 4: object pool =====
class Pool {
constructor(create, reset, size) {
this.create = create;
this.reset = reset;
this.free = [];
for (let i = 0; i < size; i++) this.free.push(create());
}
acquire() {
return this.free.length ? this.free.pop() : this.create();
}
release(obj) {
this.reset(obj);
this.free.push(obj);
}
}
const bulletPool = new Pool(
() => ({ x: 0, y: 0, alive: false }),
(b) => { b.alive = false; },
100,
);
// ===== Exercise 5: A* pathfinding =====
function aStar(start, end, neighbors, heuristic) {
const open = new Map([[key(start), { node: start, g: 0, h: heuristic(start, end), parent: null }]]);
const closed = new Set();
while (open.size) {
let curKey, cur;
let minF = Infinity;
for (const [k, v] of open) {
if (v.g + v.h < minF) { minF = v.g + v.h; curKey = k; cur = v; }
}
if (key(cur.node) === key(end)) {
const path = [];
let p = cur;
while (p) { path.push(p.node); p = p.parent; }
return path.reverse();
}
open.delete(curKey); closed.add(curKey);
for (const n of neighbors(cur.node)) {
const k = key(n);
if (closed.has(k)) continue;
const g = cur.g + 1;
const existing = open.get(k);
if (!existing || g < existing.g) {
open.set(k, { node: n, g, h: heuristic(n, end), parent: cur });
}
}
}
return null;
}
function key(n) { return n.x + ',' + n.y; }
// ===== Exercise 6: save/load via localStorage =====
function save(state) {
localStorage.setItem('save', JSON.stringify(state));
}
function load() {
const s = localStorage.getItem('save');
return s ? JSON.parse(s) : null;
}
// ===== Patterns =====
// - Fixed timestep + accumulator
// - AABB for cheap rect collision
// - FSM for enemy AI; behaviour trees for richer
// - Object pools for bullets / particles
// - A* for grid pathfinding
// - Serialise state to localStorage / IndexedDB
// ===== Pitfalls =====
// - Frame-locked physics (movement * dt not constant)
// - Forgetting to clamp dt -> simulation explodes after tab returns
// - Pools without reset -> stale data persists
// - A* without consistent heuristic -> sub-optimal paths
Why it matters
Six game dev exercises drill the daily patterns: fixed timestep loop, AABB collision, FSM, object pool, A*, save/load. The same patterns port across Unity / Godot / Phaser / Bevy — only the surface API changes.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Discussion
Loading…