phaser

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Phaser 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

核心原则

  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.
  1. 优先实现核心循环 — 在进行任何优化之前,先实现最小化游戏循环:boot → preload → create → update。在添加视觉效果、音频或趣味元素之前,先加入胜负条件和计分系统。初始范围要小:1个场景、1种机制、1种失败条件。在核心循环中同时接入Spectacle EventBus钩子(
    SPECTACLE_*
    事件)—— 这些是框架的一部分,而非后期优化内容。
  2. 优先使用TypeScript — 始终使用TypeScript以确保类型安全并获得IDE支持
  3. 基于场景的架构 — 每个游戏界面对应一个Scene;保持场景职责单一
  4. Vite打包 — 使用官方
    phaserjs/template-vite-ts
    模板
  5. 组合优于继承 — 优先通过组合实现行为,而非使用深层类继承
  6. 数据驱动设计 — 在JSON/数据文件中定义关卡、敌人和配置信息
  7. 事件驱动通信 — 所有跨场景/系统的通信均通过EventBus实现
  8. 支持安全重启 — 游戏玩法必须完全支持安全重启且结果可预测。
    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.
EventConstantWhen 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.
每个玩家操作和游戏事件都必须至少触发一个spectacle事件。这些钩子已内置在模板的EventBus中——设计阶段会为其附加视觉效果。
事件常量触发时机
spectacle:entrance
SPECTACLE_ENTRANCE
create()
方法中,当玩家/实体首次出现在屏幕上时
spectacle:action
SPECTACLE_ACTION
每次玩家输入时(点击、跳跃、射击、滑动)
spectacle:hit
SPECTACLE_HIT
当玩家击中/消灭敌人、收集物品或得分时
spectacle:combo
SPECTACLE_COMBO
连续命中/得分且未失误时。需传入
{ combo: n }
spectacle:streak
SPECTACLE_STREAK
连击数达到里程碑时(5、10、25、50)。需传入
{ streak: n }
spectacle:near_miss
SPECTACLE_NEAR_MISS
玩家侥幸避开危险时(距离碰撞半径约20%以内)
规则:如果某个游戏时刻没有对应的spectacle事件,请添加一个。设计阶段无法对无法挂钩的内容进行优化。

Mandatory Conventions

强制遵循的规范

All games MUST follow the game-creator conventions:
  • 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 for full details and code examples.
所有游戏必须遵循game-creator规范
  • core/
    目录
    :包含EventBus、GameState和Constants
  • EventBus单例 — 使用
    domain:action
    命名事件,禁止直接引用场景
  • GameState单例 — 集中式状态管理,提供
    reset()
    方法实现干净重启
  • 常量文件 — 所有魔法数字、颜色、速度和配置值都必须定义在此,禁止硬编码值
  • 场景清理 — 在
    shutdown()
    方法中移除EventBus监听器
