# The YokoCoder:4B Master Guide to HTML/CSS/JS (2D Canvas) & Three.js (3D WebGL) Video Game Engineering This guide serves both as a **reference manual for game developers** and as a **core architectural specification distilled into the `YokoCoder:4B` dataset**. Every pattern below is engineered to eliminate the most common failure modes in browser-based 2D and 3D games—such as `ReferenceError: dt is not defined`, stacked `requestAnimationFrame` loops, variable-framerate physics drift, hitbox mismatches, and Three.js memory leaks. --- ## 1. Single-File HTML/CSS/JS Game Architecture A production-quality single-file HTML game must be 100% self-contained (zero external image or audio file dependencies that could fail to load via CORS) and strictly ordered into five deterministic layers inside ` ``` ### 1.2 HiDPI Scaling for Vector Games vs. Integer Scaling for Pixel Art - **Smooth Vector Games (`400x600` logical space)**: Scale the physical canvas backing store by `dpr = Math.min(window.devicePixelRatio || 1, 2)` and call `ctx.setTransform(dpr, 0, 0, dpr, 0, 0)` so all game logic uses fixed logical coordinates (`W = 400`, `H = 600`) regardless of screen resolution. - **Native Pixel-Art Games (`144x256` or `160x288` logical buffer)**: Keep `canvas.width = 144` and `canvas.height = 256`, set `ctx.imageSmoothingEnabled = false`, and scale the CSS width/height by an integer multiplier `scale = Math.max(1, Math.floor(Math.min(window.innerWidth / 144, window.innerHeight / 256)))`. --- ## 2. The Golden Game Loop & Why `ReferenceError: dt is not defined` Happens ### 2.1 Root Cause of `Uncaught ReferenceError: dt is not defined` One of the most frequent bugs generated by small coding models is computing `const dt = ...` as a block-scoped local variable inside `loop(now)`, and then referencing `dt` inside `draw()` or `update()` **without passing `dt` as a function parameter**: ```javascript // ❌ BROKEN: dt is block-scoped inside loop(), so draw() throws ReferenceError! function draw() { wingPhase += dt * 10; // 💥 Uncaught ReferenceError: dt is not defined } function loop(now) { const dt = Math.min(0.1, (now - lastTime) / 1000); lastTime = now; update(dt); draw(); // ❌ Forgot to pass dt! requestAnimationFrame(loop); } ``` ### 2.2 The Invariant Fix: Explicit `update(dt)` and `draw(dt)` Signatures + Fixed-Step Accumulator To guarantee that `dt` is **never** undefined and physics is **100% identical on 60Hz, 120Hz, 144Hz, and 240Hz monitors**: 1. Declare timing variables (`lastTime`, `accumulator`, `rafId`) in the module scope before `loop`. 2. Guard the first frame (`if (!lastTime) lastTime = now;`) so `now - lastTime` is never `NaN` or huge on startup. 3. Clamp `frameDt = Math.min(0.1, Math.max(0, (now - lastTime) / 1000))` so switching browser tabs never causes physics tunneling. 4. Step physics in fixed `FIXED_DT = 1 / 120` increments and pass `FIXED_DT` explicitly into `update(dt)` and `frameDt` explicitly into `draw(dt)`. ```javascript const FIXED_DT = 1 / 120; let lastTime = 0; let accumulator = 0; let rafId = null; function update(dt = FIXED_DT) { // Pure physics & collision using explicit parameter `dt` } function draw(dt = FIXED_DT, alpha = 1) { // Visual animation timers (wing flap, particles, shaders) use explicit `dt` } function loop(now = performance.now()) { rafId = requestAnimationFrame(loop); if (lastTime === 0) lastTime = now; const frameDt = Math.min(0.1, Math.max(0, (now - lastTime) / 1000)); lastTime = now; accumulator += frameDt; while (accumulator >= FIXED_DT) { update(FIXED_DT); accumulator -= FIXED_DT; } draw(frameDt, accumulator / FIXED_DT); } // Start the single persistent RAF loop once: rafId = requestAnimationFrame(loop); ``` --- ## 3. State Machine & Input Discipline ### 3.1 Never Spawn Duplicate `requestAnimationFrame` Loops on Restart A classic bug occurs when `startGame()` or `resetGame()` calls `loop()` while a previous `requestAnimationFrame(loop)` is still running—doubling or tripling game speed after every death! **Rule**: Either keep **one persistent `requestAnimationFrame` loop** running across all states (`READY`, `PLAYING`, `GAMEOVER`) and branch on `state` inside `update(dt)`, OR always call `if (rafId !== null) cancelAnimationFrame(rafId);` before scheduling a new frame. ### 3.2 Time-Based Game-Over Grace Period (Never Use `frames % 10 === 0`!) Checking `if (frames % 10 === 0)` inside a click/keydown handler to "debounce" restarts drops **90% of user clicks** because input events fire asynchronously relative to frame ticks! Instead, track a time-based cooldown `gameOverLockTimer`: ```javascript const STATE = { READY: 'READY', PLAYING: 'PLAYING', GAMEOVER: 'GAMEOVER' }; let state = STATE.READY; let gameOverLockTimer = 0; // Seconds remaining before restart input is accepted function enterGameOver() { if (state === STATE.GAMEOVER) return; state = STATE.GAMEOVER; gameOverLockTimer = 0.35; // 350ms grace period prevents accidental instant skip bestScore = Math.max(bestScore, score); saveHighScore(bestScore); } function handleAction() { if (state === STATE.READY) { resetMatch(); state = STATE.PLAYING; bird.flap(); } else if (state === STATE.PLAYING) { bird.flap(); } else if (state === STATE.GAMEOVER && gameOverLockTimer <= 0) { resetMatch(); state = STATE.PLAYING; bird.flap(); } } ``` --- ## 4. 2D Collision Detection Mathematics ### 4.1 AABB vs. AABB (Axis-Aligned Bounding Box) Two rectangles $A = (x_A, y_A, w_A, h_A)$ and $B = (x_B, y_B, w_B, h_B)$ intersect if and only if projections on both axes overlap: ```javascript function aabbOverlap(ax, ay, aw, ah, bx, by, bw, bh) { return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by; } ``` ### 4.2 Forgiving Circle-vs-AABB Collision (Ideal for Rotating Sprites & Birds) When a sprite rotates visually (like the Flappy Bird tilting between $-25^\circ$ and $+90^\circ$), an axis-aligned rectangle has sharp invisible corners that feel unfair to players. A **circle** of center $(c_x, c_y)$ and radius $r$ is invariant under rotation. Clamp $(c_x, c_y)$ to the rectangle $[x, x+w] \times [y, y+h]$ to find the closest point $(p_x, p_y)$: ```javascript function circleIntersectsAABB(cx, cy, r, rx, ry, rw, rh) { const closestX = Math.max(rx, Math.min(cx, rx + rw)); const closestY = Math.max(ry, Math.min(cy, ry + rh)); const dx = cx - closestX; const dy = cy - closestY; return (dx * dx + dy * dy) < (r * r); } ``` ### 4.3 Preventing the "Fly Over Top Pipe" Bypass Bug In scrolling obstacle games, if the player flaps near $y = 0$ and the pipe collision check only tests `bird.y < pipe.top` while `bird.y` is allowed to go negative (or if top pipe caps start at $y = 0$), always: 1. Clamp the player ceiling: `if (bird.y - bird.r < 0) { bird.y = bird.r; bird.vy = 0; }`, **and** 2. Treat any horizontal overlap with a pipe while `bird.y - bird.r < pipe.gapTop` as a collision, even if `bird.y < 0`. --- ## 5. Three.js 3D Game Engineering Guide ### 5.1 Modern Import Map & Scene Setup Use ES Module Import Maps or explicit CDN script tags with a locked Three.js version (`0.160.0`) so `THREE` is reliably available without a bundler: ```html ``` ### 5.2 Preventing the Three.js "Black Screen" Bug Four mistakes cause a blank or black Three.js canvas: 1. **Using `MeshStandardMaterial` or `MeshPhysicalMaterial` without lights**: PBR materials require at least an ambient/hemisphere light (`THREE.HemisphereLight`) and a directional/point light (`THREE.DirectionalLight`). Only `MeshBasicMaterial` renders without lights. 2. **Camera inside the geometry or looking the wrong way**: Always set `camera.position.set(x, y, z)` and `camera.lookAt(targetX, targetY, targetZ)` explicitly. 3. **Zero-height container**: Ensure `html, body { width: 100%; height: 100%; margin: 0; }` and size the renderer via `renderer.setSize(window.innerWidth, window.innerHeight)`. 4. **Missing `camera.updateProjectionMatrix()` on resize**: ```javascript window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); ``` ### 5.3 Hierarchical Joint Rigging (`THREE.Group` Pivot Pattern) By default, Three.js primitives (`BoxGeometry`, `CylinderGeometry`) rotate around their geometric center $(0, 0, 0)$. To animate limbs, wings, or doors around a **joint hinge** (shoulder, hip, wing root): 1. Create a `const joint = new THREE.Group()` positioned at the hinge coordinates on the parent body. 2. Create the limb mesh and offset its local position by half its length (`limbMesh.position.y = -limbLength / 2`). 3. Rotate `joint.rotation.x`—never the offset child mesh directly! ### 5.4 Preventing WebGL GPU Memory Leaks (`dispose()` Lifecycle) Unlike JavaScript objects, Three.js `BufferGeometry`, `Material`, and `Texture` instances allocate GPU buffers in WebGL that are **not** freed by the browser's garbage collector when removed from a `Scene`. - **Rule 1**: Pre-allocate shared geometries and materials once at startup and reuse them across spawned obstacles, bullets, and particles (`const sharedPipeGeo = new THREE.CylinderGeometry(...)`). - **Rule 2**: For high-count particles or voxel chunks, use `THREE.InstancedMesh` or merged face buffers. - **Rule 3**: Whenever a unique mesh is permanently destroyed, dispose its GPU resources if not shared: ```javascript function removeAndDispose(scene, mesh) { scene.remove(mesh); if (mesh.geometry && !mesh.userData.sharedGeo) mesh.geometry.dispose(); if (mesh.material && !mesh.userData.sharedMat) { if (Array.isArray(mesh.material)) mesh.material.forEach(m => m.dispose()); else mesh.material.dispose(); } } ``` --- ## 6. Zero-Asset Procedural Web Audio Synthesizer Browsers block audio until the first user gesture (`keydown` or `pointerdown`). Use a lazy-initialized `AudioContext` helper to synthesize crisp retro arcade sound effects in pure code: ```javascript const SFX = (() => { let ctx = null; function unlock() { if (!ctx) { const AC = window.AudioContext || window.webkitAudioContext; if (AC) ctx = new AC(); } if (ctx && ctx.state === 'suspended') ctx.resume(); return ctx; } function tone(type, fStart, fEnd, duration, gainVal = 0.12) { const c = unlock(); if (!c) return; const now = c.currentTime; const osc = c.createOscillator(); const gain = c.createGain(); osc.type = type; osc.frequency.setValueAtTime(fStart, now); osc.frequency.exponentialRampToValueAtTime(Math.max(20, fEnd), now + duration); gain.gain.setValueAtTime(gainVal, now); gain.gain.exponentialRampToValueAtTime(0.0001, now + duration); osc.connect(gain).connect(c.destination); osc.start(now); osc.stop(now + duration); } return { flap: () => tone('sine', 320, 640, 0.09, 0.12), score: () => { tone('triangle', 587.33, 587.33, 0.07, 0.12); setTimeout(() => tone('triangle', 880, 880, 0.14, 0.12), 65); }, hit: () => tone('sawtooth', 160, 38, 0.26, 0.18), }; })(); ``` --- ## 7. Pre-Flight Verification Checklist for Every Game Before emitting the final `` tag, verify all 8 invariants: 1. **Scope Audit**: Every parameter used in `update(dt)` and `draw(dt)` is either declared in its signature or in module-level state. 2. **First-Frame Timestamp Guard**: `if (!lastTime) lastTime = now;` prevents `NaN` or huge initial `dt`. 3. **Tab-Switch Clamp**: `frameDt = Math.min(0.1, (now - lastTime) / 1000)` prevents teleporting through walls. 4. **Single RAF Loop**: Restarting a match resets state variables without spawning a second concurrent `requestAnimationFrame(loop)`. 5. **Input Responsiveness**: Restart works reliably after a short time-based grace period (`0.35s`), never gated on `frames % N === 0`. 6. **Zero-Allocation Hot Loop**: Particle and projectile pools use swap-and-pop (`pool[i] = pool[pool.length - 1]; pool.pop()`) instead of `Array.shift()` or `Array.filter()` inside 60fps loops. 7. **Safe Storage**: All `localStorage.getItem` / `setItem` calls are wrapped in `try { ... } catch {}` so sandboxed iframes and `file://` origins never throw `SecurityError`. 8. **Three.js Resource Hygiene**: Geometries and materials are shared across obstacle instances, lights are present for PBR materials, and window resize updates both `camera.aspect` and `camera.updateProjectionMatrix()`.