Phase 3 of 10

๐ŸŽฎ Game Engines

Pick the right engine, save 50%+ time

10 sub-phases
60 pros
60 cons
80 gotchas
50 anti-patterns
14956 chars code
3.1

๐ŸงŠ Babylon.js (Full-Stack 3D Engine)

Advancedโฑ 1 week

Full 3D engine โ€” PBR + Havok physics + particles + GUI + WebXR โ€” 1.5MB dep replaces 4 Three addons.

๐ŸŽฏ When to use: 3D city builder (stronghold-3d); NPC sim (sims-lite); FPS (fps-cops-vs-robbers); MMORPG (realm-of-heroes); Mars strategy (mars-colony); idle RPG (sj88-rpg-idle-3d)
โ›” When NOT to use
โ€ข Tight 3D model viewer <300KB โ†’ use Three.js (3.4)
โ€ข pure 2D โ†’ use Phaser (3.2)
โ€ข shader-only particle system โ†’ use raw WebGL (3.7)
โœ… Pros (6)
โ€ข PBR + Havok + particle editor + GUI + post-FX in 1.5MB โ†’ zero glue vs Three where you'd compose 3 addons
โ€ข Playground at playground.babylonjs.com = live-edit IDE โ†’ share bug-repro URL in 30s (no JSFiddle setup)
โ€ข thinInstanceAdd draws 10000 identical cubes in 1 draw call โ†’ stronghold-3d tiles 100x100 grid at 60fps
โ€ข WebXR first-class โ€” fps-cops-vs-robbers ships VR mode in 50 lines vs Three's @three/xr + manual session glue
โ€ข ArcRotateCamera + Inspector baked-in โ†’ no need to wire OrbitControls or dat.gui separately
โ€ข v8 (2024) ships Havok IK for character animation โ†’ no three-stdlib AnimationMixer ladder
โš ๏ธ Cons (6)
โ€ข 1.5MB+ minified โ‰ˆ 10x Three โ†’ mobile first-paint hurts if not code-split per route
โ€ข API surface ~5x Three โ†’ ramp-up 1 week vs 3 days; niche questions get fewer SO answers
โ€ข PBR + shadows = thermal throttling on iPhone 11 / low-end Android โ†’ must swap to StandardMaterial
โ€ข Inspector = +200KB; forgetting tree-shake ships it in prod and slows boot 300ms
โ€ข BabylonGUI 2D โ‰  DOM โ†’ no screen-reader / IME for Thai-Arabic; mix HTML overlay for inputs
โ€ข Havok v2 = WASM 3MB lazy-loaded โ†’ first physics step shows 100ms hitch unless preloaded
โšก Gotchas (8)
โ€ข freezeWorldMatrix() after placing static meshes โ€” without it Babylon recomputes world transforms each frame costing 5-15ms on 1000-mesh scenes (stronghold-3d lesson)
โ€ข thinInstanceAdd (NOT createInstance) for > 100 identical objects โ€” instance API spawns N draw calls, thin packs into 1 buffer + matrix refresh
โ€ข ArcRotateCamera alpha/beta are radians; setting alpha=Math.PI flips 180ยฐ not 360ยฐ โ€” use lerp(t,0,1) for smooth tween
โ€ข PBR materials need .env cubemap โ€” without it metallic surfaces render black; load via CubeTexture from .dds
โ€ข Havok WASM needs await HavokPhysics() before first PhysicsAggregate โ€” otherwise physics silently no-ops
โ€ข PointerEvent fires on both canvas AND DOM HUD โ†’ wire scene.onPointerObservable to avoid double-input in realm-of-heroes
โ€ข Calling scene.render() manually inside runRenderLoop = double-render โ†’ 30fps instead of 60fps; let engine own the loop
โ€ข iOS Safari drops WebGL context after 30min background โ†’ listen to engine.onContextLostObservable and rebuild scene
๐Ÿšซ Anti-patterns
โ€ข DON'T `import * as BABYLON` in TS โ€” use named imports `import { Engine, Scene } from '@babylonjs/core'` tree-shakes ~30%
โ€ข DON'T allocate Mesh per bullet โ€” pool 50 meshes; stronghold-3d reuses them across 10k spawns
โ€ข DON'T run physics in JS thread for > 200 bodies โ€” enable Havok WASM (sims-lite NPC crowds)
โ€ข DON'T use StandardMaterial outdoor โ€” PBR + IBL only; Standard gives plastic look in sunlight
โ€ข DON'T skip scene.skipPointerMovePicking = true with 1000+ pickables โ€” picking = 5ms hit-test per frame
๐Ÿ”€ Alternatives
vs 3.4 Three.js (smaller, no physics)
vs 3.7 WebGL (raw GPU)
vs 3.8 WebGPU (next-gen)
๐Ÿ“š Prereqs: 1.1, 1.6
โžก๏ธ Next: 3.10, 4.1, 4.4
๐Ÿ“– Docs: doc.babylonjs.com/
๐Ÿ’ป Minimal (218 chars) โ–ถ Full example (1229 chars)
const engine = new BABYLON.Engine(canvas, true); const scene = new BABYLON.Scene(engine); scene.createDefaultCameraOrLight(); BABYLON.MeshBuilder.CreateBox('b',{size:1},scene); engine.runRenderLoop(()=>scene.render());
// Babylon 8 โ€” Havok physics + thinInstance grid (stronghold-3d pattern)\nimport { Engine, Scene, HavokPlugin, Vector3, MeshBuilder, Matrix, PhysicsAggregate, PhysicsShapeType } from '@babylonjs/core';\nimport HavokPhysics from '@babylonjs/havok';\nconst havok = await HavokPhysics();\nconst engine = new Engine(canvas, true, { stencil: true });\nconst scene = new Scene(engine);\nscene.enablePhysics(new Vector3(0, -9.81, 0), new HavokPlugin(true, havok));\nconst ground = MeshBuilder.CreateGround('g', { width: 100, height: 100 }, scene);\nnew PhysicsAggregate(ground, PhysicsShapeType.BOX, { mass: 0 }, scene);\n// 100 crates in 1 draw call\nconst crate = MeshBuilder.CreateBox('crate', { size: 1 }, scene);\nconst matrices = new Float32Array(16 * 100);\nconst buf = new Float32Array(100 * 16);\nfor (let i = 0; i < 100; i++) {\n  Matrix.TranslationToRef(i % 10, 5, Math.floor(i / 10), crate.thinInstanceAdd ? Matrix.Identity() : Matrix.Identity());\n  Matrix.Translation(i % 10, 5, Math.floor(i / 10)).copyToArray(buf, i * 16);\n}\ncrate.thinInstanceSetBuffer('matrix', buf, 16);\ncrate.thinInstanceRefreshBoundingInfo();\nwindow.addEventListener('resize', () => engine.resize());\nengine.runRenderLoop(() => scene.render());
3dbabylonpbrwebxrhavokเน€เธเธก 3dengine
3.2

๐ŸฅŠ Phaser 3 (2D Game Engine)

Intermediateโฑ 3 days

Comprehensive 2D engine โ€” sprites, Arcade/P2/Matter physics, scene lifecycle, animations, tilemaps, sound โ€” 800KB.

๐ŸŽฏ When to use: 2D fighter (street-fighter-thai); 2D shooter (sky-ace); tower defense; platformer; RPG (realm-of-heroes top-down); mobile canvas game
โ›” When NOT to use
โ€ข Pure 3D โ†’ use Babylon (3.1)
โ€ข < 10 entities with no physics โ†’ use Vanilla Canvas (3.3)
โ€ข > 1000 sprites needing max FPS โ†’ use PixiJS (3.6)
โœ… Pros (6)
โ€ข 3 physics engines (Arcade/P2/Matter) ship built-in โ†’ Arcade for platformers, Matter for ragdoll โ€” no addon hunt
โ€ข Scene lifecycle (init/preload/create/update) forces clean state separation โ†’ realm-of-heroes uses MenuScene/GameScene/UIScene stack
โ€ข First-class TypeScript definitions โ†’ IDE autocomplete vs Phaser 2 JS-only โ€” saves 1 hour per session
โ€ข WebGL + Canvas renderers auto-fallback โ†’ iOS Safari 14 with WebGL bug falls back to Canvas without code change
โ€ข Animation editor exports JSON โ†’ designer-friendly; Phaser parses sprite-sheet atlas (TexturePacker / Aseprite) in 3 lines
โ€ข Camera effects (shake/flash/fade) + Tween manager built-in โ†’ no GSAP dep for game juice
โš ๏ธ Cons (6)
โ€ข 800KB minified โ‰ˆ 8x PixiJS โ†’ hurts mobile LCP; use Phaser MINIMAL build to drop to 400KB
โ€ข API has 3 layers (Game/Scene/GameObject) โ†’ junior dev writes boilerplate; Arc/sprite confusion
โ€ข Arcade physics is AABB-only โ†’ circles/rotated bodies need P2 or Matter (3x CPU cost)
โ€ข No built-in 9-slice โ€” must use third-party plugin or roll your own (Pixel-Game dev curse)
โ€ข Matter.js has 200KB weight โ€” sky-ace ships Matter only for ragdoll debris, Arcade for gameplay
โ€ข No first-class module system before v3.60 โ€” code-split per scene requires import map or bundler hacks
โšก Gotchas (8)
โ€ข Call this.physics.add.collider(player, layer) AFTER physics.world.enable() in create() โ€” setup order matters; sky-ace silent fall-through if reversed
โ€ข Scene shutdown must call this.events.off() on listeners โ†’ otherwise memory leak + double-fire after scene restart
โ€ข Scale.FIT + parent centered div causes 1px black bar on iOS notch โ€” use Scale.RESIZE + safe-area-inset CSS
โ€ข TexturePacker atlas needs fixedSize:false + padding:2 โ†’ otherwise Phaser bleed artifacts in sky-ace tileset
โ€ข Phaser.Loader.LOCKING = false in create() OR preload() blocks forever on slow CDN โ€” set timeout: 15000
โ€ข iOS Safari blocks AudioContext until user gesture โ†’ street-fighter-thai unlocks sound on first tap in MainScene.create
โ€ข Matter debug graphics = +20% CPU โ€” enable only in dev (Phaser config `physics.matter.debug: false` in prod)
โ€ข Tilemap collision misses if tileset imported with wrong pixel scale โ€” match Tiled's tilewidth to texture frame size exactly
๐Ÿšซ Anti-patterns
โ€ข DON'T update in render() callback โ€” use update(time, delta) for frame-time-independent physics (street-fighter-thai rule)
โ€ข DON'T load assets in create() โ€” use preload() + Loader; otherwise first-frame flickers with missing textures
โ€ข DON'T put DOM <button> over Phaser canvas for menu โ€” use Phaser text objects OR full DOM scene with Phaser.Game destroyed
โ€ข DON'T allocate new Phaser.Game per scene transition โ€” single Game instance with scene.start() is 5x faster
โ€ข DON'T use Matter for platformer โ€” Arcade AABB is 10x faster and 95% sufficient (sky-ace precedent)
๐Ÿ”€ Alternatives
vs 3.3 Vanilla Canvas (no dep)
vs 3.6 PixiJS (render-only, faster)
vs 3.5 Kaboom (simpler)
๐Ÿ“š Prereqs: 1.1, 1.6
โžก๏ธ Next: 3.10, 4.1, 4.6
๐Ÿ“– Docs: phaser.io/docs
๐Ÿ’ป Minimal (361 chars) โ–ถ Full example (1428 chars)
class MainScene extends Phaser.Scene { create(){ this.player=this.physics.add.sprite(100,450,'p').setBounce(.2); this.cursors=this.input.keyboard.createCursorKeys(); } update(){ this.player.setVelocity(0); if(this.cursors.left.isDown) this.player.setVelocityX(-200); if(this.cursors.up.isDown&&this.player.body.touching.down) this.player.setVelocityY(-400); } }
// Phaser 3 โ€” fighter skeleton (street-fighter-thai pattern)\nclass BattleScene extends Phaser.Scene {\n  constructor() { super('Battle'); }\n  preload() { this.load.atlas('fighter', 'assets/fighter.png', 'assets/fighter.json'); }\n  create() {\n    this.fighter = this.physics.add.sprite(400, 500, 'fighter', 'idle/01').setBounce(0.15);\n    this.fighter.setCollideWorldBounds(true);\n    this.cursors = this.input.keyboard.createCursorKeys();\n    this.attackKey = this.input.keyboard.addKey('SPACE');\n    this.anims.create({ key: 'punch', frames: this.anims.generateFrameNames('fighter', { prefix: 'punch/', end: 6, zeroPad: 2 }), frameRate: 18, repeat: 0 });\n    this.attackKey.on('down', () => { if (!this.fighter.anims.isPlaying) { this.fighter.anims.play('punch'); this.fighter.setVelocityX(this.fighter.flipX ? 600 : -600); } });\n  }\n  update(_, dt) {\n    this.fighter.setVelocityX(0);\n    if (this.cursors.left.isDown) { this.fighter.setVelocityX(-260); this.fighter.flipX = true; }\n    else if (this.cursors.right.isDown) { this.fighter.setVelocityX(260); this.fighter.flipX = false; }\n    if (this.cursors.up.isDown && this.fighter.body.touching.down) this.fighter.setVelocityY(-520);\n  }\n}\nnew Phaser.Game({ type: Phaser.AUTO, parent: 'game', scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH }, physics: { default: 'arcade', arcade: { gravity: { y: 900 } } }, scene: [BattleScene] });
2dphaserspritesphysicsเน€เธเธก 2dtilemapengine
3.3

๐Ÿ“ฆ Vanilla Canvas 2D (Zero-Dep 2D)

Intermediateโฑ 2 days

Pure Canvas 2D API + manual game loop. 0KB dep. Best for <200 entities with simple physics (tycoon, particles, UI tools).

