๐ค 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)"
]
}
]
}