Pixi.js 游戏开发教程 / Pixi.js 场景管理与切换
17. 场景管理与切换
概述
随着游戏规模增长,将不同游戏状态(主菜单、游戏中、暂停、结算)分离为独立场景至关重要。本章实现一个完整的场景管理系统,包括生命周期、切换动画、场景栈和数据传递。
场景管理器设计
核心思路:每个场景是一个独立的 Container,场景管理器负责切换和更新。
// 场景接口
interface IScene extends Container {
onEnter?(params?: Record<string, unknown>): Promise<void> | void;
onExit?(): Promise<void> | void;
onPause?(): void;
onResume?(params?: Record<string, unknown>): void;
onUpdate?(dt: number): void;
onResize?(width: number, height: number): void;
}
场景生命周期
| 生命周期 | 触发时机 | 用途 |
|---|---|---|
onEnter | 场景进入时 | 初始化资源、绑定事件 |
onExit | 场景离开时 | 释放资源、解绑事件 |
onPause | 新场景压栈时暂停 | 暂停游戏逻辑、动画 |
onResume | 栈顶场景弹出后恢复 | 恢复游戏逻辑 |
onUpdate | 每帧更新 | 游戏逻辑、动画 |
onResize | 窗口大小变化 | 布局自适应 |
import { Container, Application } from 'pixi.js';
abstract class Scene extends Container implements IScene {
protected app: Application;
constructor(app: Application) {
super();
this.app = app;
}
async onEnter(_params?: Record<string, unknown>): Promise<void> {}
async onExit(): Promise<void> {}
onPause(): void { this.visible = false; }
onResume(_params?: Record<string, unknown>): void { this.visible = true; }
onUpdate(_dt: number): void {}
onResize(_width: number, _height: number): void {}
}
完整 SceneManager 类实现
import { Container, Application, Graphics } from 'pixi.js';
interface SceneConstructor {
new (app: Application): IScene;
}
class SceneManager {
private app: Application;
private scenes: Map<string, SceneConstructor> = new Map();
private sceneStack: IScene[] = [];
private transitionContainer: Container;
private isTransitioning = false;
constructor(app: Application) {
this.app = app;
this.transitionContainer = new Container();
this.app.stage.addChild(this.transitionContainer);
// 全局更新循环
app.ticker.add((ticker) => {
const dt = ticker.deltaMS * 0.001;
const current = this.currentScene;
if (current && 'onUpdate' in current) {
current.onUpdate(dt);
}
});
// 窗口大小变化
app.renderer.on('resize', (width: number, height: number) => {
for (const scene of this.sceneStack) {
scene.onResize?.(width, height);
}
});
}
// 注册场景类
register(name: string, sceneClass: SceneConstructor) {
this.scenes.set(name, sceneClass);
}
// 获取当前场景
get currentScene(): IScene | null {
return this.sceneStack.length > 0
? this.sceneStack[this.sceneStack.length - 1]
: null;
}
// 切换到新场景(替换栈顶)
async switchTo(name: string, params?: Record<string, unknown>, transition?: string) {
if (this.isTransitioning) return;
this.isTransitioning = true;
try {
// 淡出当前场景
const current = this.currentScene;
if (current) {
await this.playTransition(transition || 'fadeOut', 'out');
await current.onExit();
this.app.stage.removeChild(current);
this.sceneStack.pop();
}
// 创建并进入新场景
const SceneClass = this.scenes.get(name);
if (!SceneClass) throw new Error(`Scene "${name}" not registered`);
const newScene = new SceneClass(this.app);
this.sceneStack.push(newScene);
this.app.stage.addChildAt(newScene, 0);
await newScene.onEnter(params);
// 淡入新场景
await this.playTransition(transition || 'fadeIn', 'in');
} finally {
this.isTransitioning = false;
}
}
// 压入场景(当前场景暂停)
async pushScene(name: string, params?: Record<string, unknown>) {
if (this.isTransitioning) return;
this.isTransitioning = true;
try {
const current = this.currentScene;
if (current) current.onPause();
const SceneClass = this.scenes.get(name);
if (!SceneClass) throw new Error(`Scene "${name}" not registered`);
const newScene = new SceneClass(this.app);
this.sceneStack.push(newScene);
this.app.stage.addChildAt(newScene, 0);
await newScene.onEnter(params);
} finally {
this.isTransitioning = false;
}
}
// 弹出栈顶场景
async popScene(params?: Record<string, unknown>) {
if (this.isTransitioning || this.sceneStack.length <= 1) return;
this.isTransitioning = true;
try {
const current = this.currentScene;
if (current) {
await current.onExit();
this.app.stage.removeChild(current);
this.sceneStack.pop();
}
const resumed = this.currentScene;
if (resumed) resumed.onResume(params);
} finally {
this.isTransitioning = false;
}
}
// 场景切换动画
private async playTransition(type: string, direction: 'in' | 'out'): Promise<void> {
return new Promise((resolve) => {
const overlay = new Graphics();
const { width, height } = this.app.screen;
overlay.rect(0, 0, width, height);
overlay.fill(0x000000);
this.transitionContainer.addChild(overlay);
const start = performance.now();
const duration = 300;
const tick = () => {
const elapsed = performance.now() - start;
const t = Math.min(1, elapsed / duration);
switch (type) {
case 'fadeIn':
case 'fadeOut':
overlay.alpha = direction === 'out' ? t : 1 - t;
break;
case 'slideLeft':
overlay.x = direction === 'out'
? -width * (1 - t)
: -width * t;
break;
case 'zoom':
const scale = direction === 'out' ? 1 + t * 0.5 : 1.5 - t * 0.5;
this.app.stage.scale.set(scale);
overlay.alpha = direction === 'out' ? t : 1 - t;
break;
}
if (t < 1) {
requestAnimationFrame(tick);
} else {
this.transitionContainer.removeChild(overlay);
overlay.destroy();
this.app.stage.scale.set(1);
resolve();
}
};
tick();
});
}
}
场景切换动画
淡入淡出(最常用)
// 使用 SceneManager 内置的 fadeIn/fadeOut
await sceneManager.switchTo('gameplay', { level: 1 }, 'fadeIn');
滑动切换
class SlideTransition {
static async play(app: Application, direction: 'left' | 'right' | 'up' | 'down') {
const overlay = new Graphics();
const { width, height } = app.screen;
return new Promise<void>((resolve) => {
const start = performance.now();
const duration = 400;
const dx = direction === 'left' ? -1 : direction === 'right' ? 1 : 0;
const dy = direction === 'up' ? -1 : direction === 'down' ? 1 : 0;
overlay.rect(0, 0, width, height);
overlay.fill(0x000000);
app.stage.addChild(overlay);
const tick = () => {
const t = Math.min(1, (performance.now() - start) / duration);
const eased = 1 - Math.pow(1 - t, 3); // easeOutCubic
overlay.x = dx * width * (1 - eased);
overlay.y = dy * height * (1 - eased);
overlay.alpha = t < 0.5 ? t * 2 : (1 - t) * 2;
if (t < 1) requestAnimationFrame(tick);
else {
app.stage.removeChild(overlay);
overlay.destroy();
resolve();
}
};
tick();
});
}
}
场景栈(推入/弹出)
场景栈适合管理覆盖层场景(如暂停菜单、设置面板):
// 游戏进行中 → 暂停菜单压栈 → 游戏暂停
await sceneManager.pushScene('pauseMenu');
// 暂停菜单弹出 → 游戏恢复
await sceneManager.popScene({ resumed: true });
// 场景栈示例:
// [mainMenu] → switchTo → [gameplay]
// [gameplay] → pushScene → [gameplay, pauseMenu]
// [gameplay, pauseMenu] → popScene → [gameplay]
// [gameplay] → pushScene → [gameplay, settings]
// [gameplay, settings] → pushScene → [gameplay, settings, keyBindings]
场景间数据传递
// 方式一:通过 switchTo/pushScene 的 params 参数
await sceneManager.switchTo('gameplay', {
level: 3,
difficulty: 'hard',
playerData: { hp: 100, score: 5000 },
});
// 在场景中接收
class GameplayScene extends Scene {
async onEnter(params?: Record<string, unknown>) {
if (params) {
this.level = params.level as number;
this.difficulty = params.difficulty as string;
}
}
}
// 方式二:通过全局数据总线
class GameDataBus {
private data: Map<string, unknown> = new Map();
set(key: string, value: unknown) {
this.data.set(key, value);
}
get<T>(key: string): T | undefined {
return this.data.get(key) as T;
}
clear() {
this.data.clear();
}
}
// 方式三:通过 popScene 回传数据
await sceneManager.popScene({ selectedOption: 'continue' });
class MainMenuScene extends Scene {
onResume(params?: Record<string, unknown>) {
if (params?.selectedOption === 'continue') {
this.continueGame();
}
}
}
全局管理器
某些系统需要跨场景持续运行(音频、输入、网络):
class GameManager {
public sceneManager: SceneManager;
public audio: AudioManager;
public input: InputManager;
public network: NetworkManager;
public data: GameDataBus;
constructor(app: Application) {
this.sceneManager = new SceneManager(app);
this.audio = new AudioManager();
this.input = new InputManager(app.canvas);
this.network = new NetworkManager();
this.data = new GameDataBus();
// 注册场景
this.sceneManager.register('mainMenu', MainMenuScene);
this.sceneManager.register('gameplay', GameplayScene);
this.sceneManager.register('pauseMenu', PauseMenuScene);
this.sceneManager.register('gameOver', GameOverScene);
}
async start() {
await this.sceneManager.switchTo('mainMenu');
}
}
状态机模式
场景可以内部使用状态机管理子状态:
type GameState = 'playing' | 'paused' | 'frozen' | 'transitioning';
class GameplayScene extends Scene {
private state: GameState = 'playing';
onUpdate(dt: number) {
switch (this.state) {
case 'playing':
this.updateGameplay(dt);
break;
case 'paused':
// 不更新游戏逻辑
break;
case 'frozen':
// 完全停止
break;
case 'transitioning':
this.updateTransition(dt);
break;
}
}
private updateGameplay(dt: number) {
// 玩家输入、物理、AI、渲染
}
private updateTransition(dt: number) {
// 关卡过渡动画
}
onPause() {
this.state = 'paused';
}
onResume() {
this.state = 'playing';
}
}
场景预加载
在切换场景前预加载资源,避免切换时出现加载画面:
class PreloadScene extends Scene {
async onEnter() {
const progress = this.createProgressBar();
this.addChild(progress);
const assets = [
'/image/hero.png',
'/image/enemy.png',
'/image/tileset.png',
'/image/ui.png',
];
let loaded = 0;
for (const asset of assets) {
await Assets.load(asset);
loaded++;
progress.update(loaded / assets.length);
}
// 加载完成,切换到主菜单
await this.app.sceneManager.switchTo('mainMenu');
}
private createProgressBar() {
const bar = new Container();
const bg = new Graphics();
bg.rect(0, 0, 300, 20);
bg.fill(0x333333);
bar.addChild(bg);
const fill = new Graphics();
bar.addChild(fill);
bar.x = (this.app.screen.width - 300) / 2;
bar.y = (this.app.screen.height - 20) / 2;
bar.update = (progress: number) => {
fill.clear();
fill.rect(0, 0, 300 * progress, 20);
fill.fill(0x00cc00);
};
return bar;
}
}
💡 提示:对于大型游戏,建议在预加载场景中加载核心资源,其他资源在游戏运行中异步加载(如进入新关卡时加载该关卡资源)。
完整使用示例
import { Application } from 'pixi.js';
const app = new Application();
await app.init({ width: 800, height: 600 });
document.body.appendChild(app.canvas);
const game = new GameManager(app);
// 添加场景
game.sceneManager.register('preload', PreloadScene);
game.sceneManager.register('mainMenu', MainMenuScene);
game.sceneManager.register('gameplay', GameplayScene);
game.sceneManager.register('pauseMenu', PauseMenuScene);
// 启动
await game.sceneManager.switchTo('preload');