๐ŸŽฏ When to use: Tycoon (noodle-shop customer queue); particle effects; data viz; simple click-game < 200 entities; tools (palette color picker); tile-map RPG under 50x50
โ›” When NOT to use
โ€ข > 200 entities with collisions โ†’ use Phaser (3.2)
โ€ข need physics engine โ†’ use Phaser Arcade
โ€ข performance 60fps with 1000+ sprites โ†’ use PixiJS (3.6)
โœ… Pros (6)
โ€ข 0KB dep โ†’ loads in <50ms on slow 3G; ideal for CodeHub static landing widgets
โ€ข Full control of draw order/blend modes/transforms โ€” no engine abstraction tax
โ€ข Debug = what user sees; no source-map or framework error noise in console (noodle-shop dev speed)
โ€ข Works file:// โ†’ offline / Codepen / jsFiddle without install โ€” pasta-shop demo runs from USB
โ€ข Canvas 2D matured in all browsers โ€” even iOS Safari 13+ supports Path2D, OffscreenCanvas
โ€ข No vendor lock-in; HTML/CSS overlay for HUD = free a11y + IME + screen-reader for Thai
โš ๏ธ Cons (6)
โ€ข DOM-free a11y โ†’ screen readers miss canvas content; need offscreen <canvas> mirrored to DOM (noodle-shop tax)
โ€ข No scene mgmt โ†’ 1000-line games become spaghetti; manual update/render dispatch
โ€ข Canvas 2D < WebGL perf at > 500 draw calls/frame; drawImage with rotation = 0.3ms each on mid-mobile
โ€ข Manual asset versioning; cache bust with ?v=Date.now() to defeat iOS Safari 7-day HTML cache
โ€ข No spatial index โ€” collision check O(nยฒ) past 200 entities; need quadtree or grid broadphase
โ€ข devicePixelRatio must scale canvas resolution โ€” without it retina = blurry (most common bug)
โšก Gotchas (8)
โ€ข Set canvas.width = w * devicePixelRatio AND ctx.scale(dpr, dpr) โ€” otherwise retina blurry
โ€ข requestAnimationFrame NOT setInterval โ€” rAF pauses when tab backgrounded (saves battery + matches display Hz)
โ€ข Clear with ctx.clearRect(0,0,w,h) NOT ctx.fillRect(white) โ€” clearRect is 3x faster on iOS Safari
โ€ข ctx.save()/restore() pairing inside a draw call must balance โ€” otherwise transform stack corrupts (noodle-shop lesson)
โ€ข ctx.measureText() returns cached width โ€” call once at boot for HUD text, not every frame (tycoon dashboard)
โ€ข Touch events need {passive:false} to call e.preventDefault() โ†’ without it scroll fights drag
โ€ข OffscreenCanvas.transferToImageBitmap() for worker rendering โ€” Chrome only but 10x faster for image processing
โ€ข Hit-test for clicks: track mouse pos + button-down + entity list โ€” never use canvas's hit-region API (Safari buggy)
๐Ÿšซ Anti-patterns
โ€ข DON'T draw all entities every frame without dirty-rect tracking โ€” re-use offscreen canvas for static layers (noodle-shop kitchen tiles)
โ€ข DON'T put game state in DOM <input> hidden fields โ€” use plain JS object + localStorage; DOM read/write is 10x slower per frame
โ€ข DON'T call canvas.toDataURL() per frame โ€” it stalls 50ms; only call on save event
โ€ข DON'T use ctx.drawImage with imageElement cached wrong โ€” pre-decode via img.decode() promise before first draw
โ€ข DON'T ignore ResizeObserver โ†’ canvas keeps stale size on phone rotate; obs + resize handler fixes 90% of mobile reports
๐Ÿ”€ Alternatives
vs 3.2 Phaser (scenes + physics)
vs 3.6 PixiJS (WebGL perf)
vs 3.5 Kaboom (component)
๐Ÿ“ฆ Used in (3)
๐Ÿ“š Prereqs: 1.6
โžก๏ธ Next: 4.1, 3.10
๐Ÿ’ป Minimal (209 chars) โ–ถ Full example (1360 chars)
const ctx = canvas.getContext('2d'); const loop = () => { ctx.clearRect(0,0,w,h); entities.forEach(e => { e.x += e.vx; e.y += e.vy; ctx.fillRect(e.x, e.y, e.w, e.h); }); requestAnimationFrame(loop); }; loop();
// Vanilla Canvas 2D โ€” tycoon grid with dirty-rect (noodle-shop pattern)\nconst canvas = document.getElementById('game');\nconst ctx = canvas.getContext('2d', { alpha: false });\nfunction resize() { const dpr = devicePixelRatio || 1; canvas.width = innerWidth * dpr; canvas.height = innerHeight * dpr; canvas.style.width = innerWidth + 'px'; canvas.style.height = innerHeight + 'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }\nresize(); addEventListener('resize', resize);\nconst customers = []; const grid = Array.from({ length: 8 }, () => Array(8).fill(null));\nlet last = performance.now();\nfunction tick(now) {\n  const dt = Math.min((now - last) / 1000, 0.05); last = now;\n  ctx.clearRect(0, 0, innerWidth, innerHeight);\n  // grid background cached in offscreen, blit once\n  for (const c of customers) { c.x += c.vx * dt; c.timer -= dt; if (c.timer <= 0) customers.splice(customers.indexOf(c), 1); }\n  for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) if (grid[y][x]) { ctx.fillStyle = grid[y][x].color; ctx.fillRect(x * 60, y * 60, 58, 58); }\n  for (const c of customers) { ctx.fillStyle = '#ec4899'; ctx.beginPath(); ctx.arc(c.x, c.y, 18, 0, Math.PI * 2); ctx.fill(); }\n  requestAnimationFrame(tick);\n}\nsetInterval(() => { customers.push({ x: 0, y: Math.random() * innerHeight, vx: 80, timer: 8 }); }, 1500);\nrequestAnimationFrame(tick);
canvas2dvanillano-depเธงเธฒเธ”เน€เธ‚เธตเธขเธ™game-looptycoon
3.4

๐ŸŒ Three.js (Lightweight 3D Renderer)

Intermediateโฑ 3 days

Smallest mainstream 3D renderer (~150KB) โ€” declarative scene graph + WebGL/WebGPU backends โ€” focused on rendering, NOT game features.

๐ŸŽฏ When to use: 3D model viewer (fitcheck โ€” clothes try-on); product configurator; data viz (3D bar chart); AR/VR preview; minimalist portfolio 3D; CSS3DRenderer hybrid UI
โ›” When NOT to use
โ€ข Heavy game needing physics/GUI/particles bundled โ†’ use Babylon (3.1) instead
โ€ข pixel-art 2D โ†’ use Phaser
โ€ข shader-only particle math โ†’ raw WebGL (3.7)
โœ… Pros (6)
โ€ข ~150KB core (~10x smaller than Babylon) โ†’ perfect for fitcheck embed + product configurator first-paint
โ€ข Declarative scene graph โ†’ `scene.add(mesh)` reads like document.appendChild; junior-friendly
โ€ข Massive ecosystem: @three/xr, three-stdlib (OrbitControls, GLTFLoader, EffectComposer) โ€” Babylon equivalent needs custom code
โ€ข r150+ ships WebGPURenderer flag โ€” switch to GPU when navigator.gpu exists, fallback WebGL automatically (fitcheck pattern)
โ€ข BufferGeometry is the only API (Geometry deprecated in r125) โ€” flat Float32Array is cache-friendly and serializable
โ€ข Examples at threejs.org cover 90% of use cases โ€” copy-paste + tweak faster than Babylon docs
โš ๏ธ Cons (6)
โ€ข No physics, no GUI, no particles in core โ†’ must compose 3+ addons (Cannon-es + lil-gui + tsparticles) = 400KB total
โ€ข Materials are thin wrappers over GLSL โ€” PBR with env map = 20 lines setup; Babylon is 3 lines
โ€ข WebXR requires @three/xr with manual session.requestSession + frame loop glue โ€” Babylon does it in 1 helper
โ€ข Inspector = external dep (three-inspect) not built-in โ€” debugging prod scene = print transforms to console
โ€ข Mobile post-processing is brutal โ€” EffectComposer + Bloom tanks to 20fps on iPhone 11; bake into textures
โ€ข TypeScript types are community-maintained โ€” some addon .d.ts files lag releases by weeks
โšก Gotchas (8)
โ€ข renderer.setPixelRatio(Math.min(devicePixelRatio, 2)) โ€” without cap, retina = 4x GPU fillrate = 15fps mobile
โ€ข Use BufferGeometry NOT Geometry โ€” Geometry was removed in r125; migrate legacy code immediately
โ€ข renderer.outputColorSpace = THREE.SRGBColorSpace (r152+) โ€” without it PBR textures wash out / too bright (fitcheck bug)
โ€ข renderer.toneMapping = THREE.ACESFilmicToneMapping for PBR โ€” default linear looks chalky
โ€ข GLTFLoader needs DRACO + KTX2 loaders configured OR .glb inflates 10x โ€” fitcheck preloads DRACO wasm
โ€ข Dispose resources explicitly: geometry.dispose(), material.dispose(), texture.dispose() โ€” otherwise GPU mem leaks 5MB/min
โ€ข WebGLRenderer + ShaderMaterial: don't forget to set uniforms.needsUpdate = true โ€” silent no-op is common
โ€ข Resize handler must update camera.aspect + renderer.setSize โ€” otherwise stretched on rotate
๐Ÿšซ Anti-patterns
โ€ข DON'T use MeshBasicMaterial for outdoor scene โ€” no lighting; use MeshStandardMaterial or MeshPhysicalMaterial (fitcheck rule)
โ€ข DON'T allocate new Vector3() inside render loop โ€” reuse pool of 8-10 vectors; GC pauses cause 5-10ms hitches
โ€ข DON'T load multiple GLTFs sequentially โ€” use GLTFLoader.loadAsync in parallel Promise.all (fitcheck boots 6 models in 2s not 12s)
โ€ข DON'T trust Euler rotation order โ€” default 'XYZ' vs 'YXZ' gives gimbal-lock; set explicitly: rotation.order = 'YXZ' for FPS-style cameras
โ€ข DON'T skip OrbitControls.update() in render loop โ€” damping breaks when not called each frame (fitcheck lesson)
๐Ÿ”€ Alternatives
vs 3.1 Babylon (batteries-included)
vs 3.7 WebGL (raw)
vs 3.8 WebGPU (next-gen)
๐Ÿ“ฆ Used in (1)
๐Ÿ“š Prereqs: 1.1
โžก๏ธ Next: 3.1, 3.7, 6.4
๐Ÿ“– Docs: threejs.org/docs/
๐Ÿ’ป Minimal (369 chars) โ–ถ Full example (1367 chars)
const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, w/h, 0.1, 1000); const mesh = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshStandardMaterial({color:0xec4899})); scene.add(mesh); scene.add(new THREE.DirectionalLight(0xffffff,1)); function tick(){ mesh.rotation.x+=.01; renderer.render(scene,camera); requestAnimationFrame(tick); }
// Three.js r160 โ€” fitcheck GLTF viewer pattern\nimport * as THREE from 'three';\nimport { OrbitControls } from 'three/addons/controls/OrbitControls.js';\nimport { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';\nimport { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';\nconst renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });\nrenderer.setPixelRatio(Math.min(devicePixelRatio, 2));\nrenderer.outputColorSpace = THREE.SRGBColorSpace;\nrenderer.toneMapping = THREE.ACESFilmicToneMapping;\nconst scene = new THREE.Scene();\nscene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 1));\nconst camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 100);\ncamera.position.set(0, 1.6, 3);\nconst controls = new OrbitControls(camera, renderer.domElement);\ncontrols.enableDamping = true;\nconst draco = new DRACOLoader().setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');\nnew GLTFLoader().setDRACOLoader(draco).load('model.glb', (gltf) => {\n  scene.add(gltf.scene);\n  controls.target.copy(gltf.scene.position);\n});\nfunction tick() { controls.update(); renderer.render(scene, camera); requestAnimationFrame(tick); }\naddEventListener('resize', () => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth, innerHeight); });\ntick();
3dthreegltfwebglmodel-viewerเธชเธฒเธกเธกเธดเธ•เธด
3.5

๐Ÿ•น๏ธ Kaboom.js / Kaplay (Arcade 2D)

Beginnerโฑ 4 hours

~80KB component-based arcade 2D โ€” add([rect(), pos(), body()]) chain syntax โ€” game-jam-grade simplicity.

๐ŸŽฏ When to use: Game jam 48h prototypes; tiny arcade (Pong/Snake/Mario-clone under 200 LOC); kids' coding lesson (codekids-thai); quick landing-page game
โ›” When NOT to use
โ€ข Production-grade RPG with save system โ†’ use Phaser (3.2)
โ€ข > 200 entities โ†’ use PixiJS (3.6)
โ€ข commercial launch with SLA โ†’ avoid Kaboom team churn risk
โœ… Pros (6)
โ€ข ~80KB minified โ‰ˆ 1/10 Phaser โ†’ instant load on mobile 3G; perfect for codekids-thai lesson embed
โ€ข Component chain syntax: add([rect(50,50), pos(50,50), color(0,255,0), body(), area(), 'player']) reads like a sentence
โ€ข Built-in scene() with key/state pattern โ†’ 10-line platformer prototype vs 100 in Phaser
โ€ข Zero-config input: keyDown('left', () => ...) โ†’ keyboard + touch auto-detected (codekids tablets)
โ€ข Built-in onUpdate/onDraw lifecycle โ†’ no manual requestAnimationFrame management
โ€ข Re-exported as `kaplay` since 2024 โ€” old `kaboom` still works via shim but migrate to kaplay()
โš ๏ธ Cons (6)
โ€ข Kaplay (formerly Kaboom) had 2 API breaking changes in 18 months โ†’ upgrade tax every quarter
โ€ข No first-class save system โ†’ must roll localStorage wrapper; no migration framework
โ€ข Performance modest past 100 entities โ€” no spatial hash, no GPU batching beyond default sprite atlas
โ€ข TypeScript support is partial โ€” many component types are `any`; IDE autocomplete weak (codekids friction)
โ€ข Community is shrinking vs Phaser โ€” fewer StackOverflow answers, fewer plugins, fewer devs hiring for it
โ€ข No mobile build pipeline (Cordova/Capacitor) maintained โ€” fine for web, dead for native
โšก Gotchas (8)
โ€ข Use `import kaplay from 'kaplay'` NOT `kaboom` โ€” Kaboom renamed 2024, old package is deprecated
โ€ข Component order matters in add([...]) โ€” body() before area() throws 'area not attached'
โ€ข loadSprite('player', 'p.png') must happen inside a scene's onLoad โ€” not at module top-level
โ€ข onUpdate() callback receives deltaTime but it's already dt-multiplied in v3000+ โ€” don't multiply again (silent 2x speed)
โ€ข Touch input on iPad needs touchToMouse polyfill OR scene('main', () => { onTouchStart(... ) }) โ€” auto-detect misses some
โ€ข Pos components store x/y as numbers โ€” for big worlds use camPos()/camScale() for camera, don't translate every entity
โ€ข outline() component needs a parent color() โ€” without it outline default color is black on black = invisible
โ€ข debug.inspect = true in dev shows entity tree but adds 5% CPU โ€” disable in prod
๐Ÿšซ Anti-patterns
โ€ข DON'T use Kaboom for anything beyond 2 weeks of dev โ€” the API churn + community decay will burn you at v3 update (codekids avoided)
โ€ข DON'T nest scenes 3+ deep โ€” Kaplay's scene stack uses global state; bugs at depth 3+ are untraceable
โ€ข DON'T store save state in kaboom's getData()/setData() โ€” those are session-scoped; use localStorage wrapper
โ€ข DON'T use kaboom() global init in module โ€” import kaplay and call const k = kaplay({...}) for testability
โ€ข DON'T mix Kaboom + Phaser โ€” they fight over canvas/context; pick one
๐Ÿ”€ Alternatives
vs 3.2 Phaser (production)
vs 3.3 Vanilla Canvas (no dep)
vs 3.6 PixiJS (render-only)
๐Ÿ“ฆ Used in (1)
๐Ÿ“š Prereqs: 1.1
โžก๏ธ Next: 3.2, 3.3
๐Ÿ“– Docs: kaplayjs.com/
๐Ÿ’ป Minimal (219 chars) โ–ถ Full example (1182 chars)
kaplay(); scene('main', () => { add([rect(50,50), pos(50,50), color(0,255,0), body(), area()]); const p = add([rect(40,40), pos(100,300), color(0,200,255), body(), 'player']); keyDown('left', () => p.move(-200, 0)); });
// Kaplay 3000+ โ€” Pong in 30 LOC (game-jam pattern)\nimport kaplay from 'kaplay';\nconst k = kaplay({ width: 800, height: 600, background: [0, 0, 0] });\nk.scene('main', () => {\n  k.add([k.rect(800, 20), k.pos(0, 0), k.area(), 'wall']);\n  k.add([k.rect(800, 20), k.pos(0, 580), k.area(), 'wall']);\n  const ball = k.add([k.rect(20, 20), k.pos(390, 290), k.area(), k.body({ isStatic: false }), 'ball']);\n  ball.onUpdate(() => { if (ball.pos.x < 0 || ball.pos.x > 780) k.burp(); });\n  const p1 = k.add([k.rect(20, 100), k.pos(30, 250), k.area(), k.body({ isStatic: true }), 'paddle']);\n  const p2 = k.add([k.rect(20, 100), k.pos(750, 250), k.area(), k.body({ isStatic: true }), 'paddle']);\n  k.onKeyDown('w', () => p1.move(0, -300)); k.onKeyDown('s', () => p1.move(0, 300));\n  k.onKeyDown('up', () => p2.move(0, -300)); k.onKeyDown('down', () => p2.move(0, 300));\n  ball.onCollide('paddle', (p) => { if (p.pos.x < 400) ball.dir = k.vec2(1, k.randi(-1, 2)); else ball.dir = k.vec2(-1, k.randi(-1, 2)); });\n  ball.onCollide('wall', () => k.add([k.text('POINT!'), k.pos(400, 300), k.lifespan(1, { fade: 0.5 })]));\n  k.wait(1, () => ball.dir = k.vec2(1, 0));\n});\nk.go('main');
2darcadekaplaykaboomprototypeเน€เธเธกเธ‡เนˆเธฒเธขgame-jam
3.6

๐Ÿ–ผ๏ธ PixiJS (Fastest 2D WebGL Renderer)

Intermediateโฑ 2 days

WebGL-accelerated 2D renderer โ€” fastest sprite batcher in JS โ€” filter library + mesh/particle โ€” render-only (no physics).

