phaser-scene
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePhaser 4 Scene Creation
Phaser 4 场景创建
Every Phaser 4 game is composed of Scene classes. Each scene is a self-contained unit with its own lifecycle, assets, and game objects.
每个Phaser 4游戏都由Scene类组成。每个场景都是一个独立的单元,拥有自己的生命周期、资源和游戏对象。
Scene Class Pattern
场景类模式
The canonical pattern for every scene:
typescript
import Phaser from 'phaser';
export class MyScene extends Phaser.Scene {
constructor() {
super({ key: 'MyScene' }); // key MUST be unique across all scenes
}
// Called when scene starts, before preload. Receive data from previous scene.
init(data?: Record<string, unknown>): void {
// e.g., data.level, data.score from this.scene.start('MyScene', { level: 2 })
}
// Load assets used only by this scene (prefer PreloaderScene for shared assets)
preload(): void { }
// Build the scene: create game objects, physics, input, events
create(): void { }
// Called every frame. Keep lean — call helper methods
update(time: number, delta: number): void { }
// Called when scene is about to be stopped or replaced. Clean up here.
shutdown(): void {
// Remove event listeners; stop sounds; clear tracked timers
}
}Register every scene in GameConfig:
main.tstypescript
scene: [BootScene, PreloaderScene, MainMenuScene, GameScene, GameOverScene]每个场景的标准模式:
typescript
import Phaser from 'phaser';
export class MyScene extends Phaser.Scene {
constructor() {
super({ key: 'MyScene' }); // key 在所有场景中必须唯一
}
// 场景启动时调用,在preload之前。接收来自前一个场景的数据。
init(data?: Record<string, unknown>): void {
// 例如,从this.scene.start('MyScene', { level: 2 })获取data.level、data.score
}
// 加载仅用于此场景的资源(共享资源优先使用PreloaderScene)
preload(): void { }
// 构建场景:创建游戏对象、物理系统、输入、事件
create(): void { }
// 每帧调用。保持精简——调用辅助方法
update(time: number, delta: number): void { }
// 场景即将停止或替换时调用。在此处清理资源。
shutdown(): void {
// 移除事件监听器;停止音效;清除跟踪的计时器
}
}在的GameConfig中注册所有场景:
main.tstypescript
scene: [BootScene, PreloaderScene, MainMenuScene, GameScene, GameOverScene]Scene Type Patterns
场景类型模式
BootScene
BootScene
Minimal. Loads only the assets needed for the loading bar, immediately transitions.
typescript
export class BootScene extends Phaser.Scene {
constructor() { super({ key: 'BootScene' }); }
preload(): void {
this.load.image('loading-bg', 'assets/images/loading-bg.png');
this.load.image('loading-bar', 'assets/images/loading-bar.png');
}
create(): void {
this.scene.start('PreloaderScene');
}
}极简模式。仅加载加载条所需的资源,立即切换场景。
typescript
export class BootScene extends Phaser.Scene {
constructor() { super({ key: 'BootScene' }); }
preload(): void {
this.load.image('loading-bg', 'assets/images/loading-bg.png');
this.load.image('loading-bar', 'assets/images/loading-bar.png');
}
create(): void {
this.scene.start('PreloaderScene');
}
}PreloaderScene
PreloaderScene
Shows a loading bar while loading ALL game assets. Set up global animations here too.
typescript
export class PreloaderScene extends Phaser.Scene {
constructor() { super({ key: 'PreloaderScene' }); }
preload(): void {
const { width, height } = this.scale;
// Loading bar
const bg = this.add.graphics();
bg.fillStyle(0x111111, 0.8);
bg.fillRect(width / 2 - 160, height / 2 - 25, 320, 50);
const bar = this.add.graphics();
this.load.on('progress', (value: number) => {
bar.clear();
bar.fillStyle(0x00ff88, 1);
bar.fillRect(width / 2 - 150, height / 2 - 15, 300 * value, 30);
});
// ── Load all game assets here ──
this.load.atlas('characters', 'assets/atlases/characters.png', 'assets/atlases/characters.json');
this.load.audio('bgm', ['assets/audio/bgm.mp3', 'assets/audio/bgm.ogg']);
}
create(): void {
// Create all animations here — available globally after this
this.anims.create({
key: 'player-idle',
frames: this.anims.generateFrameNumbers('player', { start: 0, end: 3 }),
frameRate: 8,
repeat: -1,
});
this.scene.start('MainMenuScene');
}
}显示加载条的同时加载所有游戏资源。也可在此处设置全局动画。
typescript
export class PreloaderScene extends Phaser.Scene {
constructor() { super({ key: 'PreloaderScene' }); }
preload(): void {
const { width, height } = this.scale;
// 加载条
const bg = this.add.graphics();
bg.fillStyle(0x111111, 0.8);
bg.fillRect(width / 2 - 160, height / 2 - 25, 320, 50);
const bar = this.add.graphics();
this.load.on('progress', (value: number) => {
bar.clear();
bar.fillStyle(0x00ff88, 1);
bar.fillRect(width / 2 - 150, height / 2 - 15, 300 * value, 30);
});
// ── 在此处加载所有游戏资源 ──
this.load.atlas('characters', 'assets/atlases/characters.png', 'assets/atlases/characters.json');
this.load.audio('bgm', ['assets/audio/bgm.mp3', 'assets/audio/bgm.ogg']);
}
create(): void {
// 在此处创建所有动画——之后全局可用
this.anims.create({
key: 'player-idle',
frames: this.anims.generateFrameNumbers('player', { start: 0, end: 3 }),
frameRate: 8,
repeat: -1,
});
this.scene.start('MainMenuScene');
}
}MainMenuScene
MainMenuScene
Title screen. Handles start button, settings navigation.
typescript
export class MainMenuScene extends Phaser.Scene {
constructor() { super({ key: 'MainMenuScene' }); }
create(): void {
const { width, height } = this.scale;
this.add.image(width / 2, height / 2, 'menu-bg');
this.add.text(width / 2, height * 0.3, 'MY GAME', {
fontSize: '64px',
color: '#ffffff',
fontFamily: 'Arial Black',
}).setOrigin(0.5);
const startBtn = this.add.text(width / 2, height * 0.6, 'PLAY', {
fontSize: '32px',
color: '#00ff88',
backgroundColor: '#333',
padding: { x: 20, y: 10 },
}).setOrigin(0.5).setInteractive({ useHandCursor: true });
startBtn.on('pointerover', () => startBtn.setStyle({ color: '#ffff00' }));
startBtn.on('pointerout', () => startBtn.setStyle({ color: '#00ff88' }));
startBtn.on('pointerdown', () => {
this.cameras.main.fadeOut(500, 0, 0, 0, () => {
this.scene.start('GameScene', { level: 1 });
});
});
}
}标题界面。处理开始按钮、设置导航。
typescript
export class MainMenuScene extends Phaser.Scene {
constructor() { super({ key: 'MainMenuScene' }); }
create(): void {
const { width, height } = this.scale;
this.add.image(width / 2, height / 2, 'menu-bg');
this.add.text(width / 2, height * 0.3, 'MY GAME', {
fontSize: '64px',
color: '#ffffff',
fontFamily: 'Arial Black',
}).setOrigin(0.5);
const startBtn = this.add.text(width / 2, height * 0.6, 'PLAY', {
fontSize: '32px',
color: '#00ff88',
backgroundColor: '#333',
padding: { x: 20, y: 10 },
}).setOrigin(0.5).setInteractive({ useHandCursor: true });
startBtn.on('pointerover', () => startBtn.setStyle({ color: '#ffff00' }));
startBtn.on('pointerout', () => startBtn.setStyle({ color: '#00ff88' }));
startBtn.on('pointerdown', () => {
this.cameras.main.fadeOut(500, 0, 0, 0, () => {
this.scene.start('GameScene', { level: 1 });
});
});
}
}GameOverScene
GameOverScene
End state. Display score, offer restart.
typescript
export class GameOverScene extends Phaser.Scene {
constructor() { super({ key: 'GameOverScene' }); }
init(data: { score: number }): void {
this.registry.set('finalScore', data.score);
}
create(): void {
const { width, height } = this.scale;
const score = this.registry.get('finalScore') as number;
this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.7);
this.add.text(width / 2, height * 0.35, 'GAME OVER', {
fontSize: '56px', color: '#ff4444',
}).setOrigin(0.5);
this.add.text(width / 2, height * 0.5, `Score: ${score}`, {
fontSize: '32px', color: '#ffffff',
}).setOrigin(0.5);
this.add.text(width / 2, height * 0.65, 'Play Again', {
fontSize: '28px', color: '#00ff88',
}).setOrigin(0.5).setInteractive({ useHandCursor: true })
.on('pointerdown', () => this.scene.start('GameScene', { level: 1 }));
}
}结束状态。显示分数,提供重新开始选项。
typescript
export class GameOverScene extends Phaser.Scene {
constructor() { super({ key: 'GameOverScene' }); }
init(data: { score: number }): void {
this.registry.set('finalScore', data.score);
}
create(): void {
const { width, height } = this.scale;
const score = this.registry.get('finalScore') as number;
this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.7);
this.add.text(width / 2, height * 0.35, 'GAME OVER', {
fontSize: '56px', color: '#ff4444',
}).setOrigin(0.5);
this.add.text(width / 2, height * 0.5, `Score: ${score}`, {
fontSize: '32px', color: '#ffffff',
}).setOrigin(0.5);
this.add.text(width / 2, height * 0.65, 'Play Again', {
fontSize: '28px', color: '#00ff88',
}).setOrigin(0.5).setInteractive({ useHandCursor: true })
.on('pointerdown', () => this.scene.start('GameScene', { level: 1 }));
}
}HUDScene (Parallel Overlay)
HUDScene(并行覆盖层)
Runs simultaneously with GameScene via . Ideal for health bars, score, minimap.
this.scene.launch('HUDScene')typescript
// In GameScene.create():
this.scene.launch('HUDScene');
// Pass events to HUD
this.events.on('scoreChanged', (score: number) => {/* HUD listens */});
// HUDScene.ts:
export class HUDScene extends Phaser.Scene {
private scoreText!: Phaser.GameObjects.Text;
constructor() { super({ key: 'HUDScene' }); }
create(): void {
this.scoreText = this.add.text(16, 16, 'Score: 0', {
fontSize: '20px', color: '#ffffff',
}).setScrollFactor(0); // Fixed to camera
const gameScene = this.scene.get('GameScene');
gameScene.events.on('scoreChanged', (score: number) => {
this.scoreText.setText(`Score: ${score}`);
}, this);
// Clean up when GameScene stops
gameScene.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
gameScene.events.off('scoreChanged');
}, this);
}
}通过与GameScene同时运行。适用于生命值条、分数、小地图。
this.scene.launch('HUDScene')typescript
// 在GameScene.create()中:
this.scene.launch('HUDScene');
// 向HUD传递事件
this.events.on('scoreChanged', (score: number) => {/* HUD监听 */});
// HUDScene.ts:
export class HUDScene extends Phaser.Scene {
private scoreText!: Phaser.GameObjects.Text;
constructor() { super({ key: 'HUDScene' }); }
create(): void {
this.scoreText = this.add.text(16, 16, 'Score: 0', {
fontSize: '20px', color: '#ffffff',
}).setScrollFactor(0); // 固定到相机
const gameScene = this.scene.get('GameScene');
gameScene.events.on('scoreChanged', (score: number) => {
this.scoreText.setText(`Score: ${score}`);
}, this);
// GameScene停止时清理
gameScene.events.once(Phaser.Scenes.Events.SHUTDOWN, () => {
gameScene.events.off('scoreChanged');
}, this);
}
}PauseScene (Modal Overlay)
PauseScene(模态覆盖层)
Launched on top of GameScene, which is paused.
typescript
// In GameScene — pause on Escape:
const esc = this.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.ESC);
esc.on('down', () => {
this.scene.pause('GameScene');
this.scene.launch('PauseScene');
});
// PauseScene.ts:
export class PauseScene extends Phaser.Scene {
constructor() { super({ key: 'PauseScene' }); }
create(): void {
const { width, height } = this.scale;
this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.5);
this.add.text(width / 2, height / 2 - 50, 'PAUSED', {
fontSize: '48px', color: '#ffffff',
}).setOrigin(0.5);
this.add.text(width / 2, height / 2 + 40, 'Resume', {
fontSize: '28px', color: '#00ff88',
}).setOrigin(0.5).setInteractive({ useHandCursor: true })
.on('pointerdown', () => {
this.scene.resume('GameScene');
this.scene.stop('PauseScene');
});
}
}在GameScene之上启动,GameScene会被暂停。
typescript
// 在GameScene中——按ESC暂停:
const esc = this.input.keyboard!.addKey(Phaser.Input.Keyboard.KeyCodes.ESC);
esc.on('down', () => {
this.scene.pause('GameScene');
this.scene.launch('PauseScene');
});
// PauseScene.ts:
export class PauseScene extends Phaser.Scene {
constructor() { super({ key: 'PauseScene' }); }
create(): void {
const { width, height } = this.scale;
this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.5);
this.add.text(width / 2, height / 2 - 50, 'PAUSED', {
fontSize: '48px', color: '#ffffff',
}).setOrigin(0.5);
this.add.text(width / 2, height / 2 + 40, 'Resume', {
fontSize: '28px', color: '#00ff88',
}).setOrigin(0.5).setInteractive({ useHandCursor: true })
.on('pointerdown', () => {
this.scene.resume('GameScene');
this.scene.stop('PauseScene');
});
}
}Scene Transitions
场景切换
typescript
// Basic transition
this.scene.start('TargetScene');
// With data
this.scene.start('GameScene', { level: 2, score: 1500 });
// With fade
this.cameras.main.fadeOut(500, 0, 0, 0, () => {
this.scene.start('NextScene');
});
// Parallel launch (both scenes run simultaneously)
this.scene.launch('HUDScene');
// Pause/resume
this.scene.pause('GameScene');
this.scene.resume('GameScene');
// Stop a scene entirely
this.scene.stop('HUDScene');
// Restart current scene
this.scene.restart();
this.scene.restart({ level: 1, score: 0 }); // with fresh datatypescript
// 基础切换
this.scene.start('TargetScene');
// 携带数据
this.scene.start('GameScene', { level: 2, score: 1500 });
// 带淡入淡出效果
this.cameras.main.fadeOut(500, 0, 0, 0, () => {
this.scene.start('NextScene');
});
// 并行启动(两个场景同时运行)
this.scene.launch('HUDScene');
// 暂停/恢复
this.scene.pause('GameScene');
this.scene.resume('GameScene');
// 完全停止场景
this.scene.stop('HUDScene');
// 重启当前场景
this.scene.restart();
this.scene.restart({ level: 1, score: 0 }); // 携带新数据Cross-Scene Communication
跨场景通信
typescript
// 1. Registry (simple key-value, fires events on change)
this.registry.set('score', 0);
this.registry.get('score');
this.registry.events.on('changedata-score', (parent, value) => {});
// 2. Scene events (strongly typed messaging)
// Emitter scene:
this.events.emit('enemyKilled', { points: 100, x: enemy.x, y: enemy.y });
// Listener scene:
this.scene.get('GameScene').events.on('enemyKilled', (data: {points: number}) => {});
// 3. Direct scene reference (use sparingly — creates tight coupling)
const gameScene = this.scene.get('GameScene') as GameScene;
gameScene.addScore(100);typescript
// 1. Registry(简单键值对,数据变化时触发事件)
this.registry.set('score', 0);
this.registry.get('score');
this.registry.events.on('changedata-score', (parent, value) => {});
// 2. 场景事件(强类型消息传递)
// 发射场景:
this.events.emit('enemyKilled', { points: 100, x: enemy.x, y: enemy.y });
// 监听场景:
this.scene.get('GameScene').events.on('enemyKilled', (data: {points: number}) => {});
// 3. 直接场景引用(谨慎使用——会造成强耦合)
const gameScene = this.scene.get('GameScene') as GameScene;
gameScene.addScore(100);Defensive Scene Shutdown
防御性场景关闭
Scene teardown is the most common source of runtime crashes in Phaser 4. , , and camera-owned objects can be null during shutdown, especially in overlay scenes stopped while still mid-tween.
physicstweens场景销毁是Phaser 4中运行时崩溃最常见的原因。、和相机所属的对象在关闭期间可能为空,尤其是在仍处于补间过程中被停止的覆盖场景。
physicstweensTimer Tracking Pattern
计时器跟踪模式
Anonymous calls accumulate and never clean up on scene restart. Track every timer:
time.addEvent()typescript
export class GameScene extends Phaser.Scene {
private activeTimers: Phaser.Time.TimerEvent[] = [];
// Use this instead of this.time.addEvent() directly
private addTimer(cfg: Phaser.Types.Time.TimerEventConfig): Phaser.Time.TimerEvent {
const t = this.time.addEvent(cfg);
this.activeTimers.push(t);
return t;
}
shutdown(): void {
for (const t of this.activeTimers) t.remove(false);
this.activeTimers = [];
this.bgMusic?.stop();
}
}匿名的调用会累积,并且在场景重启时不会清理。跟踪每个计时器:
time.addEvent()typescript
export class GameScene extends Phaser.Scene {
private activeTimers: Phaser.Time.TimerEvent[] = [];
// 直接使用此方法代替this.time.addEvent()
private addTimer(cfg: Phaser.Types.Time.TimerEventConfig): Phaser.Time.TimerEvent {
const t = this.time.addEvent(cfg);
this.activeTimers.push(t);
return t;
}
shutdown(): void {
for (const t of this.activeTimers) t.remove(false);
this.activeTimers = [];
this.bgMusic?.stop();
}
}Overlay Scene Sizing
覆盖场景尺寸
Overlay scenes (PauseScene, dialog panels) must size their full-screen backdrops from , not from module-level constants. Constants are frozen at boot time; camera dimensions update with the live viewport. A backdrop sized from a constant will be wrong after any canvas resize.
this.cameras.main.width/heighttypescript
// BAD — constant freezes at boot-time dimensions:
const GAME_W = 800;
this.add.rectangle(GAME_W / 2, GAME_H / 2, GAME_W, GAME_H, 0x000000, 0.5);
// CORRECT — reads live dimensions:
const { width, height } = this.cameras.main;
this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.5);Also add a resize listener if the overlay can remain open while the window resizes:
typescript
this.scale.on('resize', (size: Phaser.Structs.Size) => {
this.backdrop.setPosition(size.width / 2, size.height / 2);
this.backdrop.setSize(size.width, size.height);
});覆盖场景(PauseScene、对话框面板)必须从设置其全屏背景的尺寸,不能使用模块级常量。常量在启动时就固定了;相机尺寸会随实时视口更新。使用常量设置尺寸的背景在画布调整大小后会显示错误。
this.cameras.main.width/heighttypescript
// 错误——常量固定为启动时的尺寸:
const GAME_W = 800;
this.add.rectangle(GAME_W / 2, GAME_H / 2, GAME_W, GAME_H, 0x000000, 0.5);
// 正确——读取实时尺寸:
const { width, height } = this.cameras.main;
this.add.rectangle(width / 2, height / 2, width, height, 0x000000, 0.5);如果覆盖层可能在窗口调整大小时保持打开状态,还要添加一个大小调整监听器:
typescript
this.scale.on('resize', (size: Phaser.Structs.Size) => {
this.backdrop.setPosition(size.width / 2, size.height / 2);
this.backdrop.setSize(size.width, size.height);
});Additional Resources
额外资源
Reference Files
参考文件
- — Detailed patterns for every scene type, advanced transitions, scene manager patterns
references/scene-patterns.md
- —— 每种场景类型的详细模式、高级切换、场景管理器模式
references/scene-patterns.md