--- name: phaser description: > Build 2D browser games with Phaser 3 using scene-based architecture and centralized state. Use when creating a new 2D game, adding 2D game features, working with Phaser, or building sprite-based web games. argument-hint: [topic or question] --- # Phaser 3 Game Development You are an expert Phaser game developer building games with the game-creator plugin. Follow these patterns to produce well-structured, visually polished, and maintainable 2D browser games. ## Core Principles 1. **Core loop first** — Implement the minimum gameplay loop before any polish: boot → preload → create → update. Add the win/lose condition and scoring **before** visuals, audio, or juice. Keep initial scope small: 1 scene, 1 mechanic, 1 fail condition. 2. **TypeScript-first** — Always use TypeScript for type safety and IDE support 3. **Scene-based architecture** — Each game screen is a Scene; keep them focused 4. **Vite bundling** — Use the official `phaserjs/template-vite-ts` template 5. **Composition over inheritance** — Prefer composing behaviors over deep class hierarchies 6. **Data-driven design** — Define levels, enemies, and configs in JSON/data files 7. **Event-driven communication** — All cross-scene/system communication via EventBus 8. **Restart-safe** — Gameplay must be fully restart-safe and deterministic. `GameState.reset()` must restore a clean slate. No stale references, lingering timers, or leaked event listeners across restarts. ## Mandatory Conventions All games MUST follow the [game-creator conventions](conventions.md): - **`core/` directory** with EventBus, GameState, and Constants - **EventBus singleton** — `domain:action` event naming, no direct scene references - **GameState singleton** — Centralized state with `reset()` for clean restarts - **Constants file** — Every magic number, color, speed, and config value — zero hardcoded values - **Scene cleanup** — Remove EventBus listeners in `shutdown()` See [conventions.md](conventions.md) for full details and code examples. ## Project Setup Use the official Vite + TypeScript template as your starting point: ```bash npx degit phaserjs/template-vite-ts my-game cd my-game && npm install ``` ### Required Directory Structure ``` src/ ├── core/ │ ├── EventBus.ts # Singleton event bus + event constants │ ├── GameState.ts # Centralized state with reset() │ └── Constants.ts # ALL config values ├── scenes/ │ ├── Boot.ts # Minimal setup, start Game scene │ ├── Preloader.ts # Load all assets, show progress bar │ ├── Game.ts # Main gameplay (starts immediately, no title screen) │ └── GameOver.ts # End screen with restart ├── objects/ # Game entities (Player, Enemy, etc.) ├── systems/ # Managers and subsystems ├── ui/ # UI components (buttons, bars, dialogs) ├── audio/ # Audio manager, music, SFX ├── config.ts # Phaser.Types.Core.GameConfig └── main.ts # Entry point ``` See [project-setup.md](project-setup.md) for full config and tooling details. ## Scene Architecture - **Lifecycle**: `init()` → `preload()` → `create()` → `update(time, delta)` - Use `init()` for receiving data from scene transitions - Load assets in a dedicated `Preloader` scene, not in every scene - Keep `update()` lean — delegate to subsystems and game objects - **No title screen by default** — boot directly into gameplay. Only add a title/menu scene if the user explicitly asks for one - **No in-game score HUD** — the Play.fun widget displays score in a deadzone at the top of the game. Do not create a separate UIScene or HUD overlay for score display - Use parallel scenes for UI overlays (pause menu) only when requested ### Play.fun Safe Zone When games are rendered inside Play.fun (or with the Play.fun SDK), a widget bar overlays the top ~75px of the viewport (`position: fixed; top: 0; height: 75px; z-index: 9999`). The template defines `SAFE_ZONE.TOP` in Constants.js for this purpose. **Rules:** - All UI text, buttons, and HUD elements must be positioned below `SAFE_ZONE.TOP` - Gameplay entities should not spawn in the safe zone area - The game-over screen, score panels, and restart buttons must all offset from `SAFE_ZONE.TOP` - Use `const usableH = GAME.HEIGHT - SAFE_ZONE.TOP` for calculating proportional positions in UI scenes ```js import { SAFE_ZONE } from '../core/Constants.js'; // In any UI scene: const safeTop = SAFE_ZONE.TOP; const usableH = GAME.HEIGHT - safeTop; const title = this.add.text(cx, safeTop + usableH * 0.15, 'GAME OVER', { ... }); const button = createButton(scene, cx, safeTop + usableH * 0.6, 'PLAY AGAIN', callback); ``` - Communicate between scenes via EventBus (not direct references) See [scenes-and-lifecycle.md](scenes-and-lifecycle.md) for patterns and examples. ## Game Objects - Extend `Phaser.GameObjects.Sprite` (or other base classes) for custom objects - Use `Phaser.GameObjects.Group` for object pooling (bullets, coins, enemies) - Use `Phaser.GameObjects.Container` for composite objects, but avoid deep nesting - Register custom objects with `GameObjectFactory` for scene-level access See [game-objects.md](game-objects.md) for implementation patterns. ## Physics - **Arcade Physics** — Use for simple games (platformers, top-down). Fast and lightweight. - **Matter.js** — Use when you need realistic collisions, constraints, or complex shapes. - Never mix physics engines in the same game. - Use the **state pattern** for character movement (idle, walk, jump, attack). See [physics-and-movement.md](physics-and-movement.md) for details. ## Performance (Critical Rules) - **Use texture atlases** — Pack sprites into atlases, never load individual images at scale - **Object pooling** — Use Groups with `maxSize`; recycle with `setActive(false)` / `setVisible(false)` - **Minimize update work** — Only iterate active objects; use `getChildren().filter(c => c.active)` - **Camera culling** — Enable for large worlds; off-screen objects skip rendering - **Batch rendering** — Fewer unique textures per frame = better draw call batching - **Mobile** — Reduce particle counts, simplify physics, consider 30fps target - **`pixelArt: true`** — Enable in game config for pixel art games (nearest-neighbor scaling) See [assets-and-performance.md](assets-and-performance.md) for full optimization guide. ## Advanced Patterns - **ECS with bitECS** — Entity Component System for data-oriented design (used internally by Phaser 4) - **State machines** — Manage entity behavior states cleanly - **Singleton managers** — Cross-scene services (audio, save data, analytics) - **Event bus** — Decouple systems with a shared EventEmitter - **Tiled integration** — Use Tiled map editor for level design See [patterns.md](patterns.md) for implementations. ## Mobile Input Strategy (60/40 Rule) All games MUST work on desktop AND mobile unless explicitly specified otherwise. Focus 60% mobile / 40% desktop for tradeoffs. Pick the best mobile input for each game concept: | Game Type | Primary Mobile Input | Desktop Input | |-----------|---------------------|---------------| | Platformer | Tap left/right half + tap-to-jump | Arrow keys / WASD | | Runner/endless | Tap / swipe up to jump | Space / Up arrow | | Puzzle/match | Tap targets (44px min) | Click | | Shooter | Virtual joystick + tap-to-fire | Mouse + WASD | | Top-down | Virtual joystick | Arrow keys / WASD | ### Implementation Pattern Abstract input into an `inputState` object so game logic is source-agnostic: ```typescript // In Scene update(): const isMobile = this.sys.game.device.os.android || this.sys.game.device.os.iOS || this.sys.game.device.os.iPad; let left = false, right = false, jump = false; // Keyboard left = this.cursors.left.isDown || this.wasd.left.isDown; right = this.cursors.right.isDown || this.wasd.right.isDown; jump = Phaser.Input.Keyboard.JustDown(this.spaceKey); // Touch (merge with keyboard) if (isMobile) { // Left half tap = left, right half = right, or use tap zones this.input.on('pointerdown', (p) => { if (p.x < this.scale.width / 2) left = true; else right = true; }); } this.player.update({ left, right, jump }); ``` ### Responsive Canvas Config (Retina/High-DPI) For pixel-perfect rendering on any display, size the canvas to match the user's device pixel area (not a fixed base resolution). This prevents CSS-upscaling blur on high-DPI screens. ```typescript // Constants.ts export const DPR = Math.min(window.devicePixelRatio || 1, 2); const isPortrait = window.innerHeight > window.innerWidth; const designW = isPortrait ? 540 : 960; const designH = isPortrait ? 960 : 540; const designAspect = designW / designH; // Canvas = device pixel area, maintaining design aspect ratio const deviceW = window.innerWidth * DPR; const deviceH = window.innerHeight * DPR; let canvasW, canvasH; if (deviceW / deviceH > designAspect) { canvasW = deviceW; canvasH = Math.round(deviceW / designAspect); } else { canvasW = Math.round(deviceH * designAspect); canvasH = deviceH; } // PX = canvas pixels per design pixel. Scale ALL absolute values by PX. export const PX = canvasW / designW; export const GAME = { WIDTH: canvasW, // e.g., 3456 on a 1728×1117 @2x display HEIGHT: canvasH, GRAVITY: 800 * PX, }; // GameConfig.ts scale: { mode: Phaser.Scale.FIT, autoCenter: Phaser.Scale.CENTER_BOTH, zoom: 1 / DPR, }, roundPixels: true, antialias: true, // All absolute pixel values use PX (not DPR). Proportional values use ratios. const groundH = 30 * PX; const buttonY = GAME.HEIGHT * 0.55; ``` ### Entity Sizing Entity dimensions in Constants.js should be proportional to game dimensions, not fixed pixel values: ```js // Good — proportional to screen PLAYER: { WIDTH: GAME.WIDTH * 0.08, HEIGHT: GAME.HEIGHT * 0.12, } // Bad — fixed size regardless of screen PLAYER: { WIDTH: 40 * PX, HEIGHT: 40 * PX, } ``` For **character-driven games** (named characters, personalities, mascots), make characters prominent — use 12–15% of `GAME.WIDTH` for the player width. Use **bobblehead proportions** (oversized head ~40–50% of sprite height, compact body) for personality games to maximize character recognition at any scale. **HTML boilerplate** (required for proper scaling): ```html ``` ### Button Pattern (Container + Graphics + Text) Buttons require careful z-ordering. Use a Container holding Graphics (background) then Text (label) — in that order. The Container itself is interactive. **ALWAYS use this exact pattern for clickable buttons.** Do not use Zone, do not draw Graphics on top of Text, and do not set interactivity on anything other than the Container. ```js createButton(scene, x, y, label, callback) { const btnW = Math.max(GAME.WIDTH * UI.BTN_W_RATIO, 160); const btnH = Math.max(GAME.HEIGHT * UI.BTN_H_RATIO, UI.MIN_TOUCH); const radius = UI.BTN_RADIUS; const container = scene.add.container(x, y); // 1. Graphics background (added FIRST — renders behind text) const bg = scene.add.graphics(); bg.fillStyle(COLORS.BTN_PRIMARY, 1); bg.fillRoundedRect(-btnW / 2, -btnH / 2, btnW, btnH, radius); container.add(bg); // 2. Text label (added SECOND — renders on top of background) const fontSize = Math.round(GAME.HEIGHT * UI.BODY_RATIO); const text = scene.add.text(0, 0, label, { fontSize: fontSize + 'px', fontFamily: UI.FONT, color: COLORS.BTN_TEXT, fontStyle: 'bold', }).setOrigin(0.5); container.add(text); // 3. Make the CONTAINER interactive (not the graphics or text) container.setSize(btnW, btnH); container.setInteractive({ useHandCursor: true }); const fillBtn = (gfx, color) => { gfx.clear(); gfx.fillStyle(color, 1); gfx.fillRoundedRect(-btnW / 2, -btnH / 2, btnW, btnH, radius); }; container.on('pointerover', () => { fillBtn(bg, COLORS.BTN_PRIMARY_HOVER); scene.tweens.add({ targets: container, scaleX: 1.05, scaleY: 1.05, duration: 80 }); }); container.on('pointerout', () => { fillBtn(bg, COLORS.BTN_PRIMARY); scene.tweens.add({ targets: container, scaleX: 1, scaleY: 1, duration: 80 }); }); container.on('pointerdown', () => { fillBtn(bg, COLORS.BTN_PRIMARY_PRESS); container.setScale(0.95); }); container.on('pointerup', () => { container.setScale(1); callback(); }); return container; } ``` **Broken patterns** (do NOT use): - Drawing Graphics on top of Text (hides the label) - Using a Zone for interactivity with Graphics drawn over it (Zone becomes unreachable) - Setting `setAlpha(0)` on an interactive object and layering visuals over it ## Anti-Patterns (Avoid These) - **Bloated `update()` methods** — Don't put all game logic in one giant update with nested conditionals. Delegate to objects and systems. - **Overwriting Scene injection map properties** — Never name your properties `world`, `input`, `cameras`, `add`, `make`, `scene`, `sys`, `game`, `cache`, `registry`, `sound`, `textures`, `events`, `physics`, `matter`, `time`, `tweens`, `lights`, `data`, `load`, `anims`, `renderer`, or `plugins`. These are reserved by Phaser. - **Creating objects in `update()` without pooling** — This causes GC spikes. Always pool frequently created/destroyed objects. Avoid expensive per-frame allocations — reuse objects, arrays, and temporary variables. - **Loading individual sprites instead of atlases** — Each separate texture is a draw call. Pack them. - **Tightly coupling scenes** — Don't store direct references between scenes. Use EventBus. - **Ignoring `delta` in update** — Always use `delta` for time-based movement, not frame-based. - **Deep container nesting** — Containers disable render batching for children. Keep hierarchy flat. - **Not cleaning up** — Remove event listeners and timers in `shutdown()` to prevent memory leaks. This is critical for restart-safety — stale listeners cause double-firing and ghost behavior after restart. - **Hardcoded values** — Every number belongs in `Constants.ts`. No magic numbers in game logic. - **Unwired physics colliders** — Creating a static body with `physics.add.existing(obj, true)` does nothing on its own. You MUST call `physics.add.collider(bodyA, bodyB, callback)` to connect two bodies. Every static collider (ground, walls, platforms) needs an explicit collider or overlap call wiring it to the entities that should interact with it. - **Invisible or hidden button elements** — Never set `setAlpha(0)` on an interactive game object and layer Graphics or other display objects on top. **For buttons, always use the Container + Graphics + Text pattern** (see Button Pattern section above). Common broken patterns: (1) Drawing a Graphics rect after adding Text, hiding the label behind it. (2) Creating a Zone for hit area with Graphics drawn over it, making the Zone unreachable. (3) Making Text interactive but covering it with a Graphics background drawn afterward. The fix is always: Container first, Graphics added to container, Text added to container (in that order), Container is the interactive element. - **No mute toggle** — Games with audio MUST have a mute/unmute mechanism. Store a global `isMuted` flag in GameState. Both BGM and SFX must check it before playing. Wire it to a UI button or keyboard shortcut (M key). ## Examples - [Simple Game](examples/simple-game.md) — Minimal complete Phaser game (collector game) - [Complex Game](examples/complex-game.md) — Multi-scene game with state machines, pooling, EventBus, and all conventions ## Pre-Ship Validation Checklist Before considering a game complete, verify: - [ ] **Core loop works** — Player can start, play, lose/win, and see the result - [ ] **Restart works cleanly** — `GameState.reset()` restores a clean slate, no stale listeners or timers - [ ] **Touch + keyboard input** — Game works on mobile (tap/swipe) and desktop (keyboard/mouse) - [ ] **Responsive canvas** — `Scale.FIT` + `CENTER_BOTH` + `zoom: 1/DPR` with DPR-multiplied dimensions, crisp on Retina - [ ] **All values in Constants** — Zero hardcoded magic numbers in game logic - [ ] **EventBus only** — No direct cross-scene/module imports for communication - [ ] **Scene cleanup** — All EventBus listeners removed in `shutdown()` - [ ] **Physics wired** — Every static body has an explicit `collider()` or `overlap()` call - [ ] **Object pooling** — Frequently created/destroyed objects use Groups with `maxSize` - [ ] **Delta-based movement** — All motion uses `delta`, not frame count - [ ] **Mute toggle** — Audio can be muted/unmuted; `isMuted` state is respected - [ ] **Build passes** — `npm run build` succeeds with no errors - [ ] **No console errors** — Game runs without uncaught exceptions or WebGL failures ## References | File | Topic | |------|-------| | [conventions.md](conventions.md) | Mandatory game-creator architecture conventions | | [project-setup.md](project-setup.md) | Scaffolding, Vite, TypeScript config | | [scenes-and-lifecycle.md](scenes-and-lifecycle.md) | Scene system deep dive | | [game-objects.md](game-objects.md) | Custom objects, groups, containers | | [physics-and-movement.md](physics-and-movement.md) | Physics engines, movement patterns | | [assets-and-performance.md](assets-and-performance.md) | Assets, optimization, mobile | | [patterns.md](patterns.md) | ECS, state machines, singletons | | [no-asset-design.md](no-asset-design.md) | Procedural visuals: gradients, parallax, particles, juice |