关于 phaser-gamedev
面向 Phaser 3 的 2D 浏览器游戏开发 Skill,指导开发者设计场景、物理、资源和输入,并组织可维护的游戏结构。
主要功能
- 01基于场景的游戏架构
- 02Arcade 与 Matter 物理指导
- 03键盘与指针输入
- 04资源加载与 Tilemap
- 05动画与性能实践
适用场景
- 01规划场景与游戏状态
- 02实现 2D 碰撞和移动
- 03加载精灵表与 Tiled 地图
- 04规避常见 Phaser 实现问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ chongdashu/ phaserjs-tinyswords --skill phaser-gamedev安装后在 Claude Code 中输入/phaser-gamedev确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/arcade-physics.mdperformance.mdspritesheets-nineslice.mdtilemaps.md详细介绍
Phaser 游戏开发
使用 Phaser 3 基于场景的架构和物理系统,快速构建精致的 2D 浏览器游戏。
理念:将游戏视为动态系统
游戏不是静态 UI,而是动态系统:实体彼此交互,状态不断演变,玩家输入驱动一切。编写代码前,先进行架构设计。
开始构建前,请思考:
- 这个游戏需要哪些场景?(启动、菜单、游戏、暂停、游戏结束)
- 有哪些实体,它们如何交互?
- 哪些状态需要在场景间保留?
- 哪种物理模型最合适?(追求速度用 Arcade,追求真实感用 Matter)
- 玩家会使用哪些输入方式?
核心原则:
- 场景优先架构:围绕场景组织游戏,而非全局状态
- 组合优于继承:使用游戏对象和组件构建实体
- 考虑物理的设计:编写碰撞代码前选择物理系统
- 严谨管理资源管线:预加载所有资源,并通过键引用
- 与帧率无关:使用增量时间,而非帧计数
游戏配置
每个 Phaser 游戏都从一个配置对象开始。
最简配置
const config = {
type: Phaser.AUTO, // WebGL with Canvas fallback
width: 800,
height: 600,
scene: [BootScene, GameScene]
};
const game = new Phaser.Game(config);
完整配置模式
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
parent: 'game-container', // DOM element ID
backgroundColor: '#2d2d2d',
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_BOTH
},
physics: {
default: 'arcade',
arcade: {
gravity: { y: 300 },
debug: false // Enable during development
}
},
scene: [BootScene, MenuScene, GameScene, GameOverScene]
};
物理系统选择
| 系统 | 适用场景 |
|---|---|
| Arcade | 平台游戏、射击游戏和大多数 2D 游戏。速度快,AABB 碰撞简单 |
| Matter | 物理谜题、布娃娃和真实碰撞。速度较慢,但精度更高 |
| 无 | 菜单场景、视觉小说、卡牌游戏 |
场景架构
场景是基本的组织单位。每个场景都有生命周期。
场景生命周期方法
class GameScene extends Phaser.Scene {
constructor() {
super('GameScene'); // Scene key for reference
}
init(data) {
// Called first. Receive data from previous scene
this.level = data.level || 1;
}
preload() {
// Load assets. Runs before create()
this.load.image('player', 'assets/player.png');
this.load.spritesheet('enemy', 'assets/enemy.png', {
frameWidth: 32, frameHeight: 32
});
}
create() {
// Set up game objects, physics, input
this.player = this.physics.add.sprite(100, 100, 'player');
this.cursors = this.input.keyboard.createCursorKeys();
}
update(time, delta) {
// Game loop. Called every frame
// delta = milliseconds since last frame
this.player.x += this.speed * (delta / 1000);
}
}
场景切换
// Start a new scene (stops current)
this.scene.start('GameOverScene', { score: this.score });
// Launch scene in parallel (both run)
this.scene.launch('UIScene');
// Pause/resume scenes
this.scene.pause('GameScene');
this.scene.resume('GameScene');
// Stop a scene
this.scene.stop('UIScene');
推荐的场景结构
scenes/
├── BootScene.js # Asset loading, progress bar
├── MenuScene.js # Title screen, options
├── GameScene.js # Main gameplay
├── UIScene.js # HUD overlay (launched parallel)
├── PauseScene.js # Pause menu overlay
└── GameOverScene.js # End screen, restart option
游戏对象
Phaser 中所有可见内容都是游戏对象。
常见游戏对象
// Images (static)
this.add.image(400, 300, 'background');
// Sprites (can animate, physics-enabled)
const player = this.add.sprite(100, 100, 'player');
// Text
const score = this.add.text(16, 16, 'Score: 0', {
fontSize: '32px',
fill: '#fff'
});
// Graphics (draw shapes)
const graphics = this.add.graphics();
graphics.fillStyle(0xff0000);
graphics.fillRect(100, 100, 50, 50);
// Containers (group objects)
const container = this.add.container(400, 300, [sprite1, sprite2]);
// Tilemaps
const map = this.make.tilemap({ key: 'level1' });
精灵创建模式
// Basic sprite
const sprite = this.add.sprite(x, y, 'textureKey');
// Sprite with physics body
const sprite = this.physics.add.sprite(x, y, 'textureKey');
// From spritesheet frame
const sprite = this.add.sprite(x, y, 'sheet', frameIndex);
// From atlas
const sprite = this.add.sprite(x, y, 'atlas', 'frameName');
物理系统
Arcade 物理(推荐默认选项)
适用于大多数 2D 游戏的快速、简单物理系统。
// Enable physics on sprite
this.physics.add.sprite(x, y, 'player');
// Or add physics to existing sprite
this.physics.add.existing(sprite);
// Configure body
sprite.body.setVelocity(200, 0);
sprite.body.setBounce(0.5);
sprite.body.setCollideWorldBounds(true);
sprite.body.setGravityY(300);
// Collision detection
this.physics.add.collider(player, platforms);
this.physics.add.overlap(player, coins, collectCoin, null, this);
function collectCoin(player, coin) {
coin.disableBody(true, true); // Remove from physics and hide
this.score += 10;
}
物理组
// Static group (platforms, walls)
const platforms = this.physics.add.staticGroup();
platforms.create(400, 568, 'ground').setScale(2).refreshBody();
// Dynamic group (enemies, bullets)
const enemies = this.physics.add.group({
key: 'enemy',
repeat: 5,
setXY: { x: 100, y: 0, stepX: 70 }
});
enemies.children.iterate(enemy => {
enemy.setBounce(Phaser.Math.FloatBetween(0.4, 0.8));
});
Matter 物理
用于真实的物理模拟。
// Config
physics: {
default: 'matter',
matter: {
gravity: { y: 1 },
debug: true
}
}
// Create bodies
const ball = this.matter.add.circle(400, 100, 25);
const box = this.matter.add.rectangle(400, 400, 100, 50, { isStatic: true });
// Sprite with Matter body
const player = this.matter.add.sprite(100, 100, 'player');
player.setFriction(0.005);
player.setBounce(0.9);
输入处理
键盘输入
// Cursor keys
this.cursors = this.input.keyboard.createCursorKeys();
// In update()
if (this.cursors.left.isDown) {
player.setVelocityX(-160);
} else if (this.cursors.right.isDown) {
player.setVelocityX(160);
}
if (this.cursors.up.isDown && player.body.touching.down) {
player.setVelocityY(-330); // Jump
}
// Custom keys
this.spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);
// Key events
this.input.keyboard.on('keydown-SPACE', () => {
this.fire();
});
指针/鼠标输入
// Click/tap
this.input.on('pointerdown', (pointer) => {
console.log(pointer.x, pointer.y);
});
// Make object interactive
sprite.setInteractive();
sprite.on('pointerdown', () => {
sprite.setTint(0xff0000);
});
sprite.on('pointerup', () => {
sprite.clearTint();
});
// Drag
this.input.setDraggable(sprite);
this.input.on('drag', (pointer, obj, dragX, dragY) => {
obj.x = dragX;
obj.y = dragY;
});
动画
创建动画
// In create() - define once
this.anims.create({
key: 'walk',
frames: this.anims.generateFrameNumbers('player', { start: 0, end: 3 }),
frameRate: 10,
repeat: -1 // Loop forever
});
this.anims.create({
key: 'jump',
frames: [{ key: 'player', frame: 4 }],
frameRate: 20
});
// From atlas
this.anims.create({
key: 'explode',
frames: this.anims.generateFrameNames('atlas', {
prefix: 'explosion_',
start: 1,
end: 8,
zeroPad: 2
}),
frameRate: 16,
hideOnComplete: true
});
播放动画
// Play animation
sprite.anims.play('walk', true); // true = ignore if already playing
// Play once
sprite.anims.play('jump');
// Stop
sprite.anims.stop();
// Animation events
sprite.on('animationcomplete', (anim, frame) => {
if (anim.key === 'die') {
sprite.destroy();
}
});
资源加载
预加载模式
preload() {
// Images
this.load.image('sky', 'assets/sky.png');
// Spritesheets
this.load.spritesheet('player', 'assets/player.png', {
frameWidth: 32,
frameHeight: 48
});
// Atlases (TexturePacker)
this.load.atlas('sprites', 'assets/sprites.png', 'assets/sprites.json');
// Tilemaps
this.load.tilemapTiledJSON('map', 'assets/level1.json');
this.load.image('tiles', 'assets/tileset.png');
// Audio
this.load.audio('bgm', 'assets/music.mp3');
this.load.audio('sfx', ['assets/sound.ogg', 'assets/sound.mp3']);
// Progress tracking
this.load.on('progress', (value) => {
console.log(`Loading: ${Math.round(value * 100)}%`);
});
}
启动场景模式
class BootScene extends Phaser.Scene {
constructor() {
super('BootScene');
}
preload() {
// Loading bar
const width = this.cameras.main.width;
const height = this.cameras.main.height;
const progressBar = this.add.graphics();
const progressBox = this.add.graphics();
progressBox.fillStyle(0x222222, 0.8);
progressBox.fillRect(width/2 - 160, height/2 - 25, 320, 50);
this.load.on('progress', (value) => {
progressBar.clear();
progressBar.fillStyle(0xffffff, 1);
progressBar.fillRect(width/2 - 150, height/2 - 15, 300 * value, 30);
});
// Load all game assets here
this.load.image('player', 'assets/player.png');
// ... more assets
}
create() {
this.scene.start('MenuScene');
}
}
瓦片地图(Tiled 集成)
加载与创建
preload() {
this.load.tilemapTiledJSON('map', 'assets/map.json');
this.load.image('tiles', 'assets/tileset.png');
}
create() {
const map = this.make.tilemap({ key: 'map' });
const tileset = map.addTilesetImage('tileset-name-in-tiled', 'tiles');
// Create layers (match names from Tiled)
const backgroundLayer = map.createLayer('Background', tileset, 0, 0);
const groundLayer = map.createLayer('Ground', tileset, 0, 0);
// Enable collision on specific tiles
groundLayer.setCollisionByProperty({ collides: true });
// Or by tile index
groundLayer.setCollisionBetween(1, 100);
// Add collision with player
this.physics.add.collider(this.player, groundLayer);
}
对象图层
// Spawn points from Tiled object layer
const spawnPoint = map.findObject('Objects', obj => obj.name === 'spawn');
this.player = this.physics.add.sprite(spawnPoint.x, spawnPoint.y, 'player');
// Create objects from layer
const coins = map.createFromObjects('Objects', {
name: 'coin',
key: 'coin'
});
this.physics.world.enable(coins);
项目结构
推荐的组织方式
game/
├── src/
│ ├── scenes/
│ │ ├── BootScene.js
│ │ ├── MenuScene.js
│ │ ├── GameScene.js
│ │ └── UIScene.js
│ ├── gameObjects/
│ │ ├── Player.js
│ │ ├── Enemy.js
│ │ └── Collectible.js
│ ├── systems/
│ │ ├── InputManager.js
│ │ └── AudioManager.js
│ ├── config/
│ │ └── gameConfig.js
│ └── main.js
├── assets/
│ ├── images/
│ ├── audio/
│ ├── tilemaps/
│ └── fonts/
├── index.html
└── package.json
ES 模块设置
// main.js
import Phaser from 'phaser';
import BootScene from './scenes/BootScene';
import GameScene from './scenes/GameScene';
import { gameConfig } from './config/gameConfig';
const config = {
...gameConfig,
scene: [BootScene, GameScene]
};
new Phaser.Game(config);
应避免的反模式
❌ 混乱的全局状态:将游戏状态存储在 window 或模块全局变量中
为什么不好:错误无法追踪,场景切换会导致状态异常
更好的做法:使用场景数据、注册表或专用状态管理器
❌ 在 Create 中加载:在 create() 而非 preload() 中加载资源
为什么不好:引用资源时,资源可能尚未就绪
更好的做法:始终在 preload() 中加载,并使用 Boot 场景加载所有资源
❌ 依赖帧数的逻辑:使用帧数而非 delta time
为什么不好:游戏速度会随帧率变化
更好的做法:使用 this.speed * (delta / 1000) 确保移动速度一致
❌ 过度使用物理引擎:为简单的平台游戏碰撞使用 Matter 为什么不好:会影响性能,并增加不必要的复杂性 更好的做法:Arcade physics 可满足 90% 的 2D 游戏需求
❌ 臃肿的场景:用一个庞大的场景承载所有游戏逻辑 为什么不好:难以维护,也难以添加功能 更好的做法:为菜单、游戏过程和 UI 覆盖层分别创建场景
❌ 魔法数字:在代码各处散布硬编码数值 为什么不好:无法调整平衡,数值也不一致 更好的做法:使用配置对象和常量文件
❌ 忽略对象池:每帧都创建和销毁对象
为什么不好:内存频繁变动,垃圾回收会造成卡顿
更好的做法:使用带有 setActive(false) / setVisible(false) 的组
❌ 同步访问资源:假设资源会立即加载完成 为什么不好:会出现竞态条件和未定义的纹理 更好的做法:串联场景启动流程,并使用加载事件
❌ 想当然地设定精灵表帧尺寸:不经验证就猜测帧尺寸 为什么不好:尺寸错误会悄无声息地破坏帧;像素级的偏差会逐渐累积,造成画面异常 更好的做法:打开资源文件测量帧尺寸,根据间距和边距计算,并验证计算结果是否正确
❌ 忽略精灵表间距:对于存在间隙的精灵表未指定 spacing
为什么不好:帧的位置会逐渐偏移,后续帧会读取错误的像素区域
更好的做法:检查源资源中各帧之间是否存在间隙;在加载器配置中使用 spacing: N
❌ 硬编码九宫格切片颜色:所有 UI 面板变体都使用同一种背景色 为什么不好:透明的边缘会显露出与不同资源配色不匹配的颜色 更好的做法:为每个资源单独配置背景色;从中间帧(第 4 帧)采样
❌ 对带内边距的帧使用九宫格切片:当图像在每个图块内居中或带有内边距时,仍将整个帧作为切片区域 为什么不好:边缘图块会带入内部填充,在面板内显示为不透明的“侧边条” 更好的做法:将图块裁剪到实际内容边界(alpha bbox),再合成或缓存为纹理;增加约 1px 的重叠并禁用平滑,以避免接缝
❌ 缩放不连续的 UI 图像:拉伸包含内部透明间隙的裁剪条幅行 为什么不好:透明间隔会被拉伸,导致 UI 看起来不连续,或填充部分被边框遮住。 更好的做法:将资源切分为两端和中间部分,只拉伸中间部分,再拼接各部分(重叠约 1px 并禁用平滑),最后按目标尺寸渲染。
差异化指导
重要:游戏实现应根据以下因素进行调整:
- 游戏类型:平台游戏的物理机制与俯视角射击游戏不同
- 目标平台:移动端需要触控输入,桌面端可以使用键盘
- 美术风格:像素艺术使用最近邻缩放,高清美术使用线性缩放
- 性能需求:精灵较多时使用对象池;精灵较少时可直接创建
- 复杂度:简单游戏可以内联实现;复杂游戏需要类层次结构
避免千篇一律地采用:
- 始终使用 800x600 分辨率
- 始终使用 Arcade physics
- 始终采用相同的场景结构
- 不加调整地复制粘贴样板代码
快速参考
常用物理属性
body.setVelocity(x, y)
body.setVelocityX(x)
body.setBounce(x, y)
body.setGravityY(y)
body.setCollideWorldBounds(true)
body.setImmovable(true) // For static-like dynamic bodies
body.setDrag(x, y)
body.setMaxVelocity(x, y)
实用的场景属性
this.cameras.main // Main camera
this.physics.world // Physics world
this.input.keyboard // Keyboard manager
this.sound // Audio manager
this.time // Time/clock manager
this.tweens // Tween manager
this.anims // Animation manager
this.registry // Cross-scene data store
this.data // Scene-specific data store
必备事件
// Scene events
this.events.on('pause', callback)
this.events.on('resume', callback)
this.events.on('shutdown', callback)
// Physics events
this.physics.world.on('worldbounds', callback)
// Game object events
sprite.on('destroy', callback)
sprite.on('animationcomplete', callback)
另请参阅
- references/arcade-physics.md - 深入了解 Arcade 物理
- references/tilemaps.md - 高级瓦片地图技术
- references/performance.md - 优化策略
- references/spritesheets-nineslice.md - 精灵表加载(间距/边距)、九宫格 UI 面板、资源检查
请记住
Phaser 为你提供了强大的基础功能——场景、精灵、物理和输入——但架构由你负责。
从系统的角度思考:你需要哪些场景?有哪些实体?它们如何交互?在编写代码前回答这些问题,你的游戏才能随着规模扩大而保持易于维护。
Claude 有能力构建完整、精良的 Phaser 游戏。这些指南为你指明方向——而非设限。