๐ŸŽฏ When to use: Performance-critical 2D > 1000 sprites (particle storm, map editor, character creator); isometric MMORPG (realm-of-heroes zoom-out); data-viz dashboard with 5k nodes
โ›” When NOT to use
โ€ข Need built-in physics โ†’ use Phaser (3.2)
โ€ข arcade < 50 entities โ†’ use Kaboom (3.5)
โ€ข pure 2D UI < 200 nodes โ†’ use Canvas 2D (3.3)
โœ… Pros (6)
โ€ข WebGL sprite batching = 5000 sprites at 60fps on mobile; Phaser Canvas renderer tops at ~500
โ€ข Filter library: Blur/ColorMatrix/Displacement built-in โ€” particle glow / CRT effect in 3 lines
โ€ข v8 async init: `await app.init({...})` โ€” clearer than v7 callback chain; tree-shake friendly ESM
โ€ข ParticleContainer for > 100 sprites of same texture โ€” 1 draw call regardless of count (5x faster)
โ€ข Mesh + NineSlicePlane for procedural UI โ€” saves Photoshop round-trips in pixel-perfect HUDs
โ€ข Renderer-agnostic: WebGL, WebGPU (v8 alpha), Canvas โ€” future-proof your 2D investment
โš ๏ธ Cons (6)
โ€ข Render-only โ€” no scene management, no physics, no input handling (must add EventEmitter separately)
โ€ข ~400KB minified โ†’ bigger than Phaser MINIMAL but faster; trade size for FPS
โ€ข v8 API is async (await app.init) โ€” migration from v7 sync API = lots of await refactors
โ€ข No game loop helper โ€” must wire your own ticker (gsap, raf, or own loop); Phaser ships one
โ€ข Filter pipeline = 1 GPU pass per filter; stacking 3+ filters tanks to 20fps on iPhone 11
โ€ข Texture atlas must be in specific format (Pixi v8 prefers Spine-style JSON, not TexturePacker default)
โšก Gotchas (8)
โ€ข v8 (2024) is async โ€” `await app.init({width,height})` then `document.body.appendChild(app.canvas)`; v7 sync is GONE
โ€ข Use ParticleContainer (not Container) for > 100 sprites of same base texture โ€” single draw call vs 100
โ€ข app.canvas has 2x devicePixelRatio already โ€” set CSS width/height in half-pixel units to avoid blur
โ€ข Filters stack = each filter = 1 framebuffer ping-pong; cache filter result in RenderTexture if static
โ€ข Texture.from('url') caches by URL โ€” clear with Texture.removeFromCache on memory warning
โ€ข InteractionManager (v7) โ†’ FederatedEvents (v8) โ€” on('pointerdown') API changed; migration = search/replace
โ€ข Bundle size: pixi.js (full) is 400KB; @pixi/sprite + @pixi/app is 200KB โ€” choose per use case
โ€ข WebGL context loss on Android Chrome tab background โ†’ listen to app.renderer.on('contextlost') and rebuild textures
๐Ÿšซ Anti-patterns
โ€ข DON'T put 10000 individual Sprites in a Container โ€” wrap in ParticleContainer or batch by texture for 1 draw call
โ€ข DON'T mix PIXI.Text + HTML <input> in same layout โ€” different sizing model; use Pixi v8 Text or full DOM
โ€ข DON'T apply Filter to entire stage โ€” apply only to specific sprites; full-stage filter is GPU bottleneck
โ€ข DON'T resize canvas via CSS only โ€” must also call app.renderer.resize(w,h) to update buffer (v8) or canvas.width/height (v7)
โ€ข DON'T forget app.destroy() on unmount โ€” Pixi leaks WebGL context + GPU buffers; React useEffect cleanup mandatory
๐Ÿ”€ Alternatives
vs 3.2 Phaser (physics included)
vs 3.3 Vanilla Canvas (no dep)
vs 3.5 Kaboom (simpler)
๐Ÿ“ฆ Used in (1)
๐Ÿ“š Prereqs: 1.1
โžก๏ธ Next: 3.2, 3.7
๐Ÿ“– Docs: pixijs.com/docs
๐Ÿ’ป Minimal (227 chars) โ–ถ Full example (1225 chars)
const app = new PIXI.Application(); await app.init({width:800,height:600,antialias:true}); const sprite = PIXI.Sprite.from('p.png'); sprite.x=400; sprite.y=300; app.stage.addChild(sprite); document.body.appendChild(app.canvas);
// PixiJS v8 โ€” 5000 sprite particle storm (realm-of-heroes effect)\nimport { Application, Sprite, Texture, Container } from 'pixi.js';\nconst app = new Application();\nawait app.init({ width: window.innerWidth, height: window.innerHeight, antialias: true, backgroundAlpha: 0 });\ndocument.body.appendChild(app.canvas);\nconst tex = await Texture.from('https://codehub.sj88ai.com/static/sparkle.png');\nconst particles = new Container();\napp.stage.addChild(particles);\nconst sprites = [];\nfor (let i = 0; i < 5000; i++) {\n  const s = new Sprite(tex);\n  s.anchor.set(0.5);\n  s.x = Math.random() * app.screen.width;\n  s.y = Math.random() * app.screen.height;\n  s.tint = Math.random() * 0xffffff;\n  s.alpha = Math.random();\n  s.scale.set(0.2 + Math.random() * 0.4);\n  s.vx = (Math.random() - 0.5) * 2;\n  s.vy = (Math.random() - 0.5) * 2;\n  sprites.push(s);\n  particles.addChild(s);\n}\napp.ticker.add((ticker) => {\n  const dt = ticker.deltaMS / 16.67;\n  for (const s of sprites) { s.x += s.vx * dt; s.y += s.vy * dt; if (s.x < 0 || s.x > app.screen.width) s.vx *= -1; if (s.y < 0 || s.y > app.screen.height) s.vy *= -1; }\n});\nwindow.addEventListener('beforeunload', () => app.destroy(true, { children: true }));
2dpixijswebglspriteparticlerender-onlyเน€เธฃเน‡เธง
3.7

๐ŸŽ† WebGL / WebGL2 (Custom GLSL Shaders)

Advancedโฑ 2 weeks

Raw GPU API โ€” hand-write GLSL vertex/fragment shaders โ€” max performance for particle storms, fluid sim, post-FX โ€” no engine.

๐ŸŽฏ When to use: 10k+ particles with physics-on-GPU; custom fluid/water/fire shader; post-process (bloom/DoF/SSR); audio visualizer; shadertoy-style art
โ›” When NOT to use
โ€ข Generic 3D scene โ†’ use Three.js (3.4) or Babylon (3.1)
โ€ข need physics+scene-graph โ†’ don't reinvent
โ€ข 2D โ†’ Canvas 2D or PixiJS
โœ… Pros (6)
โ€ข GPU compute (WebGL2 transform feedback / WebGPU compute) โ€” 100k particles updated in <1ms on RTX 2060
โ€ข Full creative control โ€” fragment shaders can paint anything: raymarching, signed-distance fields, NPR stylization
โ€ข WebGL 2 in 96% browsers (2024) โ€” VS WebGPU 18%; WebGL2 is today's safe low-level target
โ€ข GLSL ES 3.00 vs WebGL1 1.00 โ€” uniform blocks, instanced drawing, texelFetch โ€” modern features available
โ€ข ShaderToy migration is 1:1 โ€” copy any .glsl shader, paste into <script id='vs'>, compile, render to fullscreen quad
โ€ข Three.js ShaderMaterial = on-ramp: use Three's renderer + camera + raw ShaderMaterial โ†’ keep conveniences, drop into GLSL when needed
โš ๏ธ Cons (6)
โ€ข GLSL is a real language โ€” uniforms, varyings, attributes, precision qualifiers โ€” 2-week ramp for non-graphics dev
โ€ข Debugging is brutal โ€” no breakpoints; print uniforms as color and read pixels; Spector.js / RenderDoc are external
โ€ข Context loss handling mandatory โ€” Android Chrome tab-switch drops context; must rebuild all shaders
โ€ข No scene graph โ€” you build VAO/VBO/uniform setup; 500 LOC for what Three does in 5
โ€ข Mobile GPU varies wildly โ€” Adreno 640 vs Apple A14 = 10x perf gap; must profile on real devices
โ€ข WebGL doesn't multithread โ€” heavy CPU physics + WebGL render = jank; move physics to worker (sims-lite pattern)
โšก Gotchas (8)
โ€ข WebGL2 = 96% browsers, WebGL1 = 99% โ€” for new code use WebGL2 (`canvas.getContext('webgl2')`) but feature-detect fallback (Three.js does this)
โ€ข Context loss: listen `canvas.addEventListener('webglcontextlost', e => { e.preventDefault(); rebuild(); })` โ€” must preventDefault to recover
โ€ข Precision qualifiers matter: mediump in fragment shader is fast but 16-bit float; highp required for positions, lowp for color tints
โ€ข Don't allocate textures per frame โ€” pool or use single dynamic texture with texSubImage2D updates
โ€ข Uniform location lookups are slow โ€” `gl.getUniformLocation(p, 'u')` once at boot, cache the location; per-frame lookup = 5ms tax
โ€ข Instanced drawing (gl.drawArraysInstanced) โ€” render 10000 cubes in 1 draw call instead of 10000; Three's instancedMesh wraps this
โ€ข Use float textures (EXT_color_buffer_float) for post-process ping-pong โ€” required for HDR bloom in WebGL2
โ€ข Shader compile is async โ€” call `gl.getShaderParameter(s, gl.COMPILE_STATUS)` and check `gl.getShaderInfoLog(s)` for errors; silent fail = black screen
๐Ÿšซ Anti-patterns
โ€ข DON'T write vertex shader from scratch for triangle mesh โ€” Three.js BufferGeometry + ShaderMaterial gives you the boilerplate; raw WebGL only when at GPU limits
โ€ข DON'T use `vec3` for colors in fragment shader โ€” use `vec4` with alpha for blending; vec3 alpha defaults are wrong
โ€ข DON'T skip `gl.flush()` after multiple draw calls if reading pixels back โ€” driver may reorder; explicit barrier for readPixels
โ€ข DON'T enable blending if you don't need it โ€” `gl.disable(gl.BLEND)` saves 30% GPU on opaque-only scenes
โ€ข DON'T forget to set `gl.viewport(0, 0, canvas.width, canvas.height)` on resize โ€” without it renders stretch on HiDPI
๐Ÿ”€ Alternatives
vs 3.4 Three.js (engine wrapper)
vs 3.8 WebGPU (next-gen)
vs 3.1 Babylon (full game)
๐Ÿ“ฆ Used in (2)
๐Ÿ“š Prereqs: 1.6
โžก๏ธ Next: 3.1, 3.4, 3.8
๐Ÿ’ป Minimal (230 chars) โ–ถ Full example (1863 chars)
const gl = canvas.getContext('webgl2'); const prog = gl.createProgram(); const vs = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vs, '#version 300 es\nin vec2 a; void main(){gl_Position=vec4(a,0,1);}'); gl.compileShader(vs);
// WebGL2 โ€” fullscreen quad with fragment shader (ShaderToy pattern)\nconst canvas = document.getElementById('c');\nconst gl = canvas.getContext('webgl2', { antialias: true });\ncanvas.width = innerWidth * devicePixelRatio;\ncanvas.height = innerHeight * devicePixelRatio;\nfunction compile(type, src) {\n  const sh = gl.createShader(type);\n  gl.shaderSource(sh, src);\n  gl.compileShader(sh);\n  if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(sh)); throw new Error('shader fail'); }\n  return sh;\n}\nconst vs = compile(gl.VERTEX_SHADER, `#version 300 es\nin vec2 a; void main(){gl_Position=vec4(a,0,1);}`);\nconst fs = compile(gl.FRAGMENT_SHADER, `#version 300 es\nprecision highp float;\nuniform vec2 uRes;\nuniform float uTime;\nout vec4 o;\nvoid main(){ vec2 uv = (gl_FragCoord.xy*2.0 - uRes)/min(uRes.x,uRes.y); float t = uTime*0.5; float d = length(uv - 0.5*vec2(cos(t),sin(t))); o = vec4(0.5+0.5*cos(d*8.0+vec3(0,2,4)+t), 1.0); }`);\nconst prog = gl.createProgram();\ngl.attachShader(prog, vs); gl.attachShader(prog, fs); gl.linkProgram(prog); gl.useProgram(prog);\nconst buf = gl.createBuffer();\ngl.bindBuffer(gl.ARRAY_BUFFER, buf);\ngl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);\nconst loc = gl.getAttribLocation(prog, 'a');\ngl.enableVertexAttribArray(loc);\ngl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);\nconst uRes = gl.getUniformLocation(prog, 'uRes');\nconst uT = gl.getUniformLocation(prog, 'uTime');\ncanvas.addEventListener('webglcontextlost', e => { e.preventDefault(); console.warn('context lost'); });\nfunction tick(t) { gl.viewport(0, 0, canvas.width, canvas.height); gl.uniform2f(uRes, canvas.width, canvas.height); gl.uniform1f(uT, t/1000); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); requestAnimationFrame(tick); }\nrequestAnimationFrame(tick);
webglshaderglslgpuparticleเน€เธฃเธ™เน€เธ”เธญเธฃเนŒ
3.8

โšก WebGPU (Next-Gen GPU API)

Advancedโฑ 3 weeks

WebGL successor โ€” compute shaders on web, render pipelines, multi-thread via async โ€” 2024 stable in Chrome/Edge, partial in Safari/Firefox.

๐ŸŽฏ When to use: Compute-heavy: ML inference in browser; raytracing preview; physics-on-GPU (1M particles); large terrain LOD streaming; shader-heavy NFT art
โ›” When NOT to use
โ€ข Mainstream shipping game (Safari/Firefox support still partial in 2024) โ€” fallback to WebGL or detect via navigator.gpu
โ€ข mobile low-end = no GPU adapter
โœ… Pros (6)
โ€ข Compute shaders = real GPGPU on web โ€” 1M particle sim at 60fps on M1 MacBook; WebGL needs transform feedback dance
โ€ข Render pipeline objects pre-compiled โ€” switching shader = 0ms vs WebGL's re-link cost (5-15ms)
โ€ข Bind groups = explicit resource layout โ€” GPU drivers don't have to guess; faster + safer
โ€ข Async by design โ€” adapter/device init returns Promise; no more blocking gl.flush() in worker threads
โ€ข WGSL = real typed language โ€” no GLSL precision footguns; tooling (naga) validates at compile
โ€ข Three.js r150+ has WebGPURenderer (experimental) โ€” try with `await renderer.init()` and same scene graph as WebGL
โš ๏ธ Cons (6)
โ€ข Browser support 2024: Chrome 113+/Edge โœ“; Safari TP โœ“; Firefox behind flag โ†’ production = double-maintain WebGL fallback
โ€ข API surface 3x WebGL โ€” bind groups, pipeline layouts, queue, command encoder โ€” steep ramp 3+ weeks
โ€ข WGSL still has churn (v0.10 in 2023 โ†’ v0.13 in 2024) โ€” port code between versions; no v1.0 yet
โ€ข Compute shader debugging = Spector.js only; no shader-level breakpoints; black screen = infinite loop risk
โ€ข Mobile GPU drivers less mature than WebGL โ€” Adreno/Mali sometimes return wrong results; ship CPU fallback
โ€ข Documentation still sparse vs WebGL โ€” Mozilla MDN WebGPU pages added 2024 but community answers 10x fewer than WebGL
โšก Gotchas (8)
โ€ข Feature-detect: `if (!navigator.gpu) return fallbackToWebGL()` โ€” never assume; on iOS Safari 17.4+ TP only
โ€ข requestAdapter() can return null on low-end Android โ€” check, then requestDevice(); null device = no GPU
โ€ข Bind groups must match @group(N) in WGSL exactly โ€” order matters; mismatch = validation error + silent fail
โ€ข Queue writes are batched โ€” multiple writeBuffer + 1 submit() = best perf; per-frame submit = driver overload
โ€ข Texture format matters: 'bgra8unorm' on Apple Silicon vs 'rgba8unorm' on AMD โ€” query preferredCanvasFormat
โ€ข Compute shader workgroup size is a multiple of 64 โ€” pass @workgroup_size(64); use numthreads(64,1,1) in HLSL-translated WGSL
โ€ข Async pipeline creation = first frame after compile is slow (100ms+) โ€” precompile at scene init or loading screen
โ€ข Lost device (adapter too hot) โ€” listen to device.lost.then(info => recreateDevice(info)); WebGL doesn't have this lifecycle
๐Ÿšซ Anti-patterns
โ€ข DON'T assume WebGPU available โ€” always ship WebGL fallback (or Babylon/Three handles it); iOS users = Safari = broken in 2024
โ€ข DON'T create one big bind group for whole scene โ€” split per logical resource (camera, lights, materials); bind groups are cheap
โ€ข DON'T submit command encoder per object โ€” batch all draws into 1 encoder pass; submit() = GPU sync barrier
โ€ข DON'T use WGSL f32 for indices โ€” use u32; f32 index buffer = validation error + driver panic
โ€ข DON'T load compute shader from string at runtime โ€” precompile to .wgsl binary at build step; runtime parse = 200ms hitch
๐Ÿ”€ Alternatives
vs 3.7 WebGL (universal)
vs 3.1 Babylon (engines WebGPU flag)
๐Ÿ“š Prereqs: 3.7
โžก๏ธ Next: 3.1
๐Ÿ’ป Minimal (259 chars) โ–ถ Full example (2146 chars)
const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); const module = device.createShaderModule({ code: '@vertex fn vs(@builtin(vertex_index) i:u32)->@builtin(position) vec4<f32> { return vec4<f32>(0,0,0,1); }' });
// WebGPU โ€” compute-shader 100k particle sim (next-gen pattern)\nif (!navigator.gpu) { console.warn('WebGPU unavailable, fallback'); return; }\nconst adapter = await navigator.gpu.requestAdapter();\nconst device = await adapter.requestDevice();\nconst canvas = document.getElementById('c');\nconst ctx = canvas.getContext('webgpu');\nconst format = navigator.gpu.getPreferredCanvasFormat();\nctx.configure({ device, format, alphaMode: 'premultiplied' });\nconst module = device.createShaderModule({\n  code: `struct Particle { pos: vec2<f32>, vel: vec2<f32> };\n@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;\n@compute @workgroup_size(64) fn update(@builtin(global_invocation_id) gid: vec3<u32>) {\n  let i = gid.x; if (i >= arrayLength(&particles)) { return; }\n  particles[i].pos = particles[i].pos + particles[i].vel * 0.016;\n  if (particles[i].pos.x > 1.0 || particles[i].pos.x < -1.0) { particles[i].vel.x = -particles[i].vel.x; }\n  if (particles[i].pos.y > 1.0 || particles[i].pos.y < -1.0) { particles[i].vel.y = -particles[i].vel.y; }\n}`\n});\nconst N = 100000;\nconst particleData = new Float32Array(N * 4);\nfor (let i = 0; i < N * 4; i += 4) { particleData[i] = (Math.random() - 0.5) * 2; particleData[i+1] = (Math.random() - 0.5) * 2; particleData[i+2] = (Math.random() - 0.5) * 0.02; particleData[i+3] = (Math.random() - 0.5) * 0.02; }\nconst buf = device.createBuffer({ size: N * 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });\ndevice.queue.writeBuffer(buf, 0, particleData);\nconst pipeline = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'update' } });\nconst bindGroup = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: buf } }] });\nasync function tick() {\n  const enc = device.createCommandEncoder();\n  const pass = enc.beginComputePass(); pass.setPipeline(pipeline); pass.setBindGroup(0, bindGroup); pass.dispatchWorkgroups(Math.ceil(N / 64)); pass.end();\n  device.queue.submit([enc.finish()]);\n  await device.queue.onSubmittedWorkDone();\n  requestAnimationFrame(tick);\n}\ntick();
webgpucomputewgslgpunext-genเน€เธฃเน‡เธง
3.9

