>
---
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]
license: MIT
metadata:
author: OpusGameLabs
version: 1.3.0
tags: [game, 2d, phaser, scenes, sprites, arcade-physics]
---
# 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. Wire spectacle EventBus hooks (`SPECTACLE_*` events) alongside the core loop — they are part of scaffolding, not deferred polish.
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.
## Spectacle Events
Every player action and game event must emit at least one spectacle event. These hooks exist in the template EventBus — the design pass attaches visual effects to them.
| Event | Constant | When to Emit |
|-------|----------|--------------|
| `spectacle:entrance` | `SPECTACLE_ENTRANCE` | In `create()` when the player/entities first appear on screen |
| `spectacle:action` | `SPECTACLE_ACTION` | On every player input (tap, jump, shoot, swipe) |
| `spectacle:hit` | `SPECTACLE_HIT` | When player hits/destroys an enemy, collects an item, or scores |
| `spectacle:combo` | `SPECTACLE_COMBO` | When consecutive hits/scores happen without a miss. Pass `{ combo: n }` |
| `spectacle:streak` | `SPECTACLE_STREAK` | When combo reaches milestones (5, 10, 25, 50). Pass `{ streak: n }` |
| `spectacle:near_miss` | `SPECTACLE_NEAR_MISS` | When player narrowly avoids danger (within ~20% of collision radius) |
**Rule**: If a gameplay moment has no spectacle event, add one. The design pass cannot polish what it cannot hook into.
## 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 run inside the Play.fun dashboard on mobile Safari, the SDK sets CSS custom properties on the game iframe's `document.documentElement`:
- `--ogp-safe-top-inset` — space below the Play.fun header bubbles (~68px on mobile)
- `--ogp-safe-bottom-inset` — space above Safari bottom controls (~148px on mobile)
Both default to `0px` when not running inside the dashboard (desktop, standalone).
The template's `Constants.js` reads these at boot and exposes `SAFE_ZONE.TOP` and `SAFE_ZONE.BOTTOM` in canvas pixels (CSS value × DPR). A static fallback (`GAME.HEIGHT * 0.08`) ensures the top safe zone works even without the SDK.
**Rules:**
- All UI text, buttons, and HUD elements must be positioned below `SAFE_ZONE.TOP` and above `GAME.HEIGHT - SAFE_ZONE.BOTTOM`
- Gameplay entities should not spawn in the safe zone areas
- The game-over screen, score panels, and restart buttons must offset from both `SAFE_ZONE.TOP` and `SAFE_ZONE.BOTTOM`
- Use `const usableH = GAME.HEIGHT - SAFE_ZONE.TOP - SAFE_ZONE.BOTTOM` for calculating proportional positions in UI scenes
- Game canvas and backgrounds should fill the full viewport (bleed behind browser chrome)
- Touch controls at the bottom must account for `SAFE_ZONE.BOTTOM`
```js
import { SAFE_ZONE } from '../core/Constants.js';
// In any UI scene:
const safeTop = SAFE_ZONE.TOP;
const safeBottom = SAFE_ZONE.BOTTOM;
const usableH = GAME.HEIGHT - safeTop - safeBottom;
const title = this.add.text(cx, safeTop + usableH * 0.15, 'GAME OVER', { ... });
const button = createButton(scene, cx, safeTop + usableH * 0.6, 'PLAY AGAIN', callback);
// Touch controls / bottom HUD:
const bottomY = GAME.HEIGHT - safeBottom - 40 * PX;
```
**How it works in Constants.js:**
```js
function _readSafeInsets() {
const s = getComputedStyle(document.documentElement);
const top = parseInt(s.getPropertyValue('--ogp-safe-top-inset')) || 0;
const bottom = parseInt(s.getPropertyValue('--ogp-safe-bottom-inset')) || 0;
return { top: top * DPR, bottom: bottom * DPR };
}
const _insets = _readSafeInsets();
export const SAFE_ZONE = {
TOP: Math.max(GAME.HEIGHT * 0.08, _insets.top),
BOTTOM: _insets.bottom,
LEFT: 0,
RIGHT: 0,
};
```
- 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)
See [project-setup.md](project-setup.md) for the full responsive canvas config, entity sizing, HTML boilerplate, and portrait-first game patterns.
### Visible Touch Controls
Always show visual touch indicators on touch-capable devices — never rely on invisible tap zones. Use **capability detection** (not OS-based detection) to determine touch support:
```js
// Good — detects touch laptops, tablets, 2-in-1s
const hasTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
// Bad — misses touch-screen laptops, iPadOS (reports as desktop)
const isMobile = device.os.android || device.os.iOS;
```
Render semi-transparent arrow buttons (or direction indicators) at the bottom of the screen. Use `TOUCH` constants from Constants.js for sizing (12% of canvas width), alpha (0.35 idle / 0.6 active), and margins. Update alpha in the `update()` loop based on input state for visual feedback.
Enable pointer input (pointerdown, pointermove, pointerup) on **all** devices — pointer events work for both mouse and touch. This eliminates the need for separate mobile/desktop input code paths.
### Minimum Entity Sizes for Mobile
Collectibles, hazards, and interactive items must be at least **7–8% of `GAME.WIDTH`** to be recognizable on phone screens. Smaller entities become indistinguishable blobs on mobile.
```js
// Good — recognizable on mobile
ATTACK_WIDTH: _canvasW * 0.09,
POWERUP_WIDTH: _canvasW * 0.072,
// Bad — too small on phone screens
ATTACK_WIDTH: _canvasW * 0.04,
POWERUP_WIDTH: _canvasW * 0.035,
```
For the main player character, use 12–15% of `GAME.WIDTH` (see Entity Sizing above).
### Button Pattern (Container + Graphics + Text)
See [game-objects.md](game-objects.md) for the full button implementation pattern (Container + Graphics + Text with hover/press states) and the list of broken patterns to avoid.
## 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 [game-objects.md](game-objects.md)). 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** — See the `mute-button` rule. Games with audio must have a mute toggle.
## 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** — See `mute-button` rule
- [ ] **Spectacle hooks wired** — Every player action and game event emits a `SPECTACLE_*` event; entrance sequence fires in `create()`
- [ ] **Build passes** — `npm run build` succeeds with no errors
- [ ] **No console errors** — Game runs without uncaught exceptions or WebGL failures
## Reference Files
| File | Topic |
|------|-------|
| [conventions.md](conventions.md) | Mandatory game-creator architecture conventions |
| [project-setup.md](project-setup.md) | Scaffolding, Vite, TypeScript config, responsive canvas, entity sizing, portrait mode |
| [scenes-and-lifecycle.md](scenes-and-lifecycle.md) | Scene system deep dive |
| [game-objects.md](game-objects.md) | Custom objects, groups, containers, button pattern |
| [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 |
Creator's repository · opusgamelabs/game-creator
License: MIT