phaser by opusgamelabs/game-creator
npx skills add https://github.com/opusgamelabs/game-creator --skill phaser你是一位使用 game-creator 插件构建游戏的 Phaser 游戏开发专家。遵循以下模式来制作结构良好、视觉精美且易于维护的 2D 浏览器游戏。
SPECTACLE_* 事件)与核心循环一起连接——它们是脚手架的一部分,不是延后的润色。phaserjs/template-vite-ts 模板GameState.reset() 必须恢复到干净的状态。重启之间不能有陈旧的引用、残留的计时器或泄漏的事件监听器。每个玩家操作和游戏事件必须至少触发一个特效事件。这些钩子存在于模板 EventBus 中——设计阶段会将视觉效果附加到它们上面。
| 事件 |
|---|
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 常量 |
|---|
| 触发时机 |
|---|
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 和 Constantsdomain:action 事件命名,无直接场景引用reset() 的集中式状态,用于干净重启shutdown() 中移除 EventBus 监听器完整细节和代码示例请参见 conventions.md。
使用官方的 Vite + TypeScript 模板作为起点:
npx degit phaserjs/template-vite-ts my-game
cd my-game && npm install
src/
├── core/
│ ├── EventBus.ts # 单例事件总线 + 事件常量
│ ├── GameState.ts # 具有 reset() 的集中式状态
│ └── Constants.ts # 所有配置值
├── scenes/
│ ├── Boot.ts # 最小设置,启动 Game 场景
│ ├── Preloader.ts # 加载所有资源,显示进度条
│ ├── Game.ts # 主要游戏玩法(立即开始,无标题屏幕)
│ └── GameOver.ts # 带有重启的结束屏幕
├── objects/ # 游戏实体(Player, Enemy 等)
├── systems/ # 管理器和子系统
├── ui/ # UI 组件(按钮、条、对话框)
├── audio/ # 音频管理器、音乐、音效
├── config.ts # Phaser.Types.Core.GameConfig
└── main.ts # 入口点
完整配置和工具细节请参见 project-setup.md。
init() → preload() → create() → update(time, delta)init() 接收来自场景转换的数据Preloader 场景中加载资源,而不是在每个场景中update() 精简——委托给子系统和游戏对象当游戏在移动端 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';
// 在任何 UI 场景中: 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);
// 触摸控件 / 底部 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,
};
模式和示例请参见 scenes-and-lifecycle.md。
Phaser.GameObjects.Sprite(或其他基类)Phaser.GameObjects.Group 进行对象池化(子弹、硬币、敌人)Phaser.GameObjects.Container 处理复合对象,但避免深度嵌套GameObjectFactory 注册自定义对象以进行场景级访问实现模式请参见 game-objects.md。
详细信息请参见 physics-and-movement.md。
maxSize 的组;使用 setActive(false) / setVisible(false) 回收getChildren().filter(c => c.active)pixelArt: true — 在像素艺术游戏的游戏配置中启用(最近邻缩放)完整优化指南请参见 assets-and-performance.md。
实现请参见 patterns.md。
所有游戏必须在桌面和移动端都能运行,除非另有明确说明。权衡时关注 60% 移动端 / 40% 桌面。为每个游戏概念选择最佳的移动端输入:
| 游戏类型 | 主要移动端输入 | 桌面输入 |
|---|---|---|
| 平台游戏 | 点击左/右半屏 + 点击跳跃 | 方向键 / WASD |
| 跑酷/无尽 | 点击 / 向上滑动跳跃 | 空格键 / 上方向键 |
| 解谜/匹配 | 点击目标(最小 44px) | 点击 |
| 射击游戏 | 虚拟摇杆 + 点击开火 | 鼠标 + WASD |
| 俯视角 | 虚拟摇杆 | 方向键 / WASD |
将输入抽象到 inputState 对象中,使游戏逻辑与输入源无关:
// 在 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 });
完整的响应式画布配置、实体大小调整、HTML 样板和竖屏优先游戏模式请参见 project-setup.md。
始终在支持触摸的设备上显示视觉触摸指示器——切勿依赖不可见的点击区域。使用能力检测(而非基于操作系统的检测)来确定触摸支持:
// 好 — 检测触摸笔记本电脑、平板电脑、二合一设备
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)——指针事件对鼠标和触摸都有效。这消除了对单独的移动端/桌面输入代码路径的需求。
可收集物、危险物和交互物品必须至少为 GAME.WIDTH 的 7–8%,才能在手机屏幕上被识别。较小的实体在移动端会变成无法区分的斑点。
// 好 — 在移动端可识别
ATTACK_WIDTH: _canvasW * 0.09,
POWERUP_WIDTH: _canvasW * 0.072,
// 坏 — 在手机屏幕上太小
ATTACK_WIDTH: _canvasW * 0.04,
POWERUP_WIDTH: _canvasW * 0.035,
对于主要玩家角色,使用 GAME.WIDTH 的 12–15%(参见上面的实体大小调整)。
完整的按钮实现模式(容器 + 图形 + 文本,带有悬停/按下状态)以及要避免的损坏模式列表,请参见 game-objects.md。
update() 方法 — 不要将所有游戏逻辑放在一个带有嵌套条件语句的巨大更新中。委托给对象和系统。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 峰值。始终对频繁创建/销毁的对象进行池化。避免昂贵的每帧分配——重用对象、数组和临时变量。delta — 始终使用 delta 进行基于时间的移动,而不是基于帧的移动。shutdown() 中移除事件监听器和计时器以防止内存泄漏。这对于重启安全至关重要——陈旧的监听器会导致重启后重复触发和幽灵行为。Constants.ts。游戏逻辑中不能有魔法数字。physics.add.existing(obj, true) 创建静态体本身不会做任何事情。你必须调用 physics.add.collider(bodyA, bodyB, callback) 来连接两个物体。每个静态碰撞器(地面、墙壁、平台)都需要一个显式的碰撞器或重叠调用来连接它应该与之交互的实体。setAlpha(0) 并在其上叠加图形或其他显示对象。对于按钮,始终使用容器 + 图形 + 文本模式(参见 game-objects.md)。常见的损坏模式:(1) 添加文本后绘制图形矩形,将标签隐藏在它后面。(2) 为命中区域创建一个 Zone,并在其上绘制图形,使 Zone 无法触及。(3) 使文本可交互,但随后在其上绘制图形背景。修复方法总是:先创建容器,将图形添加到容器,将文本添加到容器(按此顺序),容器是交互元素。mute-button 规则。有音频的游戏必须有静音切换。在认为游戏完成之前,请验证:
GameState.reset() 恢复到干净的状态,没有陈旧的监听器或计时器Scale.FIT + CENTER_BOTH + zoom: 1/DPR 配合 DPR 倍增的尺寸,在 Retina 屏幕上清晰shutdown() 中被移除collider() 或 overlap() 调用maxSize 的组delta,而不是帧计数mute-button 规则SPECTACLE_* 事件;入场序列在 create() 中触发npm run build 成功且无错误| 文件 | 主题 |
|---|---|
| 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 | 程序化视觉效果:渐变、视差、粒子、细节润色 |
每周安装次数
145
仓库
GitHub 星标数
26
首次出现
2026年2月21日
安全审计
已安装于
claude-code104
opencode98
codex97
cursor96
gemini-cli96
github-copilot96
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.
SPECTACLE_* events) alongside the core loop — they are part of scaffolding, not deferred polish.phaserjs/template-vite-ts templateGameState.reset() must restore a clean slate. No stale references, lingering timers, or leaked event listeners across restarts.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 |
Rule : If a gameplay moment has no spectacle event, add one. The design pass cannot polish what it cannot hook into.
All games MUST follow the game-creator conventions:
core/ directory with EventBus, GameState, and Constantsdomain:action event naming, no direct scene referencesreset() for clean restartsshutdown()See conventions.md for full details and code examples.
Use the official Vite + TypeScript template as your starting point:
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
See project-setup.md for full config and tooling details.
init() → preload() → create() → update(time, delta)init() for receiving data from scene transitionsPreloader scene, not in every sceneupdate() lean — delegate to subsystems and game objectsWhen 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
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);
How it works in 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,
};
See scenes-and-lifecycle.md for patterns and examples.
Phaser.GameObjects.Sprite (or other base classes) for custom objectsPhaser.GameObjects.Group for object pooling (bullets, coins, enemies)Phaser.GameObjects.Container for composite objects, but avoid deep nestingGameObjectFactory for scene-level accessSee game-objects.md for implementation patterns.
See physics-and-movement.md for details.
maxSize; recycle with setActive(false) / setVisible(false)getChildren().filter(c => c.active)pixelArt: true — Enable in game config for pixel art games (nearest-neighbor scaling)See assets-and-performance.md for full optimization guide.
See patterns.md for implementations.
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 |
Abstract input into an inputState object so game logic is source-agnostic:
// 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 });
See project-setup.md for the full responsive canvas config, entity sizing, HTML boilerplate, and portrait-first game patterns.
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:
// 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.
Collectibles, hazards, and interactive items must be at least 7–8% ofGAME.WIDTH to be recognizable on phone screens. Smaller entities become indistinguishable blobs on mobile.
// 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).
See game-objects.md for the full button implementation pattern (Container + Graphics + Text with hover/press states) and the list of broken patterns to avoid.
update() methods — Don't put all game logic in one giant update with nested conditionals. Delegate to objects and systems.world, input, cameras, add, make, scene, sys, game, cache, registry, , , , , , , , , , , , , or . These are reserved by Phaser.Before considering a game complete, verify:
GameState.reset() restores a clean slate, no stale listeners or timersScale.FIT + CENTER_BOTH + zoom: 1/DPR with DPR-multiplied dimensions, crisp on Retinashutdown()collider() or overlap() call| File | Topic |
|---|---|
| conventions.md | Mandatory game-creator architecture conventions |
| project-setup.md | Scaffolding, Vite, TypeScript config, responsive canvas, entity sizing, portrait mode |
| scenes-and-lifecycle.md | Scene system deep dive |
| game-objects.md | Custom objects, groups, containers, button pattern |
| physics-and-movement.md | Physics engines, movement patterns |
| assets-and-performance.md | Assets, optimization, mobile |
| patterns.md | ECS, state machines, singletons |
Weekly Installs
145
Repository
GitHub Stars
26
First Seen
Feb 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
claude-code104
opencode98
codex97
cursor96
gemini-cli96
github-copilot96
Genkit JS 开发指南:AI 应用构建、错误排查与最佳实践
6,100 周安装
| 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) |
// Touch controls / bottom HUD: const bottomY = GAME.HEIGHT - safeBottom - 40 * PX;
soundtextureseventsphysicsmattertimetweenslightsdataloadanimsrendererpluginsupdate() without pooling — This causes GC spikes. Always pool frequently created/destroyed objects. Avoid expensive per-frame allocations — reuse objects, arrays, and temporary variables.delta in update — Always use delta for time-based movement, not frame-based.shutdown() to prevent memory leaks. This is critical for restart-safety — stale listeners cause double-firing and ghost behavior after restart.Constants.ts. No magic numbers in game logic.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.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). 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.mute-button rule. Games with audio must have a mute toggle.maxSizedelta, not frame countmute-button ruleSPECTACLE_* event; entrance sequence fires in create()npm run build succeeds with no errors| no-asset-design.md | Procedural visuals: gradients, parallax, particles, juice |