๐ŸŒ WebSocket Multiplayer (Realtime 2-Way)

Advancedโฑ 3 days

Bidirectional TCP-over-HTTP socket โ€” server required โ€” low-latency realtime state sync for multiplayer games, collab editors, chat.

๐ŸŽฏ When to use: MMORPG (realm-of-heroes); realtime collab whiteboard; co-op tycoon; trading card game; spectator chat; live leaderboard; turn-based battle server-authoritative
โ›” When NOT to use
โ€ข One-shot request โ†’ use fetch/AJAX
โ€ข < 5 players no realtime need โ†’ use long-poll
โ€ข P2P voice/video โ†’ use WebRTC
โ€ข broadcast 100k clients โ†’ use SSE or MQTT
โœ… Pros (6)
โ€ข Full-duplex persistent connection โ€” server can push anytime (chat message, monster spawn); HTTP request/response can't
โ€ข Sub-100ms latency on same-region โ€” feels instant for click-to-action in realm-of-heroes PvP
โ€ข Single TCP socket multiplexes many logical channels (combat/chat/inventory) โ€” one auth, many features
โ€ข Standard browser API โ€” no library needed for basic client; battle-tested for 20+ years
โ€ข WebSocket Secure (wss://) = same TLS as HTTPS โ€” works through corporate proxies that block UDP
โ€ข Server can be Node.js / Python / Go / Elixir โ€” pick your stack; spec is implementation-agnostic
โš ๏ธ Cons (6)
โ€ข Server required โ€” minimum Node ws library OR paid service (Pusher/Ably); DevOps cost
โ€ข State sync is YOUR problem โ€” engine doesn't auto-reconcile; client A says 'I moved', client B says 'I'm at', who wins = server arbitration
โ€ข Connection drops require reconnect + state resync โ€” handle 3G subway tunnels gracefully (mobile game rule)
โ€ข No built-in auth โ€” token in first message, validate server-side; never trust client messages (cheating)
โ€ข WebSocket frames have overhead โ€” for chat text, SSE may be simpler; for binary video, WebRTC
โ€ข Scale: 10k concurrent sockets per server is easy; 100k needs Redis pub/sub + sticky sessions (CF/Traefik)
โšก Gotchas (8)
โ€ข Reconnect with exponential backoff: `setTimeout(reconnect, Math.min(30000, 1000 * 2**attempt))` โ€” never re-attempt fixed-interval (DOSes server when it comes back up)
โ€ข Heartbeat ping/pong every 25s โ€” many proxies (Cloudflare, AWS ALB) drop idle connections at 30-60s; pong frame keeps them alive
โ€ข JSON.stringify every message is 10x slower than binary โ€” for high-frequency updates (60fps positions) use MessagePack or Protobuf
โ€ข Send a `seq` number per outbound message โ€” server echos; client drops out-of-order packets; otherwise race conditions on state
โ€ข On reconnect, send `lastSeq` to server; server replays missed events; OR full state-snapshot if `lastSeq < server.bufferStart`
โ€ข WebRTC for true P2P (no server) is different API โ€” use for voice; WebSocket for game state; don't mix
โ€ข iOS Safari 14 background โ†’ WS closes after 30s โ€” listen for visibilitychange + force reconnect on visible
โ€ข Don't put WebSocket in main bundle โ€” code-split via dynamic import(); saves 30KB initial JS for users who never join multiplayer
๐Ÿšซ Anti-patterns
โ€ข DON'T trust client positions โ€” server must validate move speed/collision; otherwise realm-of-heroes PvP = teleport hack
โ€ข DON'T broadcast all messages to all players โ€” use rooms/channels; realm-of-heroes area-based subscribe = 90% bandwidth saved
โ€ข DON'T do heavy computation on every WS message โ€” batch process every 16ms (60fps) tick; per-message handler = CPU spike
โ€ข DON'T forget to handle 'binary' message type โ€” ws.onmessage event has .data as Blob or ArrayBuffer; decode based on typeof
โ€ข DON'T use ws.send() in render loop without throttling โ€” at 60fps you flood socket; aggregate 50ms snapshots
๐Ÿ”€ Alternatives
vs 9.7 Fetch/AJAX (one-shot)
vs WebRTC (P2P)
vs SSE (server-push only)
๐Ÿ“ฆ Used in (1)
๐Ÿ“š Prereqs: 1.7, 9.7
โžก๏ธ Next: 9.2
๐Ÿ’ป Minimal (299 chars) โ–ถ Full example (1466 chars)
class GameSocket { connect() { this.ws = new WebSocket(this.url); this.ws.onclose = () => setTimeout(() => this.connect(), Math.min(30000, this.reconnectDelay *= 2)); this.ws.onmessage = e => this.handle(JSON.parse(e.data)); } send(msg) { this.ws.send(JSON.stringify({...msg, seq: ++this.seq})); } }
// WebSocket โ€” battle-tested reconnecting client (realm-of-heroes pattern)\nclass GameSocket {\n  constructor(url, token) { this.url = url; this.token = token; this.seq = 0; this.lastSeq = 0; this.queue = []; this.handlers = new Map(); }\n  on(type, fn) { (this.handlers.get(type) || this.handlers.set(type, []).get(type)).push(fn); }\n  connect() {\n    this.ws = new WebSocket(`${this.url}?token=${this.token}`);\n    this.ws.onopen = () => { this.attempt = 0; this.flush(); this.heartbeat = setInterval(() => this.ws.readyState === 1 && this.ws.send('ping'), 25000); };\n    this.ws.onmessage = (e) => { if (e.data === 'pong') return; const msg = JSON.parse(e.data); if (msg.seq <= this.lastSeq) return; this.lastSeq = msg.seq; (this.handlers.get(msg.type) || []).forEach(fn => fn(msg)); };\n    this.ws.onclose = () => { clearInterval(this.heartbeat); this.attempt = (this.attempt || 0) + 1; setTimeout(() => this.connect(), Math.min(30000, 500 * 2 ** this.attempt)); };\n    this.ws.onerror = () => this.ws.close();\n  }\n  send(msg) { const payload = JSON.stringify({ ...msg, seq: ++this.seq }); this.ws.readyState === 1 ? this.ws.send(payload) : this.queue.push(payload); }\n  flush() { while (this.queue.length) this.ws.send(this.queue.shift()); }\n}\nconst sock = new GameSocket('wss://realm.sj88ai.com/ws', localStorage.token);\nsock.on('player_move', m => updatePlayer(m.id, m.x, m.y));\nsock.on('chat', m => addChatLine(m.user, m.text));\nsock.connect();
websocketmultiplayerrealtimenetworkเน€เธ™เน‡เธ•เน€เธงเธดเธฃเนŒเธ„socket
3.10

๐Ÿ”„ Game State Machine (FSM)

Intermediateโฑ 2 hours

Finite State Machine โ€” entity has 1 state at a time (IDLE/WALK/ATTACK) โ€” guards on transitions prevent invalid combos (e.g., JUMPโ†’JUMP without landing).

๐ŸŽฏ When to use: Entity with > 3 behaviors (NPC villager, enemy AI, player combat); UI flow (loadingโ†’menuโ†’gameโ†’pauseโ†’gameover); boss phase manager; AI reactions; matchmaking states
โ›” When NOT to use
โ€ข Static puzzle (no state changes)
โ€ข single-action entities (bullet = one lifetime)
โ€ข parallel behaviors โ†’ use Hierarchical FSM or Behavior Tree (4.4)
โœ… Pros (6)
โ€ข States + transitions = visual diagram โ†’ stronghold-3d villagers: IDLEโ†’WANDERโ†’WORKโ†’SLEEP all in one PNG
โ€ข Guards prevent invalid combos (JUMP only when onGround) โ€” bug class 'attack while dead' impossible by design
โ€ข onEnter/onExit hooks = clean lifecycle โ€” play anim once on ATTACK enter, stop sound on DEATH exit
โ€ข Trivial to debug โ€” log state transitions; replay shows exactly when AI broke (sims-lite NPC debugging)
โ€ข Maps directly to Unity Mecanim / Unreal StateTree โ€” port to native engines is 1:1
โ€ข String state IDs survive JSON save/load โ€” int enums break if you reorder (street-fighter-thai rule)
โš ๏ธ Cons (6)
โ€ข Parallel states need Hierarchical FSM (HFSM) โ€” basic FSM can't express 'WALK while CARRYING while HUNGRY' simultaneously
โ€ข Boilerplate per state โ€” 5 states ร— {enter, exit, update, transition} = 20 funcs; behavior tree collapses some
โ€ข Transitions can grow โ€” 5x5 matrix = 25 cells; many are 'never'; still need to declare to fail-safe
โ€ข No memory of past states โ€” 'just attacked 3 times in a row' needs state counter on top of FSM
โ€ข String state IDs typo-prone โ€” `setState('ATTAK')` silently fails if not guarded by Set check
โ€ข Doesn't model goal-directed AI well โ€” 'go find food' needs BT planner on top; FSM is reactive not proactive
โšก Gotchas (8)
โ€ข Use string IDs not numeric enums โ€” `STATES.IDLE = 'idle'`. Numeric enums reorder when JSON load gives wrong value (street-fighter-thai lesson)
โ€ข Guard functions return boolean: `{ idle: { walk: () => true, attack: m => m.inRange && m.stamina > 0 } }` โ€” guards checked in order
โ€ข Map<state, transitions> not object literal โ€” Map preserves insertion order across browsers; {} order is spec-but-implementation-leaky
โ€ข Pre-condition check inside guard, NOT inside setState โ€” setState should be dumb; guards do the validation
โ€ข onExit called BEFORE onEnter of new state โ€” order matters for anim cleanup (sims-lite: stop walk anim before start attack anim)
โ€ข Don't transition to same state โ€” `if (next === this.current) return`; otherwise onEnter fires twice causing anim restart glitch
โ€ข State machine for UI screens is global singleton, not per-entity โ€” `class GameState { state = 'menu' }` lives at root
โ€ข Save/load: serialize current state ID + counters; on resume, force onEnter to re-init timers (don't trust restored timer values)
๐Ÿšซ Anti-patterns
โ€ข DON'T use numeric enums for state โ€” `STATES.IDLE = 0`; reorder JSON = wrong state. Use string literals
โ€ข DON'T mutate current state directly โ€” always go through setState(next); otherwise onEnter/onExit skipped, anim desync
โ€ข DON'T put data inside state object โ€” state ID is just a string; counters/inventory live in entity data (separation of concerns)
โ€ข DON'T nest 5+ levels of FSM โ€” flatten with sub-states or switch to Behavior Tree; deep nesting = spaghetti
โ€ข DON'T forget to handle 'interrupted' transitions โ€” if AI was WALK and you set to ATTACK, the WALK onExit must reset pathfinding
๐Ÿ”€ Alternatives
vs 4.4 Behavior Tree (more expressive)
vs 4.2 ECS (data-oriented)
vs 1.3 Module Pattern (state holder)
๐Ÿ“š Prereqs: 1.3
โžก๏ธ Next: 4.4, 4.7
๐Ÿ’ป Minimal (216 chars) โ–ถ Full example (1690 chars)
const STATES = {IDLE:'idle',WALK:'walk',ATTACK:'attack'}; const T = {idle:{walk:()=>true}, walk:{idle:()=>Math.random()<.1, attack:m=>m.targetInRange}}; class FSM { setState(next) { /* guard + onEnter + onExit */ } }
// FSM โ€” NPC villager (stronghold-3d pattern)\nconst STATES = { IDLE: 'idle', WANDER: 'wander', WORK: 'work', SLEEP: 'sleep' };\nconst TRANSITIONS = {\n  idle: { wander: () => true },\n  wander: { idle: (v) => v.energy < 20 || v.arrived, work: (v) => v.energy > 50 && v.shift === 'day', sleep: (v) => v.time === 'night' },\n  work: { idle: (v) => v.taskDone, sleep: (v) => v.time === 'night' },\n  sleep: { idle: (v) => v.time === 'day' && v.energy > 80 },\n};\nclass FSM {\n  constructor(entity) { this.entity = entity; this.current = STATES.IDLE; this.handlers = new Map(); }\n  on(state, evt, fn) { const k = `${state}:${evt}`; if (!this.handlers.has(k)) this.handlers.set(k, []); this.handlers.get(k).push(fn); }\n  setState(next, ctx = {}) {\n    if (next === this.current) return;\n    const guard = TRANSITIONS[this.current]?.[next];\n    if (guard && !guard(this.entity)) return;\n    const exitHandlers = this.handlers.get(`${this.current}:exit`) || [];\n    exitHandlers.forEach(fn => fn(this.entity, ctx));\n    const prev = this.current; this.current = next;\n    const enterHandlers = this.handlers.get(`${next}:enter`) || [];\n    enterHandlers.forEach(fn => fn(this.entity, ctx, prev));\n  }\n}\nconst v = { energy: 50, time: 'day', shift: 'day', arrived: false, taskDone: false };\nconst fsm = new FSM(v);\nfsm.on('wander', 'enter', (v) => { v.target = randomNearbyTile(); });\nfsm.on('work', 'enter', (v) => { v.job = pickJob(v); });\nfsm.on('sleep', 'enter', (v) => { v.bed = findBed(v); });\nfsm.setState('wander'); // enters wander\nfsm.setState('work'); // guard: energy>50 && shift=day โ†’ ok\nfsm.setState('attack'); // guard fails: no transition from 'work' โ†’ rejected
fsmstateaientitystate-machineเธชเธ–เธฒเธ™เธฐ

๐Ÿค– AI-Readable JSON for this phase

All 10 sub-phases of Game Engines as JSON โ€” perfect for AI agents.

{
  "phase": {
    "id": 3,
    "name": "Game Engines",
    "icon": "๐ŸŽฎ",
    "tagline": "Pick the right engine, save 50%+ time",
    "color": "#fbbf24",
    "slug": "game-engines"
  },
  "sub_phases": [
    {
      "id": "3.1",
      "title": "Babylon.js (Full-Stack 3D Engine)",
      "icon": "๐ŸงŠ",
      "description": "Full 3D engine โ€” PBR + Havok physics + particles + GUI + WebXR โ€” 1.5MB dep replaces 4 Three addons.",
      "when_to_use": "3D city builder (stronghold-3d); NPC sim (sims-lite); FPS (fps-cops-vs-robbers); MMORPG (realm-of-heroes); Mars strategy (mars-colony); idle RPG (sj88-rpg-idle-3d)",
      "when_not_to_use": "Tight 3D model viewer <300KB โ†’ use Three.js (3.4); pure 2D โ†’ use Phaser (3.2); shader-only particle system โ†’ use raw WebGL (3.7)",
      "pros": [
        "PBR + Havok + particle editor + GUI + post-FX in 1.5MB โ†’ zero glue vs Three where you'd compose 3 addons",
        "Playground at playground.babylonjs.com = live-edit IDE โ†’ share bug-repro URL in 30s (no JSFiddle setup)",
        "thinInstanceAdd draws 10000 identical cubes in 1 draw call โ†’ stronghold-3d tiles 100x100 grid at 60fps",
        "WebXR first-class โ€” fps-cops-vs-robbers ships VR mode in 50 lines vs Three's @three/xr + manual session glue",
        "ArcRotateCamera + Inspector baked-in โ†’ no need to wire OrbitControls or dat.gui separately",
        "v8 (2024) ships Havok IK for character animation โ†’ no three-stdlib AnimationMixer ladder"
      ],
      "cons": [
        "1.5MB+ minified โ‰ˆ 10x Three โ†’ mobile first-paint hurts if not code-split per route",
        "API surface ~5x Three โ†’ ramp-up 1 week vs 3 days; niche questions get fewer SO answers",
        "PBR + shadows = thermal throttling on iPhone 11 / low-end Android โ†’ must swap to StandardMaterial",
        "Inspector = +200KB; forgetting tree-shake ships it in prod and slows boot 300ms",
        "BabylonGUI 2D โ‰  DOM โ†’ no screen-reader / IME for Thai-Arabic; mix HTML overlay for inputs",
        "Havok v2 = WASM 3MB lazy-loaded โ†’ first physics step shows 100ms hitch unless preloaded"
      ],
      "gotchas": [
        "freezeWorldMatrix() after placing static meshes โ€” without it Babylon recomputes world transforms each frame costing 5-15ms on 1000-mesh scenes (stronghold-3d lesson)",
        "thinInstanceAdd (NOT createInstance) for > 100 identical objects โ€” instance API spawns N draw calls, thin packs into 1 buffer + matrix refresh",
        "ArcRotateCamera alpha/beta are radians; setting alpha=Math.PI flips 180ยฐ not 360ยฐ โ€” use lerp(t,0,1) for smooth tween",
        "PBR materials need .env cubemap โ€” without it metallic surfaces render black; load via CubeTexture from .dds",
        "Havok WASM needs await HavokPhysics() before first PhysicsAggregate โ€” otherwise physics silently no-ops",
        "PointerEvent fires on both canvas AND DOM HUD โ†’ wire scene.onPointerObservable to avoid double-input in realm-of-heroes",
        "Calling scene.render() manually inside runRenderLoop = double-render โ†’ 30fps instead of 60fps; let engine own the loop",
        "iOS Safari drops WebGL context after 30min background โ†’ listen to engine.onContextLostObservable and rebuild scene"
      ],
      "anti_patterns": [
        "DON'T `import * as BABYLON` in TS โ€” use named imports `import { Engine, Scene } from '@babylonjs/core'` tree-shakes ~30%",
        "DON'T allocate Mesh per bullet โ€” pool 50 meshes; stronghold-3d reuses them across 10k spawns",
        "DON'T run physics in JS thread for > 200 bodies โ€” enable Havok WASM (sims-lite NPC crowds)",
        "DON'T use StandardMaterial outdoor โ€” PBR + IBL only; Standard gives plastic look in sunlight",
        "DON'T skip scene.skipPointerMovePicking = true with 1000+ pickables โ€” picking = 5ms hit-test per frame"
      ],
      "prereqs": [
        "1.1",
        "1.6"
      ],
      "next_steps": [
        "3.10",
        "4.1",
        "4.4"
      ],
      "examples": [
        "stronghold-3d",
        "sims-lite",
        "mars-colony",
        "realm-of-heroes",
        "fps-cops-vs-robbers"
      ],
      "snippet": "const engine = new BABYLON.Engine(canvas, true); const scene = new BABYLON.Scene(engine); scene.createDefaultCameraOrLight(); BABYLON.MeshBuilder.CreateBox('b',{size:1},scene); engine.runRenderLoop(()=>scene.render());",
      "snippet_full": "// Babylon 8 โ€” Havok physics + thinInstance grid (stronghold-3d pattern)\\nimport { Engine, Scene, HavokPlugin, Vector3, MeshBuilder, Matrix, PhysicsAggregate, PhysicsShapeType } from '@babylonjs/core';\\nimport HavokPhysics from '@babylonjs/havok';\\nconst havok = await HavokPhysics();\\nconst engine = new Engine(canvas, true, { stencil: true });\\nconst scene = new Scene(engine);\\nscene.enablePhysics(new Vector3(0, -9.81, 0), new HavokPlugin(true, havok));\\nconst ground = MeshBuilder.CreateGround('g', { width: 100, height: 100 }, scene);\\nnew PhysicsAggregate(ground, PhysicsShapeType.BOX, { mass: 0 }, scene);\\n// 100 crates in 1 draw call\\nconst crate = MeshBuilder.CreateBox('crate', { size: 1 }, scene);\\nconst matrices = new Float32Array(16 * 100);\\nconst buf = new Float32Array(100 * 16);\\nfor (let i = 0; i < 100; i++) {\\n  Matrix.TranslationToRef(i % 10, 5, Math.floor(i / 10), crate.thinInstanceAdd ? Matrix.Identity() : Matrix.Identity());\\n  Matrix.Translation(i % 10, 5, Math.floor(i / 10)).copyToArray(buf, i * 16);\\n}\\ncrate.thinInstanceSetBuffer('matrix', buf, 16);\\ncrate.thinInstanceRefreshBoundingInfo();\\nwindow.addEventListener('resize', () => engine.resize());\\nengine.runRenderLoop(() => scene.render());",
      "tags": [
        "3d",
        "babylon",
        "pbr",
        "webxr",
        "havok",
        "เน€เธเธก 3d",
        "engine"
      ],
      "difficulty": "Advanced",
      "time_to_learn": "1 week",
      "docs_url": "https://doc.babylonjs.com/",
      "compare_with": [
        "3.4 Three.js (smaller, no physics)",
        "3.7 WebGL (raw GPU)",
        "3.8 WebGPU (next-gen)"
      ]
    },
    {
      "id": "3.2",
      "title": "Phaser 3 (2D Game Engine)",
      "icon": "๐ŸฅŠ",
      "description": "Comprehensive 2D engine โ€” sprites, Arcade/P2/Matter physics, scene lifecycle, animations, tilemaps, sound โ€” 800KB.",
      "when_to_use": "2D fighter (street-fighter-thai); 2D shooter (sky-ace); tower defense; platformer; RPG (realm-of-heroes top-down); mobile canvas game",
      "when_not_to_use": "Pure 3D โ†’ use Babylon (3.1); < 10 entities with no physics โ†’ use Vanilla Canvas (3.3); > 1000 sprites needing max FPS โ†’ use PixiJS (3.6)",
      "pros": [
        "3 physics engines (Arcade/P2/Matter) ship built-in โ†’ Arcade for platformers, Matter for ragdoll โ€” no addon hunt",
        "Scene lifecycle (init/preload/create/update) forces clean state separation โ†’ realm-of-heroes uses MenuScene/GameScene/UIScene stack",
        "First-class TypeScript definitions โ†’ IDE autocomplete vs Phaser 2 JS-only โ€” saves 1 hour per session",
        "WebGL + Canvas renderers auto-fallback โ†’ iOS Safari 14 with WebGL bug falls back to Canvas without code change",
        "Animation editor exports JSON โ†’ designer-friendly; Phaser parses sprite-sheet atlas (TexturePacker / Aseprite) in 3 lines",
        "Camera effects (shake/flash/fade) + Tween manager built-in โ†’ no GSAP dep for game juice"
      ],
      "cons": [
        "800KB minified โ‰ˆ 8x PixiJS โ†’ hurts mobile LCP; use Phaser MINIMAL build to drop to 400KB",
        "API has 3 layers (Game/Scene/GameObject) โ†’ junior dev writes boilerplate; Arc/sprite confusion",
        "Arcade physics is AABB-only โ†’ circles/rotated bodies need P2 or Matter (3x CPU cost)",
        "No built-in 9-slice โ€” must use third-party plugin or roll your own (Pixel-Game dev curse)",
        "Matter.js has 200KB weight โ€” sky-ace ships Matter only for ragdoll debris, Arcade for gameplay",
        "No first-class module system before v3.60 โ€” code-split per scene requires import map or bundler hacks"
      ],
      "gotchas": [
        "Call this.physics.add.collider(player, layer) AFTER physics.world.enable() in create() โ€” setup order matters; sky-ace silent fall-through if reversed",
        "Scene shutdown must call this.events.off() on listeners โ†’ otherwise memory leak + double-fire after scene restart",
        "Scale.FIT + parent centered div causes 1px black bar on iOS notch โ€” use Scale.RESIZE + safe-area-inset CSS",
        "TexturePacker atlas needs fixedSize:false + padding:2 โ†’ otherwise Phaser bleed artifacts in sky-ace tileset",
        "Phaser.Loader.LOCKING = false in create() OR preload() blocks forever on slow CDN โ€” set timeout: 15000",
        "iOS Safari blocks AudioContext until user gesture โ†’ street-fighter-thai unlocks sound on first tap in MainScene.create",
        "Matter debug graphics = +20% CPU โ€” enable only in dev (Phaser config `physics.matter.debug: false` in prod)",
        "Tilemap collision misses if tileset imported with wrong pixel scale โ€” match Tiled's tilewidth to texture frame size exactly"
      ],
      "anti_patterns": [
        "DON'T update in render() callback โ€” use update(time, delta) for frame-time-independent physics (street-fighter-thai rule)",
        "DON'T load assets in create() โ€” use preload() + Loader; otherwise first-frame flickers with missing textures",
        "DON'T put DOM <button> over Phaser canvas for menu โ€” use Phaser text objects OR full DOM scene with Phaser.Game destroyed",
        "DON'T allocate new Phaser.Game per scene transition โ€” single Game instance with scene.start() is 5x faster",
        "DON'T use Matter for platformer โ€” Arcade AABB is 10x faster and 95% sufficient (sky-ace precedent)"
      ],
      "prereqs": [
        "1.1",
        "1.6"
      ],
      "next_steps": [
        "3.10",
        "4.1",
        "4.6"
      ],
      "examples": [
        "street-fighter-thai",
        "sky-ace",
        "realm-of-heroes"
      ],
      "snippet": "class MainScene extends Phaser.Scene { create(){ this.player=this.physics.add.sprite(100,450,'p').setBounce(.2); this.cursors=this.input.keyboard.createCursorKeys(); } update(){ this.player.setVelocity(0); if(this.cursors.left.isDown) this.player.setVelocityX(-200); if(this.cursors.up.isDown&&this.player.body.touching.down) this.player.setVelocityY(-400); } }",
      "snippet_full": "// Phaser 3 โ€” fighter skeleton (street-fighter-thai pattern)\\nclass BattleScene extends Phaser.Scene {\\n  constructor() { super('Battle'); }\\n  preload() { this.load.atlas('fighter', 'assets/fighter.png', 'assets/fighter.json'); }\\n  create() {\\n    this.fighter = this.physics.add.sprite(400, 500, 'fighter', 'idle/01').setBounce(0.15);\\n    this.fighter.setCollideWorldBounds(true);\\n    this.cursors = this.input.keyboard.createCursorKeys();\\n    this.attackKey = this.input.keyboard.addKey('SPACE');\\n    this.anims.create({ key: 'punch', frames: this.anims.generateFrameNames('fighter', { prefix: 'punch/', end: 6, zeroPad: 2 }), frameRate: 18, repeat: 0 });\\n    this.attackKey.on('down', () => { if (!this.fighter.anims.isPlaying) { this.fighter.anims.play('punch'); this.fighter.setVelocityX(this.fighter.flipX ? 600 : -600); } });\\n  }\\n  update(_, dt) {\\n    this.fighter.setVelocityX(0);\\n    if (this.cursors.left.isDown) { this.fighter.setVelocityX(-260); this.fighter.flipX = true; }\\n    else if (this.cursors.right.isDown) { this.fighter.setVelocityX(260); this.fighter.flipX = false; }\\n    if (this.cursors.up.isDown && this.fighter.body.touching.down) this.fighter.setVelocityY(-520);\\n  }\\n}\\nnew Phaser.Game({ type: Phaser.AUTO, parent: 'game', scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH }, physics: { default: 'arcade', arcade: { gravity: { y: 900 } } }, scene: [BattleScene] });",
      "tags": [
        "2d",
        "phaser",
        "sprites",
        "physics",
        "เน€เธเธก 2d",
        "tilemap",
        "engine"
      ],
      "difficulty": "Intermediate",
      "time_to_learn": "3 days",
      "docs_url": "https://phaser.io/docs",
      "compare_with": [
        "3.3 Vanilla Canvas (no dep)",
        "3.6 PixiJS (render-only, faster)",
        "3.5 Kaboom (simpler)"
      ]
    },
    {
      "id": "3.3",
      "title": "Vanilla Canvas 2D (Zero-Dep 2D)",
      "icon": "๐Ÿ“ฆ",
      "description": "Pure Canvas 2D API + manual game loop. 0KB dep. Best for <200 entities with simple physics (tycoon, particles, UI tools).",
      "when_to_use": "Tycoon (noodle-shop customer queue); particle effects; data viz; simple click-game < 200 entities; tools (palette color picker); tile-map RPG under 50x50",
      "when_not_to_use": "> 200 entities with collisions โ†’ use Phaser (3.2); need physics engine โ†’ use Phaser Arcade; performance 60fps with 1000+ sprites โ†’ use PixiJS (3.6)",
      "pros": [
        "0KB dep โ†’ loads in <50ms on slow 3G; ideal for CodeHub static landing widgets",
        "Full control of draw order/blend modes/transforms โ€” no engine abstraction tax",
        "Debug = what user sees; no source-map or framework error noise in console (noodle-shop dev speed)",
        "Works file:// โ†’ offline / Codepen / jsFiddle without install โ€” pasta-shop demo runs from USB",
        "Canvas 2D matured in all browsers โ€” even iOS Safari 13+ supports Path2D, OffscreenCanvas",
        "No vendor lock-in; HTML/CSS overlay for HUD = free a11y + IME + screen-reader for Thai"
      ],
      "cons": [
        "DOM-free a11y โ†’ screen readers miss canvas content; need offscreen <canvas> mirrored to DOM (noodle-shop tax)",
        "No scene mgmt โ†’ 1000-line games become spaghetti; manual update/render dispatch",
        "Canvas 2D < WebGL perf at > 500 draw calls/frame; drawImage with rotation = 0.3ms each on mid-mobile",
        "Manual asset versioning; cache bust with ?v=Date.now() to defeat iOS Safari 7-day HTML cache",
        "No spatial index โ€” collision check O(nยฒ) past 200 entities; need quadtree or grid broadphase",
        "devicePixelRatio must scale canvas resolution โ€” without it retina = blurry (most common bug)"
      ],
      "gotchas": [
        "Set canvas.width = w * devicePixelRatio AND ctx.scale(dpr, dpr) โ€” otherwise retina blurry",
        "requestAnimationFrame NOT setInterval โ€” rAF pauses when tab backgrounded (saves battery + matches display Hz)",
        "Clear with ctx.clearRect(0,0,w,h) NOT ctx.fillRect(white) โ€” clearRect is 3x faster on iOS Safari",
        "ctx.save()/restore() pairing inside a draw call must balance โ€” otherwise transform stack corrupts (noodle-shop lesson)",
        "ctx.measureText() returns cached width โ€” call once at boot for HUD text, not every frame (tycoon dashboard)",
        "Touch events need {passive:false} to call e.preventDefault() โ†’ without it scroll fights drag",
        "OffscreenCanvas.transferToImageBitmap() for worker rendering โ€” Chrome only but 10x faster for image processing",
        "Hit-test for clicks: track mouse pos + button-down + entity list โ€” never use canvas's hit-region API (Safari buggy)"
      ],
      "anti_patterns": [
        "DON'T draw all entities every frame without dirty-rect tracking โ€” re-use offscreen canvas for static layers (noodle-shop kitchen tiles)",
        "DON'T put game state in DOM <input> hidden fields โ€” use plain JS object + localStorage; DOM read/write is 10x slower per frame",
        "DON'T call canvas.toDataURL() per frame โ€” it stalls 50ms; only call on save event",
        "DON'T use ctx.drawImage with imageElement cached wrong โ€” pre-decode via img.decode() promise before first draw",
        "DON'T ignore ResizeObserver โ†’ canvas keeps stale size on phone rotate; obs + resize handler fixes 90% of mobile reports"
      ],
      "prereqs": [
        "1.6"
      ],
      "next_steps": [
        "4.1",
        "3.10"
      ],
      "examples": [
        "noodle-shop",
        "demo-coin-flip",
        "palette"
      ],
      "snippet": "const ctx = canvas.getContext('2d'); const loop = () => { ctx.clearRect(0,0,w,h); entities.forEach(e => { e.x += e.vx; e.y += e.vy; ctx.fillRect(e.x, e.y, e.w, e.h); }); requestAnimationFrame(loop); }; loop();",
      "snippet_full": "// Vanilla Canvas 2D โ€” tycoon grid with dirty-rect (noodle-shop pattern)\\nconst canvas = document.getElementById('game');\\nconst ctx = canvas.getContext('2d', { alpha: false });\\nfunction resize() { const dpr = devicePixelRatio || 1; canvas.width = innerWidth * dpr; canvas.height = innerHeight * dpr; canvas.style.width = innerWidth + 'px'; canvas.style.height = innerHeight + 'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); }\\nresize(); addEventListener('resize', resize);\\nconst customers = []; const grid = Array.from({ length: 8 }, () => Array(8).fill(null));\\nlet last = performance.now();\\nfunction tick(now) {\\n  const dt = Math.min((now - last) / 1000, 0.05); last = now;\\n  ctx.clearRect(0, 0, innerWidth, innerHeight);\\n  // grid background cached in offscreen, blit once\\n  for (const c of customers) { c.x += c.vx * dt; c.timer -= dt; if (c.timer <= 0) customers.splice(customers.indexOf(c), 1); }\\n  for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) if (grid[y][x]) { ctx.fillStyle = grid[y][x].color; ctx.fillRect(x * 60, y * 60, 58, 58); }\\n  for (const c of customers) { ctx.fillStyle = '#ec4899'; ctx.beginPath(); ctx.arc(c.x, c.y, 18, 0, Math.PI * 2); ctx.fill(); }\\n  requestAnimationFrame(tick);\\n}\\nsetInterval(() => { customers.push({ x: 0, y: Math.random() * innerHeight, vx: 80, timer: 8 }); }, 1500);\\nrequestAnimationFrame(tick);",
      "tags": [
        "canvas",
        "2d",
        "vanilla",
        "no-dep",
        "เธงเธฒเธ”เน€เธ‚เธตเธขเธ™",
        "game-loop",
        "tycoon"
      ],
      "difficulty": "Intermediate",
      "time_to_learn": "2 days",
      "docs_url": "https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D",
      "compare_with": [
        "3.2 Phaser (scenes + physics)",
        "3.6 PixiJS (WebGL perf)",
        "3.5 Kaboom (component)"
      ]
    },
    {
      "id": "3.4",
      "title": "Three.js (Lightweight 3D Renderer)",
      "icon": "๐ŸŒ",
      "description": "Smallest mainstream 3D renderer (~150KB) โ€” declarative scene graph + WebGL/WebGPU backends โ€” focused on rendering, NOT game features.",
      "when_to_use": "3D model viewer (fitcheck โ€” clothes try-on); product configurator; data viz (3D bar chart); AR/VR preview; minimalist portfolio 3D; CSS3DRenderer hybrid UI",
      "when_not_to_use": "Heavy game needing physics/GUI/particles bundled โ†’ use Babylon (3.1) instead; pixel-art 2D โ†’ use Phaser; shader-only particle math โ†’ raw WebGL (3.7)",
      "pros": [
        "~150KB core (~10x smaller than Babylon) โ†’ perfect for fitcheck embed + product configurator first-paint",
        "Declarative scene graph โ†’ `scene.add(mesh)` reads like document.appendChild; junior-friendly",
        "Massive ecosystem: @three/xr, three-stdlib (OrbitControls, GLTFLoader, EffectComposer) โ€” Babylon equivalent needs custom code",
        "r150+ ships WebGPURenderer flag โ€” switch to GPU when navigator.gpu exists, fallback WebGL automatically (fitcheck pattern)",
        "BufferGeometry is the only API (Geometry deprecated in r125) โ€” flat Float32Array is cache-friendly and serializable",
        "Examples at threejs.org cover 90% of use cases โ€” copy-paste + tweak faster than Babylon docs"
      ],
      "cons": [
        "No physics, no GUI, no particles in core โ†’ must compose 3+ addons (Cannon-es + lil-gui + tsparticles) = 400KB total",
        "Materials are thin wrappers over GLSL โ€” PBR with env map = 20 lines setup; Babylon is 3 lines",
        "WebXR requires @three/xr with manual session.requestSession + frame loop glue โ€” Babylon does it in 1 helper",
        "Inspector = external dep (three-inspect) not built-in โ€” debugging prod scene = print transforms to console",
        "Mobile post-processing is brutal โ€” EffectComposer + Bloom tanks to 20fps on iPhone 11; bake into textures",
        "TypeScript types are community-maintained โ€” some addon .d.ts files lag releases by weeks"
      ],
      "gotchas": [
        "renderer.setPixelRatio(Math.min(devicePixelRatio, 2)) โ€” without cap, retina = 4x GPU fillrate = 15fps mobile",
        "Use BufferGeometry NOT Geometry โ€” Geometry was removed in r125; migrate legacy code immediately",
        "renderer.outputColorSpace = THREE.SRGBColorSpace (r152+) โ€” without it PBR textures wash out / too bright (fitcheck bug)",
        "renderer.toneMapping = THREE.ACESFilmicToneMapping for PBR โ€” default linear looks chalky",
        "GLTFLoader needs DRACO + KTX2 loaders configured OR .glb inflates 10x โ€” fitcheck preloads DRACO wasm",
        "Dispose resources explicitly: geometry.dispose(), material.dispose(), texture.dispose() โ€” otherwise GPU mem leaks 5MB/min",
        "WebGLRenderer + ShaderMaterial: don't forget to set uniforms.needsUpdate = true โ€” silent no-op is common",
        "Resize handler must update camera.aspect + renderer.setSize โ€” otherwise stretched on rotate"
      ],
      "anti_patterns": [
        "DON'T use MeshBasicMaterial for outdoor scene โ€” no lighting; use MeshStandardMaterial or MeshPhysicalMaterial (fitcheck rule)",
        "DON'T allocate new Vector3() inside render loop โ€” reuse pool of 8-10 vectors; GC pauses cause 5-10ms hitches",
        "DON'T load multiple GLTFs sequentially โ€” use GLTFLoader.loadAsync in parallel Promise.all (fitcheck boots 6 models in 2s not 12s)",
        "DON'T trust Euler rotation order โ€” default 'XYZ' vs 'YXZ' gives gimbal-lock; set explicitly: rotation.order = 'YXZ' for FPS-style cameras",
        "DON'T skip OrbitControls.update() in render loop โ€” damping breaks when not called each frame (fitcheck lesson)"
      ],
      "prereqs": [
        "1.1"
      ],
      "next_steps": [
        "3.1",
        "3.7",
        "6.4"
      ],
      "examples": [
        "fitcheck"
      ],
      "snippet": "const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, w/h, 0.1, 1000); const mesh = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshStandardMaterial({color:0xec4899})); scene.add(mesh); scene.add(new THREE.DirectionalLight(0xffffff,1)); function tick(){ mesh.rotation.x+=.01; renderer.render(scene,camera); requestAnimationFrame(tick); }",
      "snippet_full": "// Three.js r160 โ€” fitcheck GLTF viewer pattern\\nimport * as THREE from 'three';\\nimport { OrbitControls } from 'three/addons/controls/OrbitControls.js';\\nimport { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';\\nimport { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';\\nconst renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });\\nrenderer.setPixelRatio(Math.min(devicePixelRatio, 2));\\nrenderer.outputColorSpace = THREE.SRGBColorSpace;\\nrenderer.toneMapping = THREE.ACESFilmicToneMapping;\\nconst scene = new THREE.Scene();\\nscene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 1));\\nconst camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 100);\\ncamera.position.set(0, 1.6, 3);\\nconst controls = new OrbitControls(camera, renderer.domElement);\\ncontrols.enableDamping = true;\\nconst draco = new DRACOLoader().setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');\\nnew GLTFLoader().setDRACOLoader(draco).load('model.glb', (gltf) => {\\n  scene.add(gltf.scene);\\n  controls.target.copy(gltf.scene.position);\\n});\\nfunction tick() { controls.update(); renderer.render(scene, camera); requestAnimationFrame(tick); }\\naddEventListener('resize', () => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth, innerHeight); });\\ntick();",
      "tags": [
        "3d",
        "three",
        "gltf",
        "webgl",
        "model-viewer",
        "เธชเธฒเธกเธกเธดเธ•เธด"
      ],
      "difficulty": "Intermediate",
      "time_to_learn": "3 days",
      "docs_url": "https://threejs.org/docs/",
      "compare_with": [
        "3.1 Babylon (batteries-included)",
        "3.7 WebGL (raw)",
        "3.8 WebGPU (next-gen)"
      ]
    },
    {
      "id": "3.5",
      "title": "Kaboom.js / Kaplay (Arcade 2D)",
      "icon": "๐Ÿ•น๏ธ",
      "description": "~80KB component-based arcade 2D โ€” add([rect(), pos(), body()]) chain syntax โ€” game-jam-grade simplicity.",
      "when_to_use": "Game jam 48h prototypes; tiny arcade (Pong/Snake/Mario-clone under 200 LOC); kids' coding lesson (codekids-thai); quick landing-page game",
      "when_not_to_use": "Production-grade RPG with save system โ†’ use Phaser (3.2); > 200 entities โ†’ use PixiJS (3.6); commercial launch with SLA โ†’ avoid Kaboom team churn risk",
      "pros": [
        "~80KB minified โ‰ˆ 1/10 Phaser โ†’ instant load on mobile 3G; perfect for codekids-thai lesson embed",
        "Component chain syntax: add([rect(50,50), pos(50,50), color(0,255,0), body(), area(), 'player']) reads like a sentence",
        "Built-in scene() with key/state pattern โ†’ 10-line platformer prototype vs 100 in Phaser",
        "Zero-config input: keyDown('left', () => ...) โ†’ keyboard + touch auto-detected (codekids tablets)",
        "Built-in onUpdate/onDraw lifecycle โ†’ no manual requestAnimationFrame management",
        "Re-exported as `kaplay` since 2024 โ€” old `kaboom` still works via shim but migrate to kaplay()"
      ],
      "cons": [
        "Kaplay (formerly Kaboom) had 2 API breaking changes in 18 months โ†’ upgrade tax every quarter",
        "No first-class save system โ†’ must roll localStorage wrapper; no migration framework",
        "Performance modest past 100 entities โ€” no spatial hash, no GPU batching beyond default sprite atlas",
        "TypeScript support is partial โ€” many component types are `any`; IDE autocomplete weak (codekids friction)",
        "Community is shrinking vs Phaser โ€” fewer StackOverflow answers, fewer plugins, fewer devs hiring for it",
        "No mobile build pipeline (Cordova/Capacitor) maintained โ€” fine for web, dead for native"
      ],
      "gotchas": [
        "Use `import kaplay from 'kaplay'` NOT `kaboom` โ€” Kaboom renamed 2024, old package is deprecated",
        "Component order matters in add([...]) โ€” body() before area() throws 'area not attached'",
        "loadSprite('player', 'p.png') must happen inside a scene's onLoad โ€” not at module top-level",
        "onUpdate() callback receives deltaTime but it's already dt-multiplied in v3000+ โ€” don't multiply again (silent 2x speed)",
        "Touch input on iPad needs touchToMouse polyfill OR scene('main', () => { onTouchStart(... ) }) โ€” auto-detect misses some",
        "Pos components store x/y as numbers โ€” for big worlds use camPos()/camScale() for camera, don't translate every entity",
        "outline() component needs a parent color() โ€” without it outline default color is black on black = invisible",
        "debug.inspect = true in dev shows entity tree but adds 5% CPU โ€” disable in prod"
      ],
      "anti_patterns": [
        "DON'T use Kaboom for anything beyond 2 weeks of dev โ€” the API churn + community decay will burn you at v3 update (codekids avoided)",
        "DON'T nest scenes 3+ deep โ€” Kaplay's scene stack uses global state; bugs at depth 3+ are untraceable",
        "DON'T store save state in kaboom's getData()/setData() โ€” those are session-scoped; use localStorage wrapper",
        "DON'T use kaboom() global init in module โ€” import kaplay and call const k = kaplay({...}) for testability",
        "DON'T mix Kaboom + Phaser โ€” they fight over canvas/context; pick one"
      ],
      "prereqs": [
        "1.1"
      ],
      "next_steps": [
        "3.2",
        "3.3"
      ],
      "examples": [
        "codekids-thai"
      ],
      "snippet": "kaplay(); scene('main', () => { add([rect(50,50), pos(50,50), color(0,255,0), body(), area()]); const p = add([rect(40,40), pos(100,300), color(0,200,255), body(), 'player']); keyDown('left', () => p.move(-200, 0)); });",
      "snippet_full": "// Kaplay 3000+ โ€” Pong in 30 LOC (game-jam pattern)\\nimport kaplay from 'kaplay';\\nconst k = kaplay({ width: 800, height: 600, background: [0, 0, 0] });\\nk.scene('main', () => {\\n  k.add([k.rect(800, 20), k.pos(0, 0), k.area(), 'wall']);\\n  k.add([k.rect(800, 20), k.pos(0, 580), k.area(), 'wall']);\\n  const ball = k.add([k.rect(20, 20), k.pos(390, 290), k.area(), k.body({ isStatic: false }), 'ball']);\\n  ball.onUpdate(() => { if (ball.pos.x < 0 || ball.pos.x > 780) k.burp(); });\\n  const p1 = k.add([k.rect(20, 100), k.pos(30, 250), k.area(), k.body({ isStatic: true }), 'paddle']);\\n  const p2 = k.add([k.rect(20, 100), k.pos(750, 250), k.area(), k.body({ isStatic: true }), 'paddle']);\\n  k.onKeyDown('w', () => p1.move(0, -300)); k.onKeyDown('s', () => p1.move(0, 300));\\n  k.onKeyDown('up', () => p2.move(0, -300)); k.onKeyDown('down', () => p2.move(0, 300));\\n  ball.onCollide('paddle', (p) => { if (p.pos.x < 400) ball.dir = k.vec2(1, k.randi(-1, 2)); else ball.dir = k.vec2(-1, k.randi(-1, 2)); });\\n  ball.onCollide('wall', () => k.add([k.text('POINT!'), k.pos(400, 300), k.lifespan(1, { fade: 0.5 })]));\\n  k.wait(1, () => ball.dir = k.vec2(1, 0));\\n});\\nk.go('main');",
      "tags": [
        "2d",
        "arcade",
        "kaplay",
        "kaboom",
        "prototype",
        "เน€เธเธกเธ‡เนˆเธฒเธข",
        "game-jam"
      ],
      "difficulty": "Beginner",
      "time_to_learn": "4 hours",
      "docs_url": "https://kaplayjs.com/",
      "compare_with": [
        "3.2 Phaser (production)",
        "3.3 Vanilla Canvas (no dep)",
        "3.6 PixiJS (render-only)"
      ]
    },
    {
      "id": "3.6",
      "title": "PixiJS (Fastest 2D WebGL Renderer)",
      "icon": "๐Ÿ–ผ๏ธ",
      "description": "WebGL-accelerated 2D renderer โ€” fastest sprite batcher in JS โ€” filter library + mesh/particle โ€” render-only (no physics).",
      "when_to_use": "Performance-critical 2D > 1000 sprites (particle storm, map editor, character creator); isometric MMORPG (realm-of-heroes zoom-out); data-viz dashboard with 5k nodes",
      "when_not_to_use": "Need built-in physics โ†’ use Phaser (3.2); arcade < 50 entities โ†’ use Kaboom (3.5); pure 2D UI < 200 nodes โ†’ use Canvas 2D (3.3)",
      "pros": [
        "WebGL sprite batching = 5000 sprites at 60fps on mobile; Phaser Canvas renderer tops at ~500",
        "Filter library: Blur/ColorMatrix/Displacement built-in โ€” particle glow / CRT effect in 3 lines",
        "v8 async init: `await app.init({...})` โ€” clearer than v7 callback chain; tree-shake friendly ESM",
        "ParticleContainer for > 100 sprites of same texture โ€” 1 draw call regardless of count (5x faster)",
        "Mesh + NineSlicePlane for procedural UI โ€” saves Photoshop round-trips in pixel-perfect HUDs",
        "Renderer-agnostic: WebGL, WebGPU (v8 alpha), Canvas โ€” future-proof your 2D investment"
      ],
      "cons": [
        "Render-only โ€” no scene management, no physics, no input handling (must add EventEmitter separately)",
        "~400KB minified โ†’ bigger than Phaser MINIMAL but faster; trade size for FPS",
        "v8 API is async (await app.init) โ€” migration from v7 sync API = lots of await refactors",
        "No game loop helper โ€” must wire your own ticker (gsap, raf, or own loop); Phaser ships one",
        "Filter pipeline = 1 GPU pass per filter; stacking 3+ filters tanks to 20fps on iPhone 11",
        "Texture atlas must be in specific format (Pixi v8 prefers Spine-style JSON, not TexturePacker default)"
      ],
      "gotchas": [
        "v8 (2024) is async โ€” `await app.init({width,height})` then `document.body.appendChild(app.canvas)`; v7 sync is GONE",
        "Use ParticleContainer (not Container) for > 100 sprites of same base texture โ€” single draw call vs 100",
        "app.canvas has 2x devicePixelRatio already โ€” set CSS width/height in half-pixel units to avoid blur",
        "Filters stack = each filter = 1 framebuffer ping-pong; cache filter result in RenderTexture if static",
        "Texture.from('url') caches by URL โ€” clear with Texture.removeFromCache on memory warning",
        "InteractionManager (v7) โ†’ FederatedEvents (v8) โ€” on('pointerdown') API changed; migration = search/replace",
        "Bundle size: pixi.js (full) is 400KB; @pixi/sprite + @pixi/app is 200KB โ€” choose per use case",
        "WebGL context loss on Android Chrome tab background โ†’ listen to app.renderer.on('contextlost') and rebuild textures"
      ],
      "anti_patterns": [
        "DON'T put 10000 individual Sprites in a Container โ€” wrap in ParticleContainer or batch by texture for 1 draw call",
        "DON'T mix PIXI.Text + HTML <input> in same layout โ€” different sizing model; use Pixi v8 Text or full DOM",
        "DON'T apply Filter to entire stage โ€” apply only to specific sprites; full-stage filter is GPU bottleneck",
        "DON'T resize canvas via CSS only โ€” must also call app.renderer.resize(w,h) to update buffer (v8) or canvas.width/height (v7)",
        "DON'T forget app.destroy() on unmount โ€” Pixi leaks WebGL context + GPU buffers; React useEffect cleanup mandatory"
      ],
      "prereqs": [
        "1.1"
      ],
      "next_steps": [
        "3.2",
        "3.7"
      ],
      "examples": [
        "realm-of-heroes"
      ],
      "snippet": "const app = new PIXI.Application(); await app.init({width:800,height:600,antialias:true}); const sprite = PIXI.Sprite.from('p.png'); sprite.x=400; sprite.y=300; app.stage.addChild(sprite); document.body.appendChild(app.canvas);",
      "snippet_full": "// PixiJS v8 โ€” 5000 sprite particle storm (realm-of-heroes effect)\\nimport { Application, Sprite, Texture, Container } from 'pixi.js';\\nconst app = new Application();\\nawait app.init({ width: window.innerWidth, height: window.innerHeight, antialias: true, backgroundAlpha: 0 });\\ndocument.body.appendChild(app.canvas);\\nconst tex = await Texture.from('https://codehub.sj88ai.com/static/sparkle.png');\\nconst particles = new Container();\\napp.stage.addChild(particles);\\nconst sprites = [];\\nfor (let i = 0; i < 5000; i++) {\\n  const s = new Sprite(tex);\\n  s.anchor.set(0.5);\\n  s.x = Math.random() * app.screen.width;\\n  s.y = Math.random() * app.screen.height;\\n  s.tint = Math.random() * 0xffffff;\\n  s.alpha = Math.random();\\n  s.scale.set(0.2 + Math.random() * 0.4);\\n  s.vx = (Math.random() - 0.5) * 2;\\n  s.vy = (Math.random() - 0.5) * 2;\\n  sprites.push(s);\\n  particles.addChild(s);\\n}\\napp.ticker.add((ticker) => {\\n  const dt = ticker.deltaMS / 16.67;\\n  for (const s of sprites) { s.x += s.vx * dt; s.y += s.vy * dt; if (s.x < 0 || s.x > app.screen.width) s.vx *= -1; if (s.y < 0 || s.y > app.screen.height) s.vy *= -1; }\\n});\\nwindow.addEventListener('beforeunload', () => app.destroy(true, { children: true }));",
      "tags": [
        "2d",
        "pixijs",
        "webgl",
        "sprite",
        "particle",
        "render-only",
        "เน€เธฃเน‡เธง"
      ],
      "difficulty": "Intermediate",
      "time_to_learn": "2 days",
      "docs_url": "https://pixijs.com/docs",
      "compare_with": [
        "3.2 Phaser (physics included)",
        "3.3 Vanilla Canvas (no dep)",
        "3.5 Kaboom (simpler)"
      ]
    },
    {
      "id": "3.7",
      "title": "WebGL / WebGL2 (Custom GLSL Shaders)",
      "icon": "๐ŸŽ†",
      "description": "Raw GPU API โ€” hand-write GLSL vertex/fragment shaders โ€” max performance for particle storms, fluid sim, post-FX โ€” no engine.",
      "when_to_use": "10k+ particles with physics-on-GPU; custom fluid/water/fire shader; post-process (bloom/DoF/SSR); audio visualizer; shadertoy-style art",
      "when_not_to_use": "Generic 3D scene โ†’ use Three.js (3.4) or Babylon (3.1); need physics+scene-graph โ†’ don't reinvent; 2D โ†’ Canvas 2D or PixiJS",
      "pros": [
        "GPU compute (WebGL2 transform feedback / WebGPU compute) โ€” 100k particles updated in <1ms on RTX 2060",
        "Full creative control โ€” fragment shaders can paint anything: raymarching, signed-distance fields, NPR stylization",
        "WebGL 2 in 96% browsers (2024) โ€” VS WebGPU 18%; WebGL2 is today's safe low-level target",
        "GLSL ES 3.00 vs WebGL1 1.00 โ€” uniform blocks, instanced drawing, texelFetch โ€” modern features available",
        "ShaderToy migration is 1:1 โ€” copy any .glsl shader, paste into <script id='vs'>, compile, render to fullscreen quad",
        "Three.js ShaderMaterial = on-ramp: use Three's renderer + camera + raw ShaderMaterial โ†’ keep conveniences, drop into GLSL when needed"
      ],
      "cons": [
        "GLSL is a real language โ€” uniforms, varyings, attributes, precision qualifiers โ€” 2-week ramp for non-graphics dev",
        "Debugging is brutal โ€” no breakpoints; print uniforms as color and read pixels; Spector.js / RenderDoc are external",
        "Context loss handling mandatory โ€” Android Chrome tab-switch drops context; must rebuild all shaders",
        "No scene graph โ€” you build VAO/VBO/uniform setup; 500 LOC for what Three does in 5",
        "Mobile GPU varies wildly โ€” Adreno 640 vs Apple A14 = 10x perf gap; must profile on real devices",
        "WebGL doesn't multithread โ€” heavy CPU physics + WebGL render = jank; move physics to worker (sims-lite pattern)"
      ],
      "gotchas": [
        "WebGL2 = 96% browsers, WebGL1 = 99% โ€” for new code use WebGL2 (`canvas.getContext('webgl2')`) but feature-detect fallback (Three.js does this)",
        "Context loss: listen `canvas.addEventListener('webglcontextlost', e => { e.preventDefault(); rebuild(); })` โ€” must preventDefault to recover",
        "Precision qualifiers matter: mediump in fragment shader is fast but 16-bit float; highp required for positions, lowp for color tints",
        "Don't allocate textures per frame โ€” pool or use single dynamic texture with texSubImage2D updates",
        "Uniform location lookups are slow โ€” `gl.getUniformLocation(p, 'u')` once at boot, cache the location; per-frame lookup = 5ms tax",
        "Instanced drawing (gl.drawArraysInstanced) โ€” render 10000 cubes in 1 draw call instead of 10000; Three's instancedMesh wraps this",
        "Use float textures (EXT_color_buffer_float) for post-process ping-pong โ€” required for HDR bloom in WebGL2",
        "Shader compile is async โ€” call `gl.getShaderParameter(s, gl.COMPILE_STATUS)` and check `gl.getShaderInfoLog(s)` for errors; silent fail = black screen"
      ],
      "anti_patterns": [
        "DON'T write vertex shader from scratch for triangle mesh โ€” Three.js BufferGeometry + ShaderMaterial gives you the boilerplate; raw WebGL only when at GPU limits",
        "DON'T use `vec3` for colors in fragment shader โ€” use `vec4` with alpha for blending; vec3 alpha defaults are wrong",
        "DON'T skip `gl.flush()` after multiple draw calls if reading pixels back โ€” driver may reorder; explicit barrier for readPixels",
        "DON'T enable blending if you don't need it โ€” `gl.disable(gl.BLEND)` saves 30% GPU on opaque-only scenes",
        "DON'T forget to set `gl.viewport(0, 0, canvas.width, canvas.height)` on resize โ€” without it renders stretch on HiDPI"
      ],
      "prereqs": [
        "1.6"
      ],
      "next_steps": [
        "3.1",
        "3.4",
        "3.8"
      ],
      "examples": [
        "sims-lite",
        "stronghold-3d"
      ],
      "snippet": "const gl = canvas.getContext('webgl2'); const prog = gl.createProgram(); const vs = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(vs, '#version 300 es\\nin vec2 a; void main(){gl_Position=vec4(a,0,1);}'); gl.compileShader(vs);",
      "snippet_full": "// WebGL2 โ€” fullscreen quad with fragment shader (ShaderToy pattern)\\nconst canvas = document.getElementById('c');\\nconst gl = canvas.getContext('webgl2', { antialias: true });\\ncanvas.width = innerWidth * devicePixelRatio;\\ncanvas.height = innerHeight * devicePixelRatio;\\nfunction compile(type, src) {\\n  const sh = gl.createShader(type);\\n  gl.shaderSource(sh, src);\\n  gl.compileShader(sh);\\n  if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(sh)); throw new Error('shader fail'); }\\n  return sh;\\n}\\nconst vs = compile(gl.VERTEX_SHADER, `#version 300 es\\nin vec2 a; void main(){gl_Position=vec4(a,0,1);}`);\\nconst fs = compile(gl.FRAGMENT_SHADER, `#version 300 es\\nprecision highp float;\\nuniform vec2 uRes;\\nuniform float uTime;\\nout vec4 o;\\nvoid main(){ vec2 uv = (gl_FragCoord.xy*2.0 - uRes)/min(uRes.x,uRes.y); float t = uTime*0.5; float d = length(uv - 0.5*vec2(cos(t),sin(t))); o = vec4(0.5+0.5*cos(d*8.0+vec3(0,2,4)+t), 1.0); }`);\\nconst prog = gl.createProgram();\\ngl.attachShader(prog, vs); gl.attachShader(prog, fs); gl.linkProgram(prog); gl.useProgram(prog);\\nconst buf = gl.createBuffer();\\ngl.bindBuffer(gl.ARRAY_BUFFER, buf);\\ngl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);\\nconst loc = gl.getAttribLocation(prog, 'a');\\ngl.enableVertexAttribArray(loc);\\ngl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);\\nconst uRes = gl.getUniformLocation(prog, 'uRes');\\nconst uT = gl.getUniformLocation(prog, 'uTime');\\ncanvas.addEventListener('webglcontextlost', e => { e.preventDefault(); console.warn('context lost'); });\\nfunction tick(t) { gl.viewport(0, 0, canvas.width, canvas.height); gl.uniform2f(uRes, canvas.width, canvas.height); gl.uniform1f(uT, t/1000); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); requestAnimationFrame(tick); }\\nrequestAnimationFrame(tick);",
      "tags": [
        "webgl",
        "shader",
        "glsl",
        "gpu",
        "particle",
        "เน€เธฃเธ™เน€เธ”เธญเธฃเนŒ"
      ],
      "difficulty": "Advanced",
      "time_to_learn": "2 weeks",
      "docs_url": "https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API",
      "compare_with": [
        "3.4 Three.js (engine wrapper)",
        "3.8 WebGPU (next-gen)",
        "3.1 Babylon (full game)"
      ]
    },
    {
      "id": "3.8",
      "title": "WebGPU (Next-Gen GPU API)",
      "icon": "โšก",
      "description": "WebGL successor โ€” compute shaders on web, render pipelines, multi-thread via async โ€” 2024 stable in Chrome/Edge, partial in Safari/Firefox.",
      "when_to_use": "Compute-heavy: ML inference in browser; raytracing preview; physics-on-GPU (1M particles); large terrain LOD streaming; shader-heavy NFT art",
      "when_not_to_use": "Mainstream shipping game (Safari/Firefox support still partial in 2024) โ€” fallback to WebGL or detect via navigator.gpu; mobile low-end = no GPU adapter",
      "pros": [
        "Compute shaders = real GPGPU on web โ€” 1M particle sim at 60fps on M1 MacBook; WebGL needs transform feedback dance",
        "Render pipeline objects pre-compiled โ€” switching shader = 0ms vs WebGL's re-link cost (5-15ms)",
        "Bind groups = explicit resource layout โ€” GPU drivers don't have to guess; faster + safer",
        "Async by design โ€” adapter/device init returns Promise; no more blocking gl.flush() in worker threads",
        "WGSL = real typed language โ€” no GLSL precision footguns; tooling (naga) validates at compile",
        "Three.js r150+ has WebGPURenderer (experimental) โ€” try with `await renderer.init()` and same scene graph as WebGL"
      ],
      "cons": [
        "Browser support 2024: Chrome 113+/Edge โœ“; Safari TP โœ“; Firefox behind flag โ†’ production = double-maintain WebGL fallback",
        "API surface 3x WebGL โ€” bind groups, pipeline layouts, queue, command encoder โ€” steep ramp 3+ weeks",
        "WGSL still has churn (v0.10 in 2023 โ†’ v0.13 in 2024) โ€” port code between versions; no v1.0 yet",
        "Compute shader debugging = Spector.js only; no shader-level breakpoints; black screen = infinite loop risk",
        "Mobile GPU drivers less mature than WebGL โ€” Adreno/Mali sometimes return wrong results; ship CPU fallback",
        "Documentation still sparse vs WebGL โ€” Mozilla MDN WebGPU pages added 2024 but community answers 10x fewer than WebGL"
      ],
      "gotchas": [
        "Feature-detect: `if (!navigator.gpu) return fallbackToWebGL()` โ€” never assume; on iOS Safari 17.4+ TP only",
        "requestAdapter() can return null on low-end Android โ€” check, then requestDevice(); null device = no GPU",
        "Bind groups must match @group(N) in WGSL exactly โ€” order matters; mismatch = validation error + silent fail",
        "Queue writes are batched โ€” multiple writeBuffer + 1 submit() = best perf; per-frame submit = driver overload",
        "Texture format matters: 'bgra8unorm' on Apple Silicon vs 'rgba8unorm' on AMD โ€” query preferredCanvasFormat",
        "Compute shader workgroup size is a multiple of 64 โ€” pass @workgroup_size(64); use numthreads(64,1,1) in HLSL-translated WGSL",
        "Async pipeline creation = first frame after compile is slow (100ms+) โ€” precompile at scene init or loading screen",
        "Lost device (adapter too hot) โ€” listen to device.lost.then(info => recreateDevice(info)); WebGL doesn't have this lifecycle"
      ],
      "anti_patterns": [
        "DON'T assume WebGPU available โ€” always ship WebGL fallback (or Babylon/Three handles it); iOS users = Safari = broken in 2024",
        "DON'T create one big bind group for whole scene โ€” split per logical resource (camera, lights, materials); bind groups are cheap",
        "DON'T submit command encoder per object โ€” batch all draws into 1 encoder pass; submit() = GPU sync barrier",
        "DON'T use WGSL f32 for indices โ€” use u32; f32 index buffer = validation error + driver panic",
        "DON'T load compute shader from string at runtime โ€” precompile to .wgsl binary at build step; runtime parse = 200ms hitch"
      ],
      "prereqs": [
        "3.7"
      ],
      "next_steps": [
        "3.1"
      ],
      "examples": [],
      "snippet": "const adapter = await navigator.gpu.requestAdapter(); const device = await adapter.requestDevice(); const module = device.createShaderModule({ code: '@vertex fn vs(@builtin(vertex_index) i:u32)->@builtin(position) vec4<f32> { return vec4<f32>(0,0,0,1); }' });",
      "snippet_full": "// WebGPU โ€” compute-shader 100k particle sim (next-gen pattern)\\nif (!navigator.gpu) { console.warn('WebGPU unavailable, fallback'); return; }\\nconst adapter = await navigator.gpu.requestAdapter();\\nconst device = await adapter.requestDevice();\\nconst canvas = document.getElementById('c');\\nconst ctx = canvas.getContext('webgpu');\\nconst format = navigator.gpu.getPreferredCanvasFormat();\\nctx.configure({ device, format, alphaMode: 'premultiplied' });\\nconst module = device.createShaderModule({\\n  code: `struct Particle { pos: vec2<f32>, vel: vec2<f32> };\\n@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;\\n@compute @workgroup_size(64) fn update(@builtin(global_invocation_id) gid: vec3<u32>) {\\n  let i = gid.x; if (i >= arrayLength(&particles)) { return; }\\n  particles[i].pos = particles[i].pos + particles[i].vel * 0.016;\\n  if (particles[i].pos.x > 1.0 || particles[i].pos.x < -1.0) { particles[i].vel.x = -particles[i].vel.x; }\\n  if (particles[i].pos.y > 1.0 || particles[i].pos.y < -1.0) { particles[i].vel.y = -particles[i].vel.y; }\\n}`\\n});\\nconst N = 100000;\\nconst particleData = new Float32Array(N * 4);\\nfor (let i = 0; i < N * 4; i += 4) { particleData[i] = (Math.random() - 0.5) * 2; particleData[i+1] = (Math.random() - 0.5) * 2; particleData[i+2] = (Math.random() - 0.5) * 0.02; particleData[i+3] = (Math.random() - 0.5) * 0.02; }\\nconst buf = device.createBuffer({ size: N * 16, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });\\ndevice.queue.writeBuffer(buf, 0, particleData);\\nconst pipeline = device.createComputePipeline({ layout: 'auto', compute: { module, entryPoint: 'update' } });\\nconst bindGroup = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: buf } }] });\\nasync function tick() {\\n  const enc = device.createCommandEncoder();\\n  const pass = enc.beginComputePass(); pass.setPipeline(pipeline); pass.setBindGroup(0, bindGroup); pass.dispatchWorkgroups(Math.ceil(N / 64)); pass.end();\\n  device.queue.submit([enc.finish()]);\\n  await device.queue.onSubmittedWorkDone();\\n  requestAnimationFrame(tick);\\n}\\ntick();",
      "tags": [
        "webgpu",
        "compute",
        "wgsl",
        "gpu",
        "next-gen",
        "เน€เธฃเน‡เธง"
      ],
      "difficulty": "Advanced",
      "time_to_learn": "3 weeks",
      "docs_url": "https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API",
      "compare_with": [
        "3.7 WebGL (universal)",
        "3.1 Babylon (engines WebGPU flag)"
      ]
    },
    {
      "id": "3.9",
      "title": "WebSocket Multiplayer (Realtime 2-Way)",
      "icon": "๐ŸŒ",
      "description": "Bidirectional TCP-over-HTTP socket โ€” server required โ€” low-latency realtime state sync for multiplayer games, collab editors, chat.",
      "when_to_use": "MMORPG (realm-of-heroes); realtime collab whiteboard; co-op tycoon; trading card game; spectator chat; live leaderboard; turn-based battle server-authoritative",
      "when_not_to_use": "One-shot request โ†’ use fetch/AJAX; < 5 players no realtime need โ†’ use long-poll; P2P voice/video โ†’ use WebRTC; broadcast 100k clients โ†’ use SSE or MQTT",
      "pros": [
        "Full-duplex persistent connection โ€” server can push anytime (chat message, monster spawn); HTTP request/response can't",
        "Sub-100ms latency on same-region โ€” feels instant for click-to-action in realm-of-heroes PvP",
        "Single TCP socket multiplexes many logical channels (combat/chat/inventory) โ€” one auth, many features",
        "Standard browser API โ€” no library needed for basic client; battle-tested for 20+ years",
        "WebSocket Secure (wss://) = same TLS as HTTPS โ€” works through corporate proxies that block UDP",
        "Server can be Node.js / Python / Go / Elixir โ€” pick your stack; spec is implementation-agnostic"
      ],
      "cons": [
        "Server required โ€” minimum Node ws library OR paid service (Pusher/Ably); DevOps cost",
        "State sync is YOUR problem โ€” engine doesn't auto-reconcile; client A says 'I moved', client B says 'I'm at', who wins = server arbitration",
        "Connection drops require reconnect + state resync โ€” handle 3G subway tunnels gracefully (mobile game rule)",
        "No built-in auth โ€” token in first message, validate server-side; never trust client messages (cheating)",
        "WebSocket frames have overhead โ€” for chat text, SSE may be simpler; for binary video, WebRTC",
        "Scale: 10k concurrent sockets per server is easy; 100k needs Redis pub/sub + sticky sessions (CF/Traefik)"
      ],
      "gotchas": [
        "Reconnect with exponential backoff: `setTimeout(reconnect, Math.min(30000, 1000 * 2**attempt))` โ€” never re-attempt fixed-interval (DOSes server when it comes back up)",
        "Heartbeat ping/pong every 25s โ€” many proxies (Cloudflare, AWS ALB) drop idle connections at 30-60s; pong frame keeps them alive",
        "JSON.stringify every message is 10x slower than binary โ€” for high-frequency updates (60fps positions) use MessagePack or Protobuf",
        "Send a `seq` number per outbound message โ€” server echos; client drops out-of-order packets; otherwise race conditions on state",
        "On reconnect, send `lastSeq` to server; server replays missed events; OR full state-snapshot if `lastSeq < server.bufferStart`",
        "WebRTC for true P2P (no server) is different API โ€” use for voice; WebSocket for game state; don't mix",
        "iOS Safari 14 background โ†’ WS closes after 30s โ€” listen for visibilitychange + force reconnect on visible",
        "Don't put WebSocket in main bundle โ€” code-split via dynamic import(); saves 30KB initial JS for users who never join multiplayer"
      ],
      "anti_patterns": [
        "DON'T trust client positions โ€” server must validate move speed/collision; otherwise realm-of-heroes PvP = teleport hack",
        "DON'T broadcast all messages to all players โ€” use rooms/channels; realm-of-heroes area-based subscribe = 90% bandwidth saved",
        "DON'T do heavy computation on every WS message โ€” batch process every 16ms (60fps) tick; per-message handler = CPU spike",
        "DON'T forget to handle 'binary' message type โ€” ws.onmessage event has .data as Blob or ArrayBuffer; decode based on typeof",
        "DON'T use ws.send() in render loop without throttling โ€” at 60fps you flood socket; aggregate 50ms snapshots"
      ],
      "prereqs": [
        "1.7",
        "9.7"
      ],
      "next_steps": [
        "9.2"
      ],
      "examples": [
        "realm-of-heroes"
      ],
      "snippet": "class GameSocket { connect() { this.ws = new WebSocket(this.url); this.ws.onclose = () => setTimeout(() => this.connect(), Math.min(30000, this.reconnectDelay *= 2)); this.ws.onmessage = e => this.handle(JSON.parse(e.data)); } send(msg) { this.ws.send(JSON.stringify({...msg, seq: ++this.seq})); } }",
      "snippet_full": "// WebSocket โ€” battle-tested reconnecting client (realm-of-heroes pattern)\\nclass GameSocket {\\n  constructor(url, token) { this.url = url; this.token = token; this.seq = 0; this.lastSeq = 0; this.queue = []; this.handlers = new Map(); }\\n  on(type, fn) { (this.handlers.get(type) || this.handlers.set(type, []).get(type)).push(fn); }\\n  connect() {\\n    this.ws = new WebSocket(`${this.url}?token=${this.token}`);\\n    this.ws.onopen = () => { this.attempt = 0; this.flush(); this.heartbeat = setInterval(() => this.ws.readyState === 1 && this.ws.send('ping'), 25000); };\\n    this.ws.onmessage = (e) => { if (e.data === 'pong') return; const msg = JSON.parse(e.data); if (msg.seq <= this.lastSeq) return; this.lastSeq = msg.seq; (this.handlers.get(msg.type) || []).forEach(fn => fn(msg)); };\\n    this.ws.onclose = () => { clearInterval(this.heartbeat); this.attempt = (this.attempt || 0) + 1; setTimeout(() => this.connect(), Math.min(30000, 500 * 2 ** this.attempt)); };\\n    this.ws.onerror = () => this.ws.close();\\n  }\\n  send(msg) { const payload = JSON.stringify({ ...msg, seq: ++this.seq }); this.ws.readyState === 1 ? this.ws.send(payload) : this.queue.push(payload); }\\n  flush() { while (this.queue.length) this.ws.send(this.queue.shift()); }\\n}\\nconst sock = new GameSocket('wss://realm.sj88ai.com/ws', localStorage.token);\\nsock.on('player_move', m => updatePlayer(m.id, m.x, m.y));\\nsock.on('chat', m => addChatLine(m.user, m.text));\\nsock.connect();",
      "tags": [
        "websocket",
        "multiplayer",
        "realtime",
        "network",
        "เน€เธ™เน‡เธ•เน€เธงเธดเธฃเนŒเธ„",
        "socket"
      ],
      "difficulty": "Advanced",
      "time_to_learn": "3 days",
      "docs_url": "https://developer.mozilla.org/en-US/docs/Web/API/WebSocket",
      "compare_with": [
        "9.7 Fetch/AJAX (one-shot)",
        "WebRTC (P2P)",
        "SSE (server-push only)"
      ]
    },
    {
      "id": "3.10",
      "title": "Game State Machine (FSM)",
      "icon": "๐Ÿ”„",
      "description": "Finite State Machine โ€” entity has 1 state at a time (IDLE/WALK/ATTACK) โ€” guards on transitions prevent invalid combos (e.g., JUMPโ†’JUMP without landing).",
      "when_to_use": "Entity with > 3 behaviors (NPC villager, enemy AI, player combat); UI flow (loadingโ†’menuโ†’gameโ†’pauseโ†’gameover); boss phase manager; AI reactions; matchmaking states",
      "when_not_to_use": "Static puzzle (no state changes); single-action entities (bullet = one lifetime); parallel behaviors โ†’ use Hierarchical FSM or Behavior Tree (4.4)",
      "pros": [
        "States + transitions = visual diagram โ†’ stronghold-3d villagers: IDLEโ†’WANDERโ†’WORKโ†’SLEEP all in one PNG",
        "Guards prevent invalid combos (JUMP only when onGround) โ€” bug class 'attack while dead' impossible by design",
        "onEnter/onExit hooks = clean lifecycle โ€” play anim once on ATTACK enter, stop sound on DEATH exit",
        "Trivial to debug โ€” log state transitions; replay shows exactly when AI broke (sims-lite NPC debugging)",
        "Maps directly to Unity Mecanim / Unreal StateTree โ€” port to native engines is 1:1",
        "String state IDs survive JSON save/load โ€” int enums break if you reorder (street-fighter-thai rule)"
      ],
      "cons": [
        "Parallel states need Hierarchical FSM (HFSM) โ€” basic FSM can't express 'WALK while CARRYING while HUNGRY' simultaneously",
        "Boilerplate per state โ€” 5 states ร— {enter, exit, update, transition} = 20 funcs; behavior tree collapses some",
        "Transitions can grow โ€” 5x5 matrix = 25 cells; many are 'never'; still need to declare to fail-safe",
        "No memory of past states โ€” 'just attacked 3 times in a row' needs state counter on top of FSM",
        "String state IDs typo-prone โ€” `setState('ATTAK')` silently fails if not guarded by Set check",
        "Doesn't model goal-directed AI well โ€” 'go find food' needs BT planner on top; FSM is reactive not proactive"
      ],
      "gotchas": [
        "Use string IDs not numeric enums โ€” `STATES.IDLE = 'idle'`. Numeric enums reorder when JSON load gives wrong value (street-fighter-thai lesson)",
        "Guard functions return boolean: `{ idle: { walk: () => true, attack: m => m.inRange && m.stamina > 0 } }` โ€” guards checked in order",
        "Map<state, transitions> not object literal โ€” Map preserves insertion order across browsers; {} order is spec-but-implementation-leaky",
        "Pre-condition check inside guard, NOT inside setState โ€” setState should be dumb; guards do the validation",
        "onExit called BEFORE onEnter of new state โ€” order matters for anim cleanup (sims-lite: stop walk anim before start attack anim)",
        "Don't transition to same state โ€” `if (next === this.current) return`; otherwise onEnter fires twice causing anim restart glitch",
        "State machine for UI screens is global singleton, not per-entity โ€” `class GameState { state = 'menu' }` lives at root",
        "Save/load: serialize current state ID + counters; on resume, force onEnter to re-init timers (don't trust restored timer values)"
      ],
      "anti_patterns": [
        "DON'T use numeric enums for state โ€” `STATES.IDLE = 0`; reorder JSON = wrong state. Use string literals",
        "DON'T mutate current state directly โ€” always go through setState(next); otherwise onEnter/onExit skipped, anim desync",
        "DON'T put data inside state object โ€” state ID is just a string; counters/inventory live in entity data (separation of concerns)",
        "DON'T nest 5+ levels of FSM โ€” flatten with sub-states or switch to Behavior Tree; deep nesting = spaghetti",
        "DON'T forget to handle 'interrupted' transitions โ€” if AI was WALK and you set to ATTACK, the WALK onExit must reset pathfinding"
      ],
      "prereqs": [
        "1.3"
      ],
      "next_steps": [
        "4.4",
        "4.7"
      ],
      "examples": [
        "stronghold-3d",
        "street-fighter-thai",
        "sims-lite"
      ],
      "snippet": "const STATES = {IDLE:'idle',WALK:'walk',ATTACK:'attack'}; const T = {idle:{walk:()=>true}, walk:{idle:()=>Math.random()<.1, attack:m=>m.targetInRange}}; class FSM { setState(next) { /* guard + onEnter + onExit */ } }",
      "snippet_full": "// FSM โ€” NPC villager (stronghold-3d pattern)\\nconst STATES = { IDLE: 'idle', WANDER: 'wander', WORK: 'work', SLEEP: 'sleep' };\\nconst TRANSITIONS = {\\n  idle: { wander: () => true },\\n  wander: { idle: (v) => v.energy < 20 || v.arrived, work: (v) => v.energy > 50 && v.shift === 'day', sleep: (v) => v.time === 'night' },\\n  work: { idle: (v) => v.taskDone, sleep: (v) => v.time === 'night' },\\n  sleep: { idle: (v) => v.time === 'day' && v.energy > 80 },\\n};\\nclass FSM {\\n  constructor(entity) { this.entity = entity; this.current = STATES.IDLE; this.handlers = new Map(); }\\n  on(state, evt, fn) { const k = `${state}:${evt}`; if (!this.handlers.has(k)) this.handlers.set(k, []); this.handlers.get(k).push(fn); }\\n  setState(next, ctx = {}) {\\n    if (next === this.current) return;\\n    const guard = TRANSITIONS[this.current]?.[next];\\n    if (guard && !guard(this.entity)) return;\\n    const exitHandlers = this.handlers.get(`${this.current}:exit`) || [];\\n    exitHandlers.forEach(fn => fn(this.entity, ctx));\\n    const prev = this.current; this.current = next;\\n    const enterHandlers = this.handlers.get(`${next}:enter`) || [];\\n    enterHandlers.forEach(fn => fn(this.entity, ctx, prev));\\n  }\\n}\\nconst v = { energy: 50, time: 'day', shift: 'day', arrived: false, taskDone: false };\\nconst fsm = new FSM(v);\\nfsm.on('wander', 'enter', (v) => { v.target = randomNearbyTile(); });\\nfsm.on('work', 'enter', (v) => { v.job = pickJob(v); });\\nfsm.on('sleep', 'enter', (v) => { v.bed = findBed(v); });\\nfsm.setState('wander'); // enters wander\\nfsm.setState('work'); // guard: energy>50 && shift=day โ†’ ok\\nfsm.setState('attack'); // guard fails: no transition from 'work' โ†’ rejected",
      "tags": [
        "fsm",
        "state",
        "ai",
        "entity",
        "state-machine",
        "เธชเธ–เธฒเธ™เธฐ"
      ],
      "difficulty": "Intermediate",
      "time_to_learn": "2 hours",
      "docs_url": "https://en.wikipedia.org/wiki/Finite-state_machine",
      "compare_with": [
        "4.4 Behavior Tree (more expressive)",
        "4.2 ECS (data-oriented)",
        "1.3 Module Pattern (state holder)"
      ]
    }
  ]
}
๐Ÿ”— /api/skill (all 100) ๐Ÿ’พ Download game-engines.json