๐ Trace the game loop
Before writing new code it helps to understand how the game ticks every frame. Follow this chain through the code โ no changes needed, just reading and exploring.
/** * The main game loop โ called ~60 times per second. * Creator Task 1: this is where the chain starts! */ function loop(t) { requestAnimationFrame(loop); // schedule next frame const dt = Math.min(50, t - last) / 1000; // time since last frame (seconds) last = t; if (state === 'playing') update(dt); // โ step 1: move everything render(); // โ step 2: draw everything }
Now follow the chain. Click into each file and read the function it calls:
-
requestAnimationFrame(loop) โ the browser calls
loop()roughly 60 times per second. Each call is one frame. -
dt (delta time) โ the number of seconds since the last frame (usually about 0.016). Every movement is multiplied by
dtso the game runs at the same speed on fast and slow devices. -
update(dt) in
js/game.jsโ moves all the game objects. Look for wherebelly.update(dt, 0.6, planks)is called. Then openjs/entities.jsand read theupdate()method โ notice howthis.vy += gravitymakes Belly fall down, andthis.y += this.vymoves her. -
render() in
js/game.jsโ calls all the Renderer drawing functions. Look forRenderer.drawBelly(...). Then openjs/renderer.jsand finddrawBelly()to see how Belly is actually drawn.
dt)
keeps the game fair on any device.
๐ Add a brand-new obstacle kind
You're going to add a "shopping trolley" obstacle to the game from scratch. This touches three files โ size, hitbox, and drawing.
static SIZES = { // Level 1 โ playground toys 'toy-small': { w: 56, h: 56 }, 'dinosaur': { w: 64, h: 80 }, 'shopping-trolley': { w: 90, h: 68 }, โ add this! // ... };
const OBSTACLE_HITBOX = { // Level 1 'toy-small': [0.10, 0.10, 0.80, 0.80], 'shopping-trolley': [0.06, 0.15, 0.88, 0.80], โ add this! // ... };
const kindsByLevel = { 1: ['toy-small', 'toy-large', 'toy-ball', 'bicycle', 'blocks', 'toy-car', 'dinosaur', 'shopping-trolley', โ add this! ], // ... };
// Creator Task 2: add your new obstacle drawing block here! if (kind === 'shopping-trolley') { // Body of the trolley ctx.fillStyle = '#888'; ctx.fillRect(x + w*0.05, y + h*0.25, w*0.90, h*0.55); // Wheels ctx.fillStyle = '#444'; ctx.beginPath(); ctx.arc(x + w*0.22, y + h*0.88, w*0.10, 0, Math.PI*2); ctx.arc(x + w*0.78, y + h*0.88, w*0.10, 0, Math.PI*2); ctx.fill(); }
๐ Write a new helper function
The codebase already has a function called aabb()
that checks if two rectangles overlap. You're going to write a similar helper
โ rectContainsPoint() โ that checks if a single point
is inside a rectangle. Then you'll test it yourself in the browser console.
/** * Checks whether two rectangles overlap. * Returns true if they intersect. */ function aabb(a, b) { return ( a.x < b.x + b.w // a's left edge is left of b's right edge && a.x + a.w > b.x // a's right edge is right of b's left edge && a.y < b.y + b.h // a's top is above b's bottom && a.y + a.h > b.y // a's bottom is below b's top ); }
/** * Checks whether a point lies inside a rectangle. * @param {{ x, y, w, h }} rect - The rectangle. * @param {number} px - Point x position. * @param {number} py - Point y position. * @returns {boolean} */ function rectContainsPoint(rect, px, py) { return ( px >= rect.x // point is right of left edge && px <= rect.x + rect.w // point is left of right edge && py >= rect.y // point is below top edge && py <= rect.y + rect.h // point is above bottom edge ); }
const exported = { Belly, Obstacle, Collectible, Plank, aabb, rectContainsPoint, โ add this! };
// Type these lines into the console and press Enter each time: exported.rectContainsPoint({x:10, y:10, w:50, h:50}, 30, 30) // Should print: true (point 30,30 is inside the rectangle) exported.rectContainsPoint({x:10, y:10, w:50, h:50}, 5, 5) // Should print: false (point 5,5 is outside โ top-left of rectangle)
true then false, your function works perfectly!๐ก๏ธ Add a shield power-up that blocks one hit
This is the biggest challenge in the whole Academy. You're going to add a shield collectible that protects Belly from one obstacle hit. Take it step by step โ each step produces something you can test before moving on.
// Inside the Belly constructor, add this line: this.hasShield = false;
// In COLLECTIBLE_RADII, add the shield: const COLLECTIBLE_RADII = { donut: 16, // ... shield: 18, โ add this! };
// In spawnCollectible, add 'shield' to the kinds array: const kinds = [ 'donut', 'pizza', 'icecream', // ... 'shield', โ add this! ];
if (cKind === 'candy-cane') { score += CONFIG.CANDY_SCORE; candyCaneCount++; // ... } else if (cKind === 'shield') { belly.hasShield = true; // activate the shield! score += 25; // small bonus for finding it } else { score += CONFIG.COLLECT_SCORE; belly.collect++; }
if (exported.aabb(belly.hitBox(), obstacles[i].hitBox())) { if (invincible) { obstacles.splice(i, 1); continue; } obstacles.splice(i, 1); if (belly.hasShield) { belly.hasShield = false; // shield absorbs the hit! continue; // skip the lives-lost code below } belly.lives--; lostLifeThisLevel = true; // ... }
// Draw the shield ring around Belly when active if (belly.hasShield) { ctx.save(); ctx.strokeStyle = '#00eeff'; ctx.lineWidth = 4; ctx.globalAlpha = 0.75; ctx.beginPath(); ctx.arc( belly.x + belly.width / 2, belly.y + belly.height / 2, belly.width * 0.65, 0, Math.PI * 2 ); ctx.stroke(); ctx.restore(); }
You're a game developer!
You traced the game loop, added an obstacle, wrote a utility function, and built a complete power-up mechanic. That's real game development. Keep experimenting โ and remember you can always Ctrl+Z to undo!