关于 phaser
面向编码 Agent 的 Phaser 3 浏览器游戏开发技能,涵盖场景组织、核心循环、移动端输入与性能实践。强调 TypeScript、事件驱动通信和可安全重启的游戏逻辑。
主要功能
- 01TypeScript 优先的 Phaser 3 模式
- 02基于场景的游戏结构
- 03EventBus 与集中式状态
- 04Spectacle 事件钩子
- 05移动端与桌面输入指导
- 06性能与清理实践
适用场景
- 01搭建 Phaser 3 浏览器游戏
- 02组织场景与游戏系统
- 03支持触控和键盘操作
- 04实现确定性的游戏重启
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ playableintelligence/ game-creator --skill phaser安装后在 Claude Code 中输入/phaser确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets-and-performance.mdconventions.mdgame-objects.mdno-asset-design.mdpatterns.mdphysics-and-movement.mdproject-setup.mdscenes-and-lifecycle.mdexamples/complex-game.mdsimple-game.md详细介绍
Phaser 3 游戏开发
你是使用 game-creator 插件开发游戏的 Phaser 专家。遵循以下模式,打造结构清晰、视觉精致且易于维护的 2D 浏览器游戏。
核心原则
- 先实现核心循环 — 在进行任何润色之前,先实现最小可用的游戏循环:启动 → 预加载 → 创建 → 更新。先添加胜负条件和计分,再处理视觉效果、音频或爽感表现。初始范围要小:1 个场景、1 个机制、1 个失败条件。将华丽效果 EventBus 钩子(
SPECTACLE_*事件)与核心循环一起接入——它们属于脚手架,而不是后续润色内容。 - 优先使用 TypeScript — 始终使用 TypeScript,以获得类型安全和 IDE 支持
- 基于场景的架构 — 每个游戏界面都是一个场景;让场景各司其职
- 使用 Vite 打包 — 使用官方的
phaserjs/template-vite-ts模板 - 组合优于继承 — 优先通过组合行为实现功能,而不是构建深层类继承体系
- 数据驱动设计 — 在 JSON/数据文件中定义关卡、敌人和配置
- 事件驱动通信 — 所有跨场景/系统通信都通过 EventBus 进行
- 安全重启 — 游戏过程必须可安全、确定性地完整重启。
GameState.reset()必须恢复干净状态。重启后不得残留过期引用、计时器或事件监听器。
华丽效果事件
每个玩家动作和游戏事件都必须至少触发一个华丽效果事件。这些钩子已存在于模板的 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% 以内) |
规则:如果某个游戏过程节点没有华丽效果事件,就添加一个。设计阶段无法润色没有钩子的内容。
强制约定
所有游戏都必须遵循 game-creator 约定:
core/目录,包含 EventBus、GameState 和 Constants- EventBus 单例 — 采用
domain:action事件命名,不直接引用场景 - GameState 单例 — 集中管理状态,并通过
reset()实现干净重启 - Constants 文件 — 每个魔法数字、颜色、速度和配置值都必须定义;不得硬编码
- 场景清理 — 在
shutdown()中移除 EventBus 监听器
完整详情和代码示例请参阅 conventions.md。
项目设置
从官方 Vite + TypeScript 模板开始:
npx degit phaserjs/template-vite-ts my-game
cd my-game && npm install
必需的目录结构
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
完整的配置和工具详情请参阅 project-setup.md。
场景架构
- 生命周期:
init()→preload()→create()→update(time, delta) - 使用
init()接收场景切换时传递的数据 - 在专用的
Preloader场景中加载资源,不要在每个场景中重复加载 - 保持
update()简洁——将工作委派给子系统和游戏对象 - 默认不添加标题画面 — 直接启动游戏。只有用户明确提出要求时,才添加标题/菜单场景
- 不添加游戏内计分 HUD — Play.fun 小组件会在游戏顶部的死区中显示分数。不要为显示分数单独创建 UIScene 或 HUD 覆层
- 仅在用户要求时,才使用并行场景显示 UI 覆层(暂停菜单)
Play.fun 安全区域
当游戏在移动版 Safari 的 Play.fun 仪表板中运行时,SDK 会在游戏 iframe 的 document.documentElement 上设置 CSS 自定义属性:
--ogp-safe-top-inset— Play.fun 页眉下方的空间(移动设备上约为 68px)--ogp-safe-bottom-inset— Safari 底部控件上方的空间(移动设备上约为 148px)
在仪表板以外运行时(桌面端、独立运行),两者的默认值均为 0px。
模板的 Constants.js 会在启动时读取这些值,并以画布像素为单位(CSS 值 × DPR)提供 SAFE_ZONE.TOP 和 SAFE_ZONE.BOTTOM。静态回退值(GAME.HEIGHT * 0.08)可确保即使没有 SDK,顶部安全区域也能正常工作。
规则:
- 所有 UI 文本、按钮和 HUD 元素都必须位于
SAFE_ZONE.TOP以下,并且位于GAME.HEIGHT - SAFE_ZONE.BOTTOM以上 - 游戏实体不应在安全区域内生成
- 游戏结束画面、分数面板和重新开始按钮的位置必须同时避开
SAFE_ZONE.TOP和SAFE_ZONE.BOTTOM - 在 UI 场景中计算比例位置时,使用
const usableH = GAME.HEIGHT - SAFE_ZONE.TOP - SAFE_ZONE.BOTTOM - 游戏画布和背景应铺满整个视口(延伸至浏览器界面下方)
- 底部的触控控件必须考虑
SAFE_ZONE.BOTTOM
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;
Constants.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,
};
- 通过 EventBus 在场景之间通信(不要使用直接引用)
有关模式和示例,请参阅 scenes-and-lifecycle.md。
游戏对象
- 通过扩展
Phaser.GameObjects.Sprite(或其他基类)创建自定义对象 - 使用
Phaser.GameObjects.Group进行对象池管理(子弹、金币、敌人) - 使用
Phaser.GameObjects.Container创建复合对象,但避免深层嵌套 - 使用
GameObjectFactory注册自定义对象,以便在场景级别访问
有关实现模式,请参阅 game-objects.md。
物理
- Arcade Physics — 用于简单游戏(平台游戏、俯视角游戏),速度快且轻量
- Matter.js — 需要真实碰撞、约束或复杂形状时使用
- 切勿在同一款游戏中混用物理引擎
- 使用状态模式管理角色移动(待机、行走、跳跃、攻击)
详情请参阅 physics-and-movement.md。
性能(关键规则)
- 使用纹理图集 — 将精灵打包到图集中;不要在大规模使用时单独加载图片
- 对象池 — 使用带
maxSize的 Group;通过setActive(false)/setVisible(false)回收对象 - 尽量减少更新工作 — 仅遍历活动对象;使用
getChildren().filter(c => c.active) - 相机剔除 — 用于大型世界;跳过屏幕外对象的渲染
- 批量渲染 — 每帧使用的独特纹理越少,绘制调用批处理效果越好
- 移动设备 — 减少粒子数量、简化物理效果,并考虑以 30fps 为目标
pixelArt: true— 像素风游戏应在游戏配置中启用(最近邻缩放)
完整优化指南请参阅 assets-and-performance.md。
高级模式
- 使用 bitECS 实现 ECS — 面向数据设计的实体组件系统(Phaser 4 内部使用)
- 状态机 — 清晰地管理实体行为状态
- 单例管理器 — 跨场景服务(音频、存档数据、分析)
- 事件总线 — 使用共享的 EventEmitter 解耦系统
- Tiled 集成 — 使用 Tiled 地图编辑器进行关卡设计
有关实现,请参阅 patterns.md。
移动端输入策略(60/40 规则)
除非明确说明,否则所有游戏 MUST 同时支持桌面端和移动端。在权衡取舍时,侧重 60% 移动端 / 40% 桌面端。根据每种游戏概念选择最佳的移动端输入方式:
| 游戏类型 | 主要移动端输入 | 桌面端输入 |
|---|---|---|
| 平台跳跃 | 点击左半边/右半边 + 点击跳跃 | 方向键 / WASD |
| 跑酷/无尽 | 点击 / 向上滑动以跳跃 | 空格键 / 上方向键 |
| 解谜/配对 | 点击目标(至少 44px) | 单击 |
| 射击 | 虚拟摇杆 + 点击开火 | 鼠标 + WASD |
| 俯视角 | 虚拟摇杆 | 方向键 / WASD |
实现模式
将输入抽象为 inputState 对象,让游戏逻辑不依赖特定输入源:
// 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 });
响应式 Canvas 配置(Retina/高 DPI)
完整的响应式 Canvas 配置、实体尺寸、HTML 样板代码和纵向优先的游戏模式,请参阅 project-setup.md。
可见的触控控件
在支持触控的设备上始终显示可见的触控指示器——切勿依赖不可见的点击区域。使用功能检测(而非基于操作系统的检测)来判断是否支持触控:
// 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;
在屏幕底部绘制半透明箭头按钮(或方向指示器)。使用 Constants.js 中的 TOUCH 常量设置尺寸(画布宽度的 12%)、透明度(闲置时 0.35 / 激活时 0.6)和边距。在 update() 循环中根据输入状态更新透明度,以提供视觉反馈。
在所有设备上启用指针输入(pointerdown、pointermove、pointerup)——指针事件同时适用于鼠标和触控。这样就无需为移动端和桌面端分别编写输入代码路径。
移动端实体最小尺寸
可收集物、危险物和交互物品的尺寸必须至少达到 GAME.WIDTH 的 7–8%,才能在手机屏幕上清晰辨认。尺寸更小的实体在移动端会变成难以区分的色块。
// 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,
对于主要玩家角色,使用 GAME.WIDTH 的 12–15%(参见上文的实体尺寸)。
按钮模式(容器 + 图形 + 文本)
完整的按钮实现模式(容器 + 图形 + 文本,以及悬停/按下状态)和应避免的错误模式列表,请参阅 game-objects.md。
反模式(请避免)
- 臃肿的
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 模式(参见 game-objects.md)。常见的错误模式:(1)添加 Text 后绘制 Graphics 矩形,导致标签被遮住。(2)为命中区域创建 Zone,却在其上绘制 Graphics,导致无法触达 Zone。(3)让 Text 可交互,却在之后绘制 Graphics 背景将其遮住。解决方法始终是:先创建 Container,再按顺序将 Graphics 和 Text 添加到容器中;Container 是可交互元素。 - 没有静音切换 —— 请参见
mute-button规则。带音频的游戏必须提供静音切换。
示例
发布前验证清单
在确认游戏完成前,请验证:
- 核心循环正常 — 玩家可以开始游戏、游玩、输赢并看到结果
- 重新开始正常 —
GameState.reset()可恢复干净状态,不残留监听器或计时器 - 触控 + 键盘输入 — 游戏在移动设备上(点击/滑动)和桌面设备上(键盘/鼠标)均可运行
- 画布自适应 — 使用
Scale.FIT+CENTER_BOTH+zoom: 1/DPR,尺寸乘以 DPR,在 Retina 屏幕上清晰显示 - 所有数值均在 Constants 中 — 游戏逻辑中没有硬编码的魔法数字
- 仅使用 EventBus — 通信不通过跨场景/模块直接导入实现
- 场景清理 — 在
shutdown()中移除所有 EventBus 监听器 - 物理设置完整 — 每个静态物体都有显式的
collider()或overlap()调用 - 对象池 — 频繁创建/销毁的对象使用带有
maxSize的 Groups - 基于时间增量的移动 — 所有运动都使用
delta,而非帧数 - 静音切换 — 参见
mute-button规则 - Spectacle 钩子已接入 — 每个玩家操作和游戏事件都会触发一个
SPECTACLE_*事件;入场序列在create()中触发 - 构建通过 —
npm run build成功执行且无错误 - 没有控制台错误 — 游戏运行时没有未捕获的异常或 WebGL 故障
参考文件
| 文件 | 主题 |
|---|---|
| conventions.md | 游戏创作者必须遵守的架构约定 |
| 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 | 程序化视觉效果:渐变、视差、粒子、表现力 |