phaser
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePhaser 3 Game Development
Phaser 3 游戏开发
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.
你是一位资深Phaser游戏开发者,正在使用game-creator插件开发游戏。请遵循以下模式,构建结构清晰、视觉精美且易于维护的2D浏览器游戏。
Core Principles
核心原则
- 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 (events) alongside the core loop — they are part of scaffolding, not deferred polish.
SPECTACLE_* - TypeScript-first — Always use TypeScript for type safety and IDE support
- Scene-based architecture — Each game screen is a Scene; keep them focused
- Vite bundling — Use the official template
phaserjs/template-vite-ts - Composition over inheritance — Prefer composing behaviors over deep class hierarchies
- Data-driven design — Define levels, enemies, and configs in JSON/data files
- Event-driven communication — All cross-scene/system communication via EventBus
- Restart-safe — Gameplay must be fully restart-safe and deterministic. must restore a clean slate. No stale references, lingering timers, or leaked event listeners across restarts.
GameState.reset()
- 优先实现核心循环 — 在进行任何优化之前,先实现最小化游戏循环:boot → preload → create → update。在添加视觉效果、音频或趣味元素之前,先加入胜负条件和计分系统。初始范围要小:1个场景、1种机制、1种失败条件。在核心循环中同时接入Spectacle EventBus钩子(事件)—— 这些是框架的一部分,而非后期优化内容。
SPECTACLE_* - 优先使用TypeScript — 始终使用TypeScript以确保类型安全并获得IDE支持
- 基于场景的架构 — 每个游戏界面对应一个Scene;保持场景职责单一
- Vite打包 — 使用官方模板
phaserjs/template-vite-ts - 组合优于继承 — 优先通过组合实现行为,而非使用深层类继承
- 数据驱动设计 — 在JSON/数据文件中定义关卡、敌人和配置信息
- 事件驱动通信 — 所有跨场景/系统的通信均通过EventBus实现
- 支持安全重启 — 游戏玩法必须完全支持安全重启且结果可预测。必须能恢复至初始干净状态。重启后不能存在陈旧引用、残留计时器或未移除的事件监听器。
GameState.reset()
Spectacle Events
Spectacle事件
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 |
|---|---|---|
| | In |
| | On every player input (tap, jump, shoot, swipe) |
| | When player hits/destroys an enemy, collects an item, or scores |
| | When consecutive hits/scores happen without a miss. Pass |
| | When combo reaches milestones (5, 10, 25, 50). Pass |
| | 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.
每个玩家操作和游戏事件都必须至少触发一个spectacle事件。这些钩子已内置在模板的EventBus中——设计阶段会为其附加视觉效果。
| 事件 | 常量 | 触发时机 |
|---|---|---|
| | 在 |
| | 每次玩家输入时(点击、跳跃、射击、滑动) |
| | 当玩家击中/消灭敌人、收集物品或得分时 |
| | 连续命中/得分且未失误时。需传入 |
| | 连击数达到里程碑时(5、10、25、50)。需传入 |
| | 玩家侥幸避开危险时(距离碰撞半径约20%以内) |
规则:如果某个游戏时刻没有对应的spectacle事件,请添加一个。设计阶段无法对无法挂钩的内容进行优化。
Mandatory Conventions
强制遵循的规范
All games MUST follow the game-creator conventions:
- directory with EventBus, GameState, and Constants
core/ - EventBus singleton — event naming, no direct scene references
domain:action - GameState singleton — Centralized state with for clean restarts
reset() - Constants file — Every magic number, color, speed, and config value — zero hardcoded values
- Scene cleanup — Remove EventBus listeners in
shutdown()
See conventions.md for full details and code examples.
所有游戏必须遵循game-creator规范:
- 目录:包含EventBus、GameState和Constants
core/ - EventBus单例 — 使用命名事件,禁止直接引用场景
domain:action - GameState单例 — 集中式状态管理,提供方法实现干净重启
reset() - 常量文件 — 所有魔法数字、颜色、速度和配置值都必须定义在此,禁止硬编码值
- 场景清理 — 在方法中移除EventBus监听器
shutdown()
详见conventions.md获取完整细节和代码示例。
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以官方Vite + TypeScript模板作为起点:
bash
npx degit phaserjs/template-vite-ts my-game
cd my-game && npm installRequired 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 pointSee project-setup.md for full config and tooling details.
src/
├── core/
│ ├── EventBus.ts # 单例事件总线 + 事件常量
│ ├── GameState.ts # 集中式状态管理,含reset()方法
│ └── Constants.ts # 所有配置值
├── scenes/
│ ├── Boot.ts # 最小化设置,启动Game场景
│ ├── Preloader.ts # 加载所有资源,显示进度条
│ ├── Game.ts # 主游戏玩法(直接启动,无标题界面)
│ └── GameOver.ts # 结束界面,支持重启
├── objects/ # 游戏实体(玩家、敌人等)
├── systems/ # 管理器和子系统
├── ui/ # UI组件(按钮、进度条、对话框)
├── audio/ # 音频管理器、音乐、音效
├── config.ts # Phaser.Types.Core.GameConfig
└── main.ts # 入口文件详见project-setup.md获取完整配置和工具细节。
Scene Architecture
场景架构
- Lifecycle: →
init()→preload()→create()update(time, delta) - Use for receiving data from scene transitions
init() - Load assets in a dedicated scene, not in every scene
Preloader - Keep lean — delegate to subsystems and game objects
update() - 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
- 生命周期:→
init()→preload()→create()update(time, delta) - 使用接收场景间传递的数据
init() - 在专用的场景中加载资源,而非每个场景单独加载
Preloader - 保持方法精简 — 将逻辑委托给子系统和游戏对象
update() - 默认无标题界面 — 直接启动游戏玩法。仅当用户明确要求时才添加标题/菜单场景
- 无游戏内计分HUD — Play.fun小部件会在屏幕顶部的死区显示分数。请勿创建单独的UIScene或HUD overlay来显示分数
- 仅当用户要求时,才使用并行场景实现UI覆盖层(如暂停菜单)
Play.fun Safe Zone
Play.fun 安全区域
When games are rendered inside Play.fun (or with the Play.fun SDK), a widget bar overlays the top ~75px of the viewport (). The template defines in Constants.js for this purpose.
position: fixed; top: 0; height: 75px; z-index: 9999SAFE_ZONE.TOPRules:
- 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 for calculating proportional positions in UI scenes
const usableH = GAME.HEIGHT - SAFE_ZONE.TOP
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 for patterns and examples.
当游戏在Play.fun(或使用Play.fun SDK)中渲染时,一个小部件栏会覆盖视口顶部约75px的区域()。模板在Constants.js中定义了来对应此区域。
position: fixed; top: 0; height: 75px; z-index: 9999SAFE_ZONE.TOP规则:
- 所有UI文本、按钮和HUD元素必须定位在下方
SAFE_ZONE.TOP - 游戏实体不应在安全区域内生成
- 游戏结束界面、分数面板和重启按钮必须与保持偏移
SAFE_ZONE.TOP - 在UI场景中,使用计算比例位置
const usableH = GAME.HEIGHT - SAFE_ZONE.TOP
js
import { SAFE_ZONE } from '../core/Constants.js';
// 在任意UI场景中:
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);- 通过EventBus实现场景间通信(禁止直接引用)
详见scenes-and-lifecycle.md获取模式和示例。
Game Objects
游戏对象
- Extend (or other base classes) for custom objects
Phaser.GameObjects.Sprite - Use for object pooling (bullets, coins, enemies)
Phaser.GameObjects.Group - Use for composite objects, but avoid deep nesting
Phaser.GameObjects.Container - Register custom objects with for scene-level access
GameObjectFactory
See game-objects.md for implementation patterns.
- 继承(或其他基类)实现自定义对象
Phaser.GameObjects.Sprite - 使用实现对象池(子弹、金币、敌人等)
Phaser.GameObjects.Group - 使用实现复合对象,但避免深层嵌套
Phaser.GameObjects.Container - 向注册自定义对象,以便在场景中全局访问
GameObjectFactory
详见game-objects.md获取实现模式。
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 for details.
- Arcade Physics — 适用于简单游戏(平台跳跃、俯视视角)。快速且轻量。
- Matter.js — 当你需要真实碰撞、约束或复杂形状时使用。
- 禁止在同一游戏中混合使用多个物理引擎。
- 使用状态模式实现角色移动( idle、行走、跳跃、攻击)。
详见physics-and-movement.md获取细节。
Performance (Critical Rules)
性能(关键规则)
- Use texture atlases — Pack sprites into atlases, never load individual images at scale
- Object pooling — Use Groups with ; recycle with
maxSize/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
- — Enable in game config for pixel art games (nearest-neighbor scaling)
pixelArt: true
See assets-and-performance.md for full optimization guide.
- 使用纹理图集 — 将精灵打包到图集中,绝不在大规模场景中加载单个图像
- 对象池 — 使用带的Groups;通过
maxSize/setActive(false)回收对象setVisible(false) - 最小化update方法工作量 — 仅遍历活跃对象;使用
getChildren().filter(c => c.active) - 相机剔除 — 大型世界中启用此功能;屏幕外的对象会跳过渲染
- 批处理渲染 — 每帧使用的唯一纹理越少,绘制调用批处理效果越好
- 移动端优化 — 减少粒子数量,简化物理系统,考虑以30fps为目标
- — 像素艺术游戏需在游戏配置中启用此选项(最近邻缩放)
pixelArt: true
详见assets-and-performance.md获取完整优化指南。
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 for implementations.
- 使用bitECS实现ECS — 面向数据设计的实体组件系统(Phaser 4内部使用)
- 状态机 — 清晰管理实体行为状态
- 单例管理器 — 跨场景服务(音频、存档数据、分析)
- 事件总线 — 通过共享EventEmitter解耦系统
- Tiled集成 — 使用Tiled地图编辑器进行关卡设计
详见patterns.md获取实现方案。
Mobile Input Strategy (60/40 Rule)
移动端输入策略(60/40规则)
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 |
除非明确指定,所有游戏必须同时支持桌面端和移动端。在权衡时优先考虑60%移动端 / 40%桌面端。为每种游戏概念选择最佳移动端输入方式:
| 游戏类型 | 主要移动端输入 | 桌面端输入 |
|---|---|---|
| 平台跳跃 | 点击左右半屏 + 点击跳跃 | 方向键 / WASD |
| 跑酷/无尽模式 | 点击/上滑跳跃 | 空格键 / 上方向键 |
| 解谜/消除 | 点击目标(最小44px) | 鼠标点击 |
| 射击 | 虚拟摇杆 + 点击射击 | 鼠标 + WASD |
| 俯视视角 | 虚拟摇杆 | 方向键 / WASD |
Implementation Pattern
实现模式
Abstract input into an object so game logic is source-agnostic:
inputStatetypescript
// 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 });将输入抽象为对象,使游戏逻辑与输入源无关:
inputStatetypescript
// 在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;
// 键盘输入
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);
// 触摸输入(与键盘输入合并)
if (isMobile) {
// 左半屏点击=左移,右半屏点击=右移,或使用点击区域
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)
响应式画布配置(Retina/高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;为在任意显示器上实现像素完美渲染,画布尺寸需匹配用户设备的像素区域(而非固定基础分辨率)。这可避免高DPI屏幕上的CSS缩放模糊问题。
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;
// 画布=设备像素区域,保持设计宽高比
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 = 画布像素 / 设计像素。所有绝对值需乘以PX。
export const PX = canvasW / designW;
export const GAME = {
WIDTH: canvasW, // 例如:在1728×1117 @2x显示器上为3456
HEIGHT: canvasH,
GRAVITY: 800 * PX,
};
// GameConfig.ts
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH,
zoom: 1 / DPR,
},
roundPixels: true,
antialias: true,
// 所有绝对像素值使用PX(而非DPR)。比例值使用比率。
const groundH = 30 * PX;
const buttonY = GAME.HEIGHT * 0.55;Entity Sizing
实体尺寸
Character dimensions must preserve their spritesheet aspect ratio across all orientations. Derive HEIGHT from WIDTH using the sprite's native aspect ratio (200×300 spritesheets = 1.5):
js
const SPRITE_ASPECT = 1.5;
// Good — HEIGHT derived from WIDTH, correct in both landscape and portrait
PLAYER: {
WIDTH: GAME.WIDTH * 0.08,
HEIGHT: GAME.WIDTH * 0.08 * SPRITE_ASPECT,
}
// Bad — independent GAME.HEIGHT ratio squishes characters in portrait mode
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 for the player width. Use caricature proportions (large head ~40–50% of sprite height with exaggerated features, compact body) for personality games to maximize character recognition at any scale. Never define character HEIGHT as — on mobile portrait, is much larger than , breaking the aspect ratio and squishing heads vertically.
GAME.WIDTHGAME.HEIGHT * ratioGAME.HEIGHTGAME.WIDTHHTML boilerplate (required for proper scaling):
html
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
#game-container { width: 100%; height: 100%; }
</style>角色尺寸必须在所有方向上保持其精灵表的宽高比。使用精灵的原生宽高比从宽度推导高度(例如200×300的精灵表宽高比为1.5):
js
const SPRITE_ASPECT = 1.5;
// 正确做法 — 高度由宽度推导,横竖屏均保持正确比例
PLAYER: {
WIDTH: GAME.WIDTH * 0.08,
HEIGHT: GAME.WIDTH * 0.08 * SPRITE_ASPECT,
}
// 错误做法 — 独立使用GAME.HEIGHT比例会导致竖屏时角色被挤压
PLAYER: {
WIDTH: GAME.WIDTH * 0.08,
HEIGHT: GAME.HEIGHT * 0.12,
}
// 错误做法 — 固定尺寸,不随屏幕变化
PLAYER: {
WIDTH: 40 * PX,
HEIGHT: 40 * PX,
}对于角色驱动型游戏(有命名角色、个性、吉祥物),需让角色更突出 — 玩家宽度使用的12–15%。对于个性游戏,使用漫画比例(头部占精灵高度的40–50%,特征夸张,身体紧凑)以在任意尺寸下最大化角色辨识度。绝不能将角色高度定义为 — 在移动端竖屏模式下,远大于,会破坏宽高比并导致头部垂直挤压。
GAME.WIDTHGAME.HEIGHT * 比率GAME.HEIGHTGAME.WIDTHHTML模板(正确缩放必备):
html
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; }
#game-container { width: 100%; height: 100%; }
</style>Portrait-First Games
优先竖屏游戏
For vertical game types (dodgers, runners, collectors, endless fallers), force portrait mode regardless of device orientation. Set in Constants.js — this locks and uses fixed 540×960 design dimensions. On desktop, automatically pillarboxes with black bars (no CSS changes needed when is set on body).
FORCE_PORTRAIT = true_isPortrait = trueScale.FIT + CENTER_BOTHbackground: #000js
// Constants.js — force portrait for vertical games
const FORCE_PORTRAIT = true;
const _isPortrait = FORCE_PORTRAIT || window.innerHeight > window.innerWidth;
const _designW = 540;
const _designH = 960;Without this, desktop browsers stretch the game to landscape, ruining the vertical layout. The template default is (auto-detect orientation).
FORCE_PORTRAIT = false对于垂直类型游戏(躲避类、跑酷类、收集类、无尽下落类),强制锁定竖屏模式,无论设备方向如何。在Constants.js中设置 — 这会将设为true,并使用固定的540×960设计尺寸。在桌面端,会自动添加黑边(当body设置时无需修改CSS)。
FORCE_PORTRAIT = true_isPortraitScale.FIT + CENTER_BOTHbackground: #000js
// Constants.js — 垂直游戏强制竖屏
const FORCE_PORTRAIT = true;
const _isPortrait = FORCE_PORTRAIT || window.innerHeight > window.innerWidth;
const _designW = 540;
const _designH = 960;如果不设置此选项,桌面浏览器会将游戏拉伸为横屏,破坏垂直布局。模板默认(自动检测方向)。
FORCE_PORTRAIT = falseVisible 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 constants from Constants.js for sizing (12% of canvas width), alpha (0.35 idle / 0.6 active), and margins. Update alpha in the loop based on input state for visual feedback.
TOUCHupdate()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.
在支持触摸的设备上,始终显示视觉触摸指示器 — 绝不要依赖不可见的点击区域。使用能力检测(而非基于系统检测)来判断是否支持触摸:
js
// 正确做法 — 支持触摸笔记本、平板、二合一设备
const hasTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
// 错误做法 — 遗漏触摸屏笔记本、iPadOS(会被识别为桌面端)
const isMobile = device.os.android || device.os.iOS;在屏幕底部渲染半透明的箭头按钮(或方向指示器)。使用Constants.js中的常量设置尺寸(画布宽度的12%)、透明度(闲置时0.35 / 活跃时0.6)和边距。在循环中根据输入状态更新透明度,提供视觉反馈。
TOUCHupdate()在所有设备上启用指针输入(pointerdown、pointermove、pointerup)——指针事件同时支持鼠标和触摸输入。这消除了编写单独移动端/桌面端输入代码路径的需求。
Minimum Entity Sizes for Mobile
移动端最小实体尺寸
Collectibles, hazards, and interactive items must be at least 7–8% of to be recognizable on phone screens. Smaller entities become indistinguishable blobs on mobile.
GAME.WIDTHjs
// 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 (see Entity Sizing above).
GAME.WIDTH可收集物品、危险物和交互元素的尺寸至少应为的7–8%,以便在手机屏幕上清晰可辨。更小的实体在移动端会变成难以区分的模糊块。
GAME.WIDTHjs
// 正确做法 — 移动端清晰可辨
ATTACK_WIDTH: _canvasW * 0.09,
POWERUP_WIDTH: _canvasW * 0.072,
// 错误做法 — 手机屏幕上过小
ATTACK_WIDTH: _canvasW * 0.04,
POWERUP_WIDTH: _canvasW * 0.035,主玩家角色尺寸请使用的12–15%(见上文实体尺寸部分)。
GAME.WIDTHButton Pattern (Container + Graphics + Text)
按钮模式(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 on an interactive object and layering visuals over it
setAlpha(0)
按钮需要仔细管理层级。使用Container包含Graphics(背景)和Text(标签)—— 按此顺序添加。Container本身设置为可交互。
所有可点击按钮必须严格遵循此模式。禁止使用Zone,禁止在Text上方绘制Graphics,禁止为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背景(先添加 — 渲染在文字下方)
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标签(后添加 — 渲染在背景上方)
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. 将CONTAINER设置为可交互(而非Graphics或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;
}错误模式(禁止使用):
- 在Text上方绘制Graphics(遮挡标签)
- 使用Zone实现交互性并在其上绘制Graphics(Zone会变得不可点击)
- 将可交互对象设置为并在其上叠加视觉元素
setAlpha(0)
Anti-Patterns (Avoid These)
反模式(避免使用)
- Bloated methods — Don't put all game logic in one giant update with nested conditionals. Delegate to objects and systems.
update() - 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, orrenderer. These are reserved by Phaser.plugins - Creating objects in without pooling — This causes GC spikes. Always pool frequently created/destroyed objects. Avoid expensive per-frame allocations — reuse objects, arrays, and temporary variables.
update() - 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 in update — Always use
deltafor time-based movement, not frame-based.delta - Deep container nesting — Containers disable render batching for children. Keep hierarchy flat.
- Not cleaning up — Remove event listeners and timers in to prevent memory leaks. This is critical for restart-safety — stale listeners cause double-firing and ghost behavior after restart.
shutdown() - Hardcoded values — Every number belongs in . No magic numbers in game logic.
Constants.ts - Unwired physics colliders — Creating a static body with does nothing on its own. You MUST call
physics.add.existing(obj, true)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.physics.add.collider(bodyA, bodyB, callback) - Invisible or hidden button elements — Never set 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.
setAlpha(0) - No mute toggle — See the rule. Games with audio must have a mute toggle.
mute-button
- 臃肿的方法 — 不要将所有游戏逻辑放入一个庞大的update方法中,避免嵌套条件判断。将逻辑委托给对象和系统。
update() - 覆盖Scene注入映射属性 — 禁止将你的属性命名为、
world、input、cameras、add、make、scene、sys、game、cache、registry、sound、textures、events、physics、matter、time、tweens、lights、data、load、anims或renderer。这些是Phaser的保留属性。plugins - 在中创建对象但不使用对象池 — 这会导致GC峰值。对于频繁创建/销毁的对象,必须使用对象池。避免每帧进行昂贵的内存分配 — 复用对象、数组和临时变量。
update() - 加载单个精灵而非纹理图集 — 每个独立纹理都会产生一次绘制调用。请将精灵打包到图集中。
- 场景间紧耦合 — 禁止在场景间存储直接引用。使用EventBus。
- 更新时忽略— 始终使用
delta实现基于时间的移动,而非基于帧计数。delta - 深层容器嵌套 — 容器会禁用其子元素的渲染批处理。保持层级扁平化。
- 不进行清理 — 在中移除事件监听器和计时器,防止内存泄漏。这对支持安全重启至关重要 — 陈旧监听器会导致重启后事件重复触发和幽灵行为。
shutdown() - 硬编码值 — 所有数值都应定义在中。游戏逻辑中禁止出现魔法数字。
Constants.ts - 未连接物理碰撞器 — 使用创建静态物体本身不会产生任何效果。必须调用
physics.add.existing(obj, true)来连接两个物体。每个静态碰撞器(地面、墙壁、平台)都需要显式调用collider或overlap方法,将其与应交互的实体关联。physics.add.collider(bodyA, bodyB, callback) - 不可见或隐藏的按钮元素 — 禁止将可交互游戏对象设置为并在其上叠加Graphics或其他显示元素。按钮必须始终使用Container + Graphics + Text模式(见上文按钮模式部分)。常见错误模式:(1) 添加Text后绘制Graphics,将标签遮挡在后方。(2) 创建Zone作为点击区域并在其上绘制Graphics,导致Zone不可点击。(3) 将Text设置为可交互,但之后绘制Graphics背景覆盖其上。修复方法始终是:先创建Container,向Container添加Graphics,再向Container添加Text(按此顺序),将Container设置为可交互元素。
setAlpha(0) - 无静音开关 — 遵循规则。带音频的游戏必须提供静音开关。
mute-button
Examples
示例
- Simple Game — Minimal complete Phaser game (collector game)
- Complex Game — Multi-scene game with state machines, pooling, EventBus, and all conventions
- 简单游戏 — 最小化完整Phaser游戏(收集类游戏)
- 复杂游戏 — 多场景游戏,包含状态机、对象池、EventBus和所有规范
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 — restores a clean slate, no stale listeners or timers
GameState.reset() - Touch + keyboard input — Game works on mobile (tap/swipe) and desktop (keyboard/mouse)
- Responsive canvas — +
Scale.FIT+CENTER_BOTHwith DPR-multiplied dimensions, crisp on Retinazoom: 1/DPR - 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 or
collider()calloverlap() - Object pooling — Frequently created/destroyed objects use Groups with
maxSize - Delta-based movement — All motion uses , not frame count
delta - Mute toggle — See rule
mute-button - Spectacle hooks wired — Every player action and game event emits a event; entrance sequence fires in
SPECTACLE_*create() - Build passes — succeeds with no errors
npm run build - No console errors — Game runs without uncaught exceptions or WebGL failures
在认为游戏完成前,请验证以下内容:
- 核心循环正常工作 — 玩家可以开始游戏、进行游玩、胜负、查看结果
- 重启功能正常 — 可恢复至干净初始状态,无陈旧监听器或计时器
GameState.reset() - 触摸+键盘输入支持 — 游戏可在移动端(点击/滑动)和桌面端(键盘/鼠标)正常运行
- 响应式画布 — +
Scale.FIT+CENTER_BOTH,使用DPR倍增尺寸,Retina屏幕显示清晰zoom: 1/DPR - 所有值均定义在常量中 — 游戏逻辑中无硬编码魔法数字
- 仅使用EventBus通信 — 跨场景/模块通信禁止使用直接导入
- 场景清理完成 — 所有EventBus监听器已在中移除
shutdown() - 物理系统已连接 — 每个静态物体都有显式的或
collider()调用overlap() - 对象池已使用 — 频繁创建/销毁的对象使用带的Groups
maxSize - 基于delta的移动 — 所有移动均使用,而非帧计数
delta - 静音开关已添加 — 遵循规则
mute-button - Spectacle钩子已连接 — 每个玩家操作和游戏事件都触发事件;入场序列在
SPECTACLE_*中触发create() - 构建通过 — 执行成功,无错误
npm run build - 无控制台错误 — 游戏运行时无未捕获异常或WebGL失败
References
参考资料
| File | Topic |
|---|---|
| conventions.md | Mandatory game-creator architecture conventions |
| project-setup.md | Scaffolding, Vite, TypeScript config |
| scenes-and-lifecycle.md | Scene system deep dive |
| game-objects.md | Custom objects, groups, containers |
| physics-and-movement.md | Physics engines, movement patterns |
| assets-and-performance.md | Assets, optimization, mobile |
| patterns.md | ECS, state machines, singletons |
| no-asset-design.md | Procedural visuals: gradients, parallax, particles, juice |
| 文件 | 主题 |
|---|---|
| conventions.md | 强制遵循的game-creator架构规范 |
| project-setup.md | 脚手架、Vite、TypeScript配置 |
| scenes-and-lifecycle.md | 场景系统深度解析 |
| game-objects.md | 自定义对象、组、容器 |
| physics-and-movement.md | 物理引擎、移动模式 |
| assets-and-performance.md | 资源、优化、移动端适配 |
| patterns.md | ECS、状态机、单例 |
| no-asset-design.md | 程序化视觉效果:渐变、视差、粒子、趣味元素 |