详见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 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 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
    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
  • 生命周期
    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 (
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 for patterns and examples.
当游戏在Play.fun(或使用Play.fun SDK)中渲染时,一个小部件栏会覆盖视口顶部约75px的区域(
position: fixed; top: 0; height: 75px; z-index: 9999
)。模板在Constants.js中定义了
SAFE_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
    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 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
    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 for full optimization guide.
  • 使用纹理图集 — 将精灵打包到图集中,绝不在大规模场景中加载单个图像
  • 对象池 — 使用带
    maxSize
    的Groups;通过
    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 TypePrimary Mobile InputDesktop Input
PlatformerTap left/right half + tap-to-jumpArrow keys / WASD
Runner/endlessTap / swipe up to jumpSpace / Up arrow
Puzzle/matchTap targets (44px min)Click
ShooterVirtual joystick + tap-to-fireMouse + WASD
Top-downVirtual joystickArrow keys / WASD
除非明确指定,所有游戏必须同时支持桌面端和移动端。在权衡时优先考虑60%移动端 / 40%桌面端。为每种游戏概念选择最佳移动端输入方式:
游戏类型主要移动端输入桌面端输入
平台跳跃点击左右半屏 + 点击跳跃方向键 / WASD
跑酷/无尽模式点击/上滑跳跃空格键 / 上方向键
解谜/消除点击目标(最小44px)鼠标点击
射击虚拟摇杆 + 点击射击鼠标 + WASD
俯视视角虚拟摇杆方向键 / 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 });
将输入抽象为
inputState
对象,使游戏逻辑与输入源无关:
typescript
// 在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
GAME.WIDTH
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
GAME.HEIGHT * ratio
— on mobile portrait,
GAME.HEIGHT
is much larger than
GAME.WIDTH
, breaking the aspect ratio and squishing heads vertically.
HTML 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,
}
对于角色驱动型游戏(有命名角色、个性、吉祥物),需让角色更突出 — 玩家宽度使用
GAME.WIDTH
的12–15%。对于个性游戏,使用漫画比例(头部占精灵高度的40–50%,特征夸张,身体紧凑)以在任意尺寸下最大化角色辨识度。绝不能将角色高度定义为
GAME.HEIGHT * 比率
— 在移动端竖屏模式下,
GAME.HEIGHT
远大于
GAME.WIDTH
,会破坏宽高比并导致头部垂直挤压。
HTML模板(正确缩放必备):
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
FORCE_PORTRAIT = true
in Constants.js — this locks
_isPortrait = true
and uses fixed 540×960 design dimensions. On desktop,
Scale.FIT + CENTER_BOTH
automatically pillarboxes with black bars (no CSS changes needed when
background: #000
is set on body).
js
// 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
FORCE_PORTRAIT = false
(auto-detect orientation).
对于垂直类型游戏(躲避类、跑酷类、收集类、无尽下落类),强制锁定竖屏模式,无论设备方向如何。在Constants.js中设置
FORCE_PORTRAIT = true
— 这会将
_isPortrait
设为true,并使用固定的540×960设计尺寸。在桌面端,
Scale.FIT + CENTER_BOTH
会自动添加黑边(当body设置
background: #000
时无需修改CSS)。
js
// Constants.js — 垂直游戏强制竖屏
const FORCE_PORTRAIT = true;
const _isPortrait = FORCE_PORTRAIT || window.innerHeight > window.innerWidth;
const _designW = 540;
const _designH = 960;
如果不设置此选项,桌面浏览器会将游戏拉伸为横屏,破坏垂直布局。模板默认
FORCE_PORTRAIT = false
(自动检测方向)。

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.
在支持触摸的设备上,始终显示视觉触摸指示器 — 绝不要依赖不可见的点击区域。使用能力检测(而非基于系统检测)来判断是否支持触摸:
js
// 正确做法 — 支持触摸笔记本、平板、二合一设备
const hasTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);

// 错误做法 — 遗漏触摸屏笔记本、iPadOS(会被识别为桌面端)
const isMobile = device.os.android || device.os.iOS;
在屏幕底部渲染半透明的箭头按钮(或方向指示器)。使用Constants.js中的
TOUCH
常量设置尺寸(画布宽度的12%)、透明度(闲置时0.35 / 活跃时0.6)和边距。在
update()
循环中根据输入状态更新透明度,提供视觉反馈。
所有设备上启用指针输入(pointerdown、pointermove、pointerup)——指针事件同时支持鼠标和触摸输入。这消除了编写单独移动端/桌面端输入代码路径的需求。

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).
可收集物品、危险物和交互元素的尺寸至少应为
GAME.WIDTH
7–8%,以便在手机屏幕上清晰可辨。更小的实体在移动端会变成难以区分的模糊块。
js
// 正确做法 — 移动端清晰可辨
ATTACK_WIDTH: _canvasW * 0.09,
POWERUP_WIDTH: _canvasW * 0.072,

// 错误做法 — 手机屏幕上过小
ATTACK_WIDTH: _canvasW * 0.04,
POWERUP_WIDTH: _canvasW * 0.035,
主玩家角色尺寸请使用
GAME.WIDTH
的12–15%(见上文实体尺寸部分)。

Button 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
    setAlpha(0)
    on an interactive object and layering visuals over it
