--- name: game-builder description: Build 2D and 3D games as DenchClaw apps using p5.js, Three.js, Matter.js, and other game libraries. Covers game architecture, sprites, physics, particles, audio, tilemaps, and complete game examples. metadata: { "openclaw": { "inject": true, "always": true, "emoji": "🎮" } } --- # App Game Builder This skill covers building 2D and 3D games as DenchClaw apps. For core app structure, manifest reference, and bridge API basics, see the parent **app-builder** skill (`app-builder/SKILL.md`). --- ## 2D Games with p5.js **Always use p5.js for 2D games, simulations, generative art, and interactive 2D experiences.** p5.js is the default choice for anything 2D unless the user specifically requests something else. ### When to Use p5.js - 2D games (platformer, puzzle, arcade, card games, board games) - Generative art and creative coding - Physics simulations and particle systems - Interactive data visualizations with animation - Educational simulations and demonstrations - Drawing and painting tools - Any 2D canvas-based interactive experience ### p5.js App Template ``` apps/my-game.dench.app/ .dench.yaml index.html sketch.js assets/ # sprites, sounds, fonts ``` **`.dench.yaml`:** ```yaml name: "My Game" description: "A fun 2D game built with p5.js" icon: "gamepad-2" version: "1.0.0" entry: "index.html" runtime: "static" ``` No permissions needed unless the game reads/writes workspace data. **`index.html`:** ```html My Game ``` **`sketch.js` (game loop skeleton):** ```javascript let isDark = true; function setup() { createCanvas(windowWidth, windowHeight); // Detect theme from DenchClaw if (window.dench) { window.dench.app .getTheme() .then((theme) => { isDark = theme === "dark"; }) .catch(() => {}); } } function draw() { background(isDark ? 15 : 245); // Game rendering goes here } function windowResized() { resizeCanvas(windowWidth, windowHeight); } ``` ### p5.js Instance Mode (Recommended for Complex Apps) Use instance mode to avoid global namespace pollution. This is especially important for multi-file apps: ```javascript const sketch = (p) => { let isDark = true; let player; p.setup = () => { p.createCanvas(p.windowWidth, p.windowHeight); player = { x: p.width / 2, y: p.height / 2, size: 30, speed: 4 }; if (window.dench) { window.dench.app .getTheme() .then((theme) => { isDark = theme === "dark"; }) .catch(() => {}); } }; p.draw = () => { p.background(isDark ? 15 : 245); // Input handling if (p.keyIsDown(p.LEFT_ARROW) || p.keyIsDown(65)) player.x -= player.speed; if (p.keyIsDown(p.RIGHT_ARROW) || p.keyIsDown(68)) player.x += player.speed; if (p.keyIsDown(p.UP_ARROW) || p.keyIsDown(87)) player.y -= player.speed; if (p.keyIsDown(p.DOWN_ARROW) || p.keyIsDown(83)) player.y += player.speed; // Keep in bounds player.x = p.constrain(player.x, 0, p.width); player.y = p.constrain(player.y, 0, p.height); // Draw player p.fill(isDark ? "#6366f1" : "#4f46e5"); p.noStroke(); p.ellipse(player.x, player.y, player.size); }; p.windowResized = () => { p.resizeCanvas(p.windowWidth, p.windowHeight); }; }; new p5(sketch); ``` ### p5.js Game Architecture Patterns #### Game State Machine ```javascript const GameState = { MENU: "menu", PLAYING: "playing", PAUSED: "paused", GAME_OVER: "gameover" }; let state = GameState.MENU; let score = 0; let highScore = 0; function draw() { switch (state) { case GameState.MENU: drawMenu(); break; case GameState.PLAYING: drawGame(); break; case GameState.PAUSED: drawPause(); break; case GameState.GAME_OVER: drawGameOver(); break; } } function keyPressed() { if (state === GameState.MENU && (key === " " || key === "Enter")) { state = GameState.PLAYING; resetGame(); } else if (state === GameState.PLAYING && key === "Escape") { state = GameState.PAUSED; } else if (state === GameState.PAUSED && key === "Escape") { state = GameState.PLAYING; } else if (state === GameState.GAME_OVER && (key === " " || key === "Enter")) { state = GameState.PLAYING; resetGame(); } } function drawMenu() { background(15); fill(255); textAlign(CENTER, CENTER); textSize(48); text("MY GAME", width / 2, height / 2 - 60); textSize(18); fill(150); text("Press SPACE or ENTER to start", width / 2, height / 2 + 20); if (highScore > 0) { textSize(14); text("High Score: " + highScore, width / 2, height / 2 + 60); } } function drawGameOver() { background(15); fill("#ef4444"); textAlign(CENTER, CENTER); textSize(48); text("GAME OVER", width / 2, height / 2 - 60); fill(255); textSize(24); text("Score: " + score, width / 2, height / 2); textSize(16); fill(150); text("Press SPACE to play again", width / 2, height / 2 + 50); } ``` #### Sprite Management ```javascript class Sprite { constructor(x, y, w, h) { this.pos = createVector(x, y); this.vel = createVector(0, 0); this.w = w; this.h = h; this.alive = true; } update() { this.pos.add(this.vel); } draw() { rectMode(CENTER); rect(this.pos.x, this.pos.y, this.w, this.h); } collidesWith(other) { return ( this.pos.x - this.w / 2 < other.pos.x + other.w / 2 && this.pos.x + this.w / 2 > other.pos.x - other.w / 2 && this.pos.y - this.h / 2 < other.pos.y + other.h / 2 && this.pos.y + this.h / 2 > other.pos.y - other.h / 2 ); } isOffscreen() { return ( this.pos.x < -this.w || this.pos.x > width + this.w || this.pos.y < -this.h || this.pos.y > height + this.h ); } } ``` #### Particle System ```javascript class Particle { constructor(x, y, color) { this.pos = createVector(x, y); this.vel = p5.Vector.random2D().mult(random(1, 5)); this.acc = createVector(0, 0.1); this.color = color; this.alpha = 255; this.size = random(3, 8); this.life = 1.0; this.decay = random(0.01, 0.04); } update() { this.vel.add(this.acc); this.pos.add(this.vel); this.life -= this.decay; this.alpha = this.life * 255; } draw() { noStroke(); fill(red(this.color), green(this.color), blue(this.color), this.alpha); ellipse(this.pos.x, this.pos.y, this.size); } isDead() { return this.life <= 0; } } let particles = []; function spawnExplosion(x, y, col, count = 30) { for (let i = 0; i < count; i++) { particles.push(new Particle(x, y, col)); } } function updateParticles() { for (let i = particles.length - 1; i >= 0; i--) { particles[i].update(); particles[i].draw(); if (particles[i].isDead()) particles.splice(i, 1); } } ``` #### Camera / Scrolling ```javascript let camera = { x: 0, y: 0 }; function draw() { background(15); // Follow player camera.x = lerp(camera.x, player.x - width / 2, 0.1); camera.y = lerp(camera.y, player.y - height / 2, 0.1); push(); translate(-camera.x, -camera.y); // Draw world (in world coordinates) drawWorld(); drawPlayer(); drawEnemies(); pop(); // Draw HUD (in screen coordinates) drawHUD(); } ``` #### Tilemap Rendering ```javascript const TILE_SIZE = 32; const tilemap = [ [1, 1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0, 0, 1], [1, 0, 0, 2, 0, 0, 0, 1], [1, 0, 0, 0, 0, 3, 0, 1], [1, 1, 1, 1, 1, 1, 1, 1], ]; const TILE_COLORS = { 0: null, // empty 1: "#4a4a6a", // wall 2: "#22c55e", // item 3: "#ef4444", // enemy }; function drawTilemap() { for (let row = 0; row < tilemap.length; row++) { for (let col = 0; col < tilemap[row].length; col++) { const tile = tilemap[row][col]; if (TILE_COLORS[tile]) { fill(TILE_COLORS[tile]); noStroke(); rect(col * TILE_SIZE, row * TILE_SIZE, TILE_SIZE, TILE_SIZE); } } } } ``` #### Sound Effects (using p5.sound or Howler.js) For sound, prefer Howler.js since p5.sound adds significant bundle size: ```html ``` ```javascript const sounds = { jump: new Howl({ src: ["assets/jump.wav"], volume: 0.5 }), hit: new Howl({ src: ["assets/hit.wav"], volume: 0.7 }), coin: new Howl({ src: ["assets/coin.wav"], volume: 0.4 }), music: new Howl({ src: ["assets/music.mp3"], loop: true, volume: 0.3 }), }; ``` If no sound assets are available, generate simple audio with Tone.js or the Web Audio API: ```javascript function playBeep(freq = 440, duration = 0.1) { const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = freq; gain.gain.setValueAtTime(0.3, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration); osc.start(); osc.stop(ctx.currentTime + duration); } ``` ### p5.js with Physics (Matter.js) For games needing realistic 2D physics (platformers, ragdoll, pinball): ```html ``` ```javascript const { Engine, World, Bodies, Body, Events } = Matter; let engine, world; let ground, player; function setup() { createCanvas(windowWidth, windowHeight); engine = Engine.create(); world = engine.world; ground = Bodies.rectangle(width / 2, height - 20, width, 40, { isStatic: true }); player = Bodies.circle(width / 2, height / 2, 20, { restitution: 0.5 }); World.add(world, [ground, player]); } function draw() { Engine.update(engine); background(15); // Draw ground fill("#4a4a6a"); rectMode(CENTER); rect(ground.position.x, ground.position.y, width, 40); // Draw player fill("#6366f1"); ellipse(player.position.x, player.position.y, 40); } function keyPressed() { if (key === " ") { Body.applyForce(player, player.position, { x: 0, y: -0.05 }); } } ``` ### p5.js Responsive Canvas Always handle window resizing and use the full viewport: ```javascript function setup() { createCanvas(windowWidth, windowHeight); } function windowResized() { resizeCanvas(windowWidth, windowHeight); } ``` For fixed-aspect-ratio games (e.g., retro pixel games), scale the canvas: ```javascript const GAME_W = 320; const GAME_H = 240; let scaleFactor; function setup() { createCanvas(windowWidth, windowHeight); pixelDensity(1); noSmooth(); calcScale(); } function calcScale() { scaleFactor = min(windowWidth / GAME_W, windowHeight / GAME_H); } function draw() { background(0); push(); translate((width - GAME_W * scaleFactor) / 2, (height - GAME_H * scaleFactor) / 2); scale(scaleFactor); // All game drawing at GAME_W x GAME_H logical resolution drawGameWorld(); pop(); } function windowResized() { resizeCanvas(windowWidth, windowHeight); calcScale(); } ``` ### Touch / Mobile Input for p5.js Games ```javascript let touchActive = false; let touchX = 0, touchY = 0; function touchStarted() { touchActive = true; touchX = mouseX; touchY = mouseY; return false; // prevent default } function touchMoved() { touchX = mouseX; touchY = mouseY; return false; } function touchEnded() { touchActive = false; return false; } // Unified input: works for both mouse and touch function getInputX() { return mouseX; } function getInputY() { return mouseY; } function isInputActive() { return mouseIsPressed || touchActive; } ``` ### p5.js High Score Persistence with DuckDB If the game has a `database` permission, persist high scores: ```javascript async function loadHighScore() { try { await window.dench.db.execute(` CREATE TABLE IF NOT EXISTS game_scores ( game TEXT, score INTEGER, played_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); const result = await window.dench.db.query( `SELECT MAX(score) as high_score FROM game_scores WHERE game = 'my-game'`, ); return result.rows?.[0]?.high_score || 0; } catch { return 0; } } async function saveScore(score) { try { await window.dench.db.execute( `INSERT INTO game_scores (game, score) VALUES ('my-game', ${score})`, ); } catch {} } ``` --- ## 3D Games & Experiences with Three.js **Always use Three.js for 3D games, visualizations, and interactive 3D experiences.** Three.js is the default choice for anything 3D. ### When to Use Three.js - 3D games (first-person, third-person, flying, racing) - 3D product viewers and configurators - Terrain and world visualization - 3D data visualization (3D scatter plots, network graphs) - Architectural walkthroughs - Generative 3D art - Physics-based 3D simulations ### Three.js App Template ``` apps/my-3d-app.dench.app/ .dench.yaml index.html app.js # Main Three.js module assets/ model.glb # 3D models (optional) texture.jpg # Textures (optional) ``` **`.dench.yaml`:** ```yaml name: "3D World" description: "An interactive 3D experience" icon: "box" version: "1.0.0" entry: "index.html" runtime: "static" ``` **`index.html`:** ```html 3D World
Loading...
``` **`app.js` (Three.js module skeleton):** ```javascript import * as THREE from "three"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; // --- Scene setup --- const scene = new THREE.Scene(); scene.background = new THREE.Color(0x0f0f1a); scene.fog = new THREE.Fog(0x0f0f1a, 50, 200); const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 5, 10); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; renderer.toneMapping = THREE.ACESFilmicToneMapping; renderer.toneMappingExposure = 1.0; document.body.appendChild(renderer.domElement); // --- Controls --- const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.05; controls.maxPolarAngle = Math.PI / 2; // --- Lighting --- const ambientLight = new THREE.AmbientLight(0x404060, 0.5); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5); directionalLight.position.set(10, 20, 10); directionalLight.castShadow = true; directionalLight.shadow.mapSize.set(2048, 2048); directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 100; directionalLight.shadow.camera.left = -30; directionalLight.shadow.camera.right = 30; directionalLight.shadow.camera.top = 30; directionalLight.shadow.camera.bottom = -30; scene.add(directionalLight); // --- Ground --- const groundGeo = new THREE.PlaneGeometry(200, 200); const groundMat = new THREE.MeshStandardMaterial({ color: 0x1a1a2e, roughness: 0.8 }); const ground = new THREE.Mesh(groundGeo, groundMat); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; scene.add(ground); // --- Objects --- const geometry = new THREE.BoxGeometry(2, 2, 2); const material = new THREE.MeshStandardMaterial({ color: 0x6366f1, roughness: 0.3, metalness: 0.5, }); const cube = new THREE.Mesh(geometry, material); cube.position.y = 1; cube.castShadow = true; scene.add(cube); // --- Theme --- if (window.dench) { window.dench.app .getTheme() .then((theme) => { if (theme === "light") { scene.background = new THREE.Color(0xf0f0f5); scene.fog = new THREE.Fog(0xf0f0f5, 50, 200); groundMat.color.set(0xe8e8f0); } }) .catch(() => {}); } // --- Hide loading screen --- document.getElementById("loading")?.classList.add("hidden"); // --- Animation loop --- const clock = new THREE.Clock(); function animate() { requestAnimationFrame(animate); const dt = clock.getDelta(); const elapsed = clock.getElapsedTime(); cube.rotation.y = elapsed * 0.5; cube.position.y = 1 + Math.sin(elapsed) * 0.5; controls.update(); renderer.render(scene, camera); } animate(); // --- Resize --- window.addEventListener("resize", () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }); ``` ### Three.js Common Addons Load additional Three.js modules as needed via the import map: ```javascript // First-person controls import { PointerLockControls } from "three/addons/controls/PointerLockControls.js"; // GLTF model loading import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; // Post-processing import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"; import { RenderPass } from "three/addons/postprocessing/RenderPass.js"; import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"; // Environment maps import { RGBELoader } from "three/addons/loaders/RGBELoader.js"; // Text import { FontLoader } from "three/addons/loaders/FontLoader.js"; import { TextGeometry } from "three/addons/geometries/TextGeometry.js"; // Sky import { Sky } from "three/addons/objects/Sky.js"; // Water import { Water } from "three/addons/objects/Water.js"; // Physics integration (use cannon-es via CDN) // Add to importmap: "cannon-es": "https://unpkg.com/cannon-es@0.20/dist/cannon-es.js" import * as CANNON from "cannon-es"; ``` ### Three.js First-Person Game Pattern ```javascript import * as THREE from "three"; import { PointerLockControls } from "three/addons/controls/PointerLockControls.js"; const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement); const controls = new PointerLockControls(camera, document.body); // Click to enter pointer lock document.addEventListener("click", () => { if (!controls.isLocked) controls.lock(); }); // Movement state const velocity = new THREE.Vector3(); const direction = new THREE.Vector3(); const keys = { forward: false, backward: false, left: false, right: false, jump: false }; document.addEventListener("keydown", (e) => { switch (e.code) { case "KeyW": case "ArrowUp": keys.forward = true; break; case "KeyS": case "ArrowDown": keys.backward = true; break; case "KeyA": case "ArrowLeft": keys.left = true; break; case "KeyD": case "ArrowRight": keys.right = true; break; case "Space": keys.jump = true; break; } }); document.addEventListener("keyup", (e) => { switch (e.code) { case "KeyW": case "ArrowUp": keys.forward = false; break; case "KeyS": case "ArrowDown": keys.backward = false; break; case "KeyA": case "ArrowLeft": keys.left = false; break; case "KeyD": case "ArrowRight": keys.right = false; break; case "Space": keys.jump = false; break; } }); let onGround = true; const MOVE_SPEED = 50; const JUMP_FORCE = 12; const GRAVITY = 30; const clock = new THREE.Clock(); function animate() { requestAnimationFrame(animate); const dt = Math.min(clock.getDelta(), 0.1); if (controls.isLocked) { // Apply friction velocity.x -= velocity.x * 10.0 * dt; velocity.z -= velocity.z * 10.0 * dt; velocity.y -= GRAVITY * dt; direction.z = Number(keys.forward) - Number(keys.backward); direction.x = Number(keys.right) - Number(keys.left); direction.normalize(); if (keys.forward || keys.backward) velocity.z -= direction.z * MOVE_SPEED * dt; if (keys.left || keys.right) velocity.x -= direction.x * MOVE_SPEED * dt; if (keys.jump && onGround) { velocity.y = JUMP_FORCE; onGround = false; } controls.moveRight(-velocity.x * dt); controls.moveForward(-velocity.z * dt); camera.position.y += velocity.y * dt; if (camera.position.y < 1.7) { velocity.y = 0; camera.position.y = 1.7; onGround = true; } } renderer.render(scene, camera); } animate(); ``` ### Three.js GLTF Model Loading ```javascript import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js"; const loader = new GLTFLoader(); const dracoLoader = new DRACOLoader(); dracoLoader.setDecoderPath("https://unpkg.com/three@0.170/examples/jsm/libs/draco/"); loader.setDRACOLoader(dracoLoader); // Load a model from the app's assets folder loader.load( "assets/model.glb", (gltf) => { const model = gltf.scene; model.traverse((child) => { if (child.isMesh) { child.castShadow = true; child.receiveShadow = true; } }); model.scale.setScalar(1); scene.add(model); // If the model has animations if (gltf.animations.length > 0) { const mixer = new THREE.AnimationMixer(model); const action = mixer.clipAction(gltf.animations[0]); action.play(); // In animate loop: mixer.update(dt); } }, undefined, (error) => { console.error("Model load error:", error); }, ); ``` ### Three.js Post-Processing (Bloom, etc.) ```javascript import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js"; import { RenderPass } from "three/addons/postprocessing/RenderPass.js"; import { UnrealBloomPass } from "three/addons/postprocessing/UnrealBloomPass.js"; const composer = new EffectComposer(renderer); composer.addPass(new RenderPass(scene, camera)); const bloomPass = new UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 0.5, // strength 0.4, // radius 0.85, // threshold ); composer.addPass(bloomPass); // In animate loop, replace renderer.render(scene, camera) with: // composer.render(); // On resize, also update: // composer.setSize(window.innerWidth, window.innerHeight); ``` ### Three.js Procedural Terrain ```javascript function createTerrain(width, depth, resolution) { const geometry = new THREE.PlaneGeometry(width, depth, resolution, resolution); const vertices = geometry.attributes.position.array; for (let i = 0; i < vertices.length; i += 3) { const x = vertices[i]; const z = vertices[i + 1]; vertices[i + 2] = noise(x * 0.02, z * 0.02) * 15; } geometry.computeVertexNormals(); const material = new THREE.MeshStandardMaterial({ color: 0x3a7d44, roughness: 0.9, flatShading: true, }); const mesh = new THREE.Mesh(geometry, material); mesh.rotation.x = -Math.PI / 2; mesh.receiveShadow = true; return mesh; } // Simple noise function (for procedural generation without dependencies) function noise(x, y) { const n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453; return n - Math.floor(n); } ``` ### Three.js HUD / UI Overlay Since Three.js renders to a canvas, use HTML overlays for UI: ```html
``` ```javascript function updateHUD(score, health) { document.getElementById("score").textContent = `Score: ${score}`; document.getElementById("health-fill").style.width = `${health}%`; document.getElementById("health-fill").style.background = health > 50 ? "#22c55e" : health > 25 ? "#f59e0b" : "#ef4444"; } ``` --- ## Full Game Examples ### Example 1: Arcade Game (p5.js) A complete asteroid-dodge game with scoring, particles, and game states. **`.dench.yaml`:** ```yaml name: "Asteroid Dodge" description: "Dodge the falling asteroids! Arrow keys or WASD to move." icon: "rocket" version: "1.0.0" entry: "index.html" runtime: "static" ``` **`index.html`:** ```html Asteroid Dodge ``` **`game.js`:** ```javascript const State = { MENU: 0, PLAY: 1, OVER: 2 }; let state = State.MENU; let player, asteroids, particles, stars; let score, highScore = 0, spawnTimer, difficulty; function setup() { createCanvas(windowWidth, windowHeight); textFont("system-ui"); stars = Array.from({ length: 100 }, () => ({ x: random(width), y: random(height), s: random(1, 3), b: random(100, 255), })); if (window.dench) { window.dench.app.getTheme().catch(() => {}); } } function resetGame() { player = { x: width / 2, y: height - 80, size: 24, speed: 5, lives: 3, invincible: 0 }; asteroids = []; particles = []; score = 0; spawnTimer = 0; difficulty = 1; } function draw() { background(10, 10, 26); drawStars(); switch (state) { case State.MENU: drawMenu(); break; case State.PLAY: updateGame(); drawGame(); drawHUD(); break; case State.OVER: drawGame(); drawHUD(); drawGameOver(); break; } } function drawStars() { noStroke(); for (const s of stars) { fill(255, s.b); ellipse(s.x, s.y, s.s); s.y += s.s * 0.3; if (s.y > height) { s.y = 0; s.x = random(width); } } } function drawMenu() { fill(255); textAlign(CENTER, CENTER); textSize(min(width * 0.08, 56)); textStyle(BOLD); text("ASTEROID DODGE", width / 2, height / 2 - 60); textSize(min(width * 0.03, 18)); textStyle(NORMAL); fill(180); text("Arrow keys or WASD to move", width / 2, height / 2 + 10); fill(99, 102, 241); text("Press SPACE or ENTER to start", width / 2, height / 2 + 50); if (highScore > 0) { fill(120); textSize(14); text("High Score: " + highScore, width / 2, height / 2 + 90); } } function updateGame() { // Player movement if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) player.x -= player.speed; if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) player.x += player.speed; if (keyIsDown(UP_ARROW) || keyIsDown(87)) player.y -= player.speed; if (keyIsDown(DOWN_ARROW) || keyIsDown(83)) player.y += player.speed; player.x = constrain(player.x, player.size, width - player.size); player.y = constrain(player.y, player.size, height - player.size); if (player.invincible > 0) player.invincible--; // Spawn asteroids difficulty = 1 + score / 500; spawnTimer++; if (spawnTimer > max(15, 45 - difficulty * 3)) { asteroids.push({ x: random(width), y: -30, size: random(15, 35), vy: random(2, 4) * difficulty, vx: random(-1, 1), rot: random(TWO_PI), rotSpeed: random(-0.05, 0.05), }); spawnTimer = 0; } // Update asteroids for (let i = asteroids.length - 1; i >= 0; i--) { const a = asteroids[i]; a.y += a.vy; a.x += a.vx; a.rot += a.rotSpeed; if (a.y > height + 50) { asteroids.splice(i, 1); score += 10; continue; } // Collision if (player.invincible <= 0 && dist(player.x, player.y, a.x, a.y) < player.size + a.size / 2) { spawnParticles(a.x, a.y, color(239, 68, 68), 20); asteroids.splice(i, 1); player.lives--; player.invincible = 90; if (player.lives <= 0) { highScore = max(highScore, score); state = State.OVER; } } } // Update particles for (let i = particles.length - 1; i >= 0; i--) { const p = particles[i]; p.x += p.vx; p.y += p.vy; p.vy += 0.05; p.life -= 0.02; if (p.life <= 0) particles.splice(i, 1); } score++; } function drawGame() { // Draw asteroids for (const a of asteroids) { push(); translate(a.x, a.y); rotate(a.rot); fill(120, 120, 140); stroke(80, 80, 100); strokeWeight(1); beginShape(); for (let i = 0; i < 7; i++) { const angle = map(i, 0, 7, 0, TWO_PI); const r = (a.size / 2) * (0.7 + 0.3 * sin(i * 2.5)); vertex(cos(angle) * r, sin(angle) * r); } endShape(CLOSE); pop(); } // Draw particles noStroke(); for (const p of particles) { fill(red(p.col), green(p.col), blue(p.col), p.life * 255); ellipse(p.x, p.y, p.size * p.life); } // Draw player if (state === State.PLAY) { if (player.invincible <= 0 || frameCount % 6 < 3) { push(); translate(player.x, player.y); fill(99, 102, 241); noStroke(); triangle( 0, -player.size, -player.size * 0.6, player.size * 0.6, player.size * 0.6, player.size * 0.6, ); fill(129, 140, 248); triangle( 0, -player.size * 0.5, -player.size * 0.3, player.size * 0.3, player.size * 0.3, player.size * 0.3, ); pop(); } } } function drawHUD() { fill(255); noStroke(); textAlign(LEFT, TOP); textSize(20); textStyle(BOLD); text("Score: " + score, 20, 20); textStyle(NORMAL); textSize(14); fill(200); for (let i = 0; i < player.lives; i++) { fill(239, 68, 68); ellipse(20 + i * 22, 55, 14); } } function drawGameOver() { fill(0, 0, 0, 150); rect(0, 0, width, height); fill(239, 68, 68); textAlign(CENTER, CENTER); textSize(min(width * 0.07, 48)); textStyle(BOLD); text("GAME OVER", width / 2, height / 2 - 40); fill(255); textSize(22); textStyle(NORMAL); text("Score: " + score, width / 2, height / 2 + 10); fill(180); textSize(16); text("Press SPACE to play again", width / 2, height / 2 + 50); } function spawnParticles(x, y, col, count) { for (let i = 0; i < count; i++) { const angle = random(TWO_PI); const speed = random(1, 5); particles.push({ x, y, vx: cos(angle) * speed, vy: sin(angle) * speed, size: random(4, 10), col, life: 1.0, }); } } function keyPressed() { if (state === State.MENU && (key === " " || key === "Enter")) { state = State.PLAY; resetGame(); } else if (state === State.OVER && (key === " " || key === "Enter")) { state = State.PLAY; resetGame(); } } function windowResized() { resizeCanvas(windowWidth, windowHeight); } ``` ### Example 2: 3D Scene Viewer (Three.js) **`.dench.yaml`:** ```yaml name: "3D Playground" description: "Interactive 3D scene with orbit controls" icon: "box" version: "1.0.0" entry: "index.html" runtime: "static" ``` **`index.html`:** ```html 3D Playground ``` **`scene.js`:** ```javascript import * as THREE from "three"; import { OrbitControls } from "three/addons/controls/OrbitControls.js"; const scene = new THREE.Scene(); let bgColor = 0x0f0f1a; if (window.dench) { window.dench.app .getTheme() .then((t) => { bgColor = t === "light" ? 0xf0f0f5 : 0x0f0f1a; scene.background = new THREE.Color(bgColor); scene.fog = new THREE.Fog(bgColor, 30, 100); }) .catch(() => {}); } scene.background = new THREE.Color(bgColor); scene.fog = new THREE.Fog(bgColor, 30, 100); const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 500); camera.position.set(8, 6, 12); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(innerWidth, innerHeight); renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; document.body.appendChild(renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; scene.add(new THREE.AmbientLight(0x404060, 0.6)); const sun = new THREE.DirectionalLight(0xffffff, 1.5); sun.position.set(10, 20, 10); sun.castShadow = true; sun.shadow.mapSize.set(1024, 1024); scene.add(sun); const ground = new THREE.Mesh( new THREE.PlaneGeometry(60, 60), new THREE.MeshStandardMaterial({ color: 0x1a1a2e, roughness: 0.8 }), ); ground.rotation.x = -Math.PI / 2; ground.receiveShadow = true; scene.add(ground); const shapes = []; const colors = [0x6366f1, 0x22c55e, 0xf59e0b, 0xef4444, 0x06b6d4]; for (let i = 0; i < 12; i++) { const geos = [ new THREE.BoxGeometry(1, 1, 1), new THREE.SphereGeometry(0.6, 32, 32), new THREE.ConeGeometry(0.5, 1.2, 6), new THREE.TorusGeometry(0.5, 0.2, 16, 32), new THREE.OctahedronGeometry(0.6), ]; const geo = geos[Math.floor(Math.random() * geos.length)]; const mat = new THREE.MeshStandardMaterial({ color: colors[Math.floor(Math.random() * colors.length)], roughness: 0.3, metalness: 0.5, }); const mesh = new THREE.Mesh(geo, mat); mesh.position.set( (Math.random() - 0.5) * 16, 0.5 + Math.random() * 3, (Math.random() - 0.5) * 16, ); mesh.castShadow = true; mesh.userData = { baseY: mesh.position.y, phase: Math.random() * Math.PI * 2, rotSpeed: (Math.random() - 0.5) * 0.02, }; scene.add(mesh); shapes.push(mesh); } const clock = new THREE.Clock(); function animate() { requestAnimationFrame(animate); const t = clock.getElapsedTime(); for (const s of shapes) { s.position.y = s.userData.baseY + Math.sin(t + s.userData.phase) * 0.4; s.rotation.y += s.userData.rotSpeed; } controls.update(); renderer.render(scene, camera); } animate(); addEventListener("resize", () => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth, innerHeight); }); ```