按钮需要仔细管理层级。使用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
    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 — See the
    mute-button
    rule. Games with audio must have a mute toggle.
  • 臃肿的
    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
    plugins
    。这些是Phaser的保留属性。
  • update()
    中创建对象但不使用对象池
    — 这会导致GC峰值。对于频繁创建/销毁的对象,必须使用对象池。避免每帧进行昂贵的内存分配 — 复用对象、数组和临时变量。
  • 加载单个精灵而非纹理图集 — 每个独立纹理都会产生一次绘制调用。请将精灵打包到图集中。
  • 场景间紧耦合 — 禁止在场景间存储直接引用。使用EventBus。
  • 更新时忽略
    delta
    — 始终使用
    delta
    实现基于时间的移动,而非基于帧计数。
  • 深层容器嵌套 — 容器会禁用其子元素的渲染批处理。保持层级扁平化。
  • 不进行清理 — 在
    shutdown()
    中移除事件监听器和计时器,防止内存泄漏。这对支持安全重启至关重要 — 陈旧监听器会导致重启后事件重复触发和幽灵行为。
  • 硬编码值 — 所有数值都应定义在
    Constants.ts
    中。游戏逻辑中禁止出现魔法数字。
  • 未连接物理碰撞器 — 使用
    physics.add.existing(obj, true)
    创建静态物体本身不会产生任何效果。必须调用
    physics.add.collider(bodyA, bodyB, callback)
    来连接两个物体。每个静态碰撞器(地面、墙壁、平台)都需要显式调用collider或overlap方法,将其与应交互的实体关联。
  • 不可见或隐藏的按钮元素 — 禁止将可交互游戏对象设置为
    setAlpha(0)
    并在其上叠加Graphics或其他显示元素。按钮必须始终使用Container + Graphics + Text模式(见上文按钮模式部分)。常见错误模式:(1) 添加Text后绘制Graphics,将标签遮挡在后方。(2) 创建Zone作为点击区域并在其上绘制Graphics,导致Zone不可点击。(3) 将Text设置为可交互,但之后绘制Graphics背景覆盖其上。修复方法始终是:先创建Container,向Container添加Graphics,再向Container添加Text(按此顺序),将Container设置为可交互元素。
  • 无静音开关 — 遵循
    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
    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
在认为游戏完成前,请验证以下内容:
  • 核心循环正常工作 — 玩家可以开始游戏、进行游玩、胜负、查看结果
  • 重启功能正常
    GameState.reset()
    可恢复至干净初始状态,无陈旧监听器或计时器
  • 触摸+键盘输入支持 — 游戏可在移动端(点击/滑动)和桌面端(键盘/鼠标)正常运行
  • 响应式画布
    Scale.FIT
    +
    CENTER_BOTH
    +
    zoom: 1/DPR
    ,使用DPR倍增尺寸,Retina屏幕显示清晰
  • 所有值均定义在常量中 — 游戏逻辑中无硬编码魔法数字
  • 仅使用EventBus通信 — 跨场景/模块通信禁止使用直接导入
  • 场景清理完成 — 所有EventBus监听器已在
    shutdown()
    中移除
  • 物理系统已连接 — 每个静态物体都有显式的
    collider()
    overlap()
    调用
  • 对象池已使用 — 频繁创建/销毁的对象使用带
    maxSize
    的Groups
  • 基于delta的移动 — 所有移动均使用
    delta
    ,而非帧计数
  • 静音开关已添加 — 遵循
    mute-button
    规则
  • Spectacle钩子已连接 — 每个玩家操作和游戏事件都触发
    SPECTACLE_*
    事件;入场序列在
    create()
    中触发
  • 构建通过
    npm run build
    执行成功,无错误
  • 无控制台错误 — 游戏运行时无未捕获异常或WebGL失败

References

参考资料

FileTopic
conventions.mdMandatory game-creator architecture conventions
project-setup.mdScaffolding, Vite, TypeScript config
scenes-and-lifecycle.mdScene system deep dive
game-objects.mdCustom objects, groups, containers
physics-and-movement.mdPhysics engines, movement patterns
assets-and-performance.mdAssets, optimization, mobile
patterns.mdECS, state machines, singletons
no-asset-design.mdProcedural 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.mdECS、状态机、单例
no-asset-design.md程序化视觉效果:渐变、视差、粒子、趣味元素