Pixi.js 游戏开发教程 / 容器与场景图
容器与场景图
概述
场景图(Scene Graph)是 Pixi.js 组织显示对象的核心数据结构。它是一棵树,app.stage 是根节点,所有显示对象(Sprite、Graphics、Text 等)都是树中的节点。Container(容器)是场景图中的分支节点,可以包含子节点。
理解场景图对于构建复杂的游戏 UI、多层场景、以及优化渲染性能至关重要。
Container 基础
Container 是所有可显示对象的基类,本身不渲染任何内容,仅作为子节点的容器。
import { Application, Container, Sprite, Graphics, Assets } from 'pixi.js';
const app = new Application();
await app.init({ width: 800, height: 600, background: '#1a1a2e' });
document.body.appendChild(app.canvas);
// 创建容器
const gameWorld = new Container();
const uiLayer = new Container();
app.stage.addChild(gameWorld);
app.stage.addChild(uiLayer);
// 向容器中添加子节点
const heroTexture = await Assets.load('/image/hero.png');
const hero = new Sprite(heroTexture);
hero.position.set(400, 300);
gameWorld.addChild(hero);
// UI 元素添加到 UI 层
const hpBar = new Graphics();
hpBar.rect(0, 0, 200, 20);
hpBar.fill(0xe74c3c);
hpBar.position.set(20, 20);
uiLayer.addChild(hpBar);
addChild 与 removeChild
添加子节点
const parent = new Container();
const child = new Sprite(texture);
// 添加到末尾
parent.addChild(child);
// 添加到指定索引
parent.addChildAt(child, 0);
// 添加多个
parent.addChild(child1, child2, child3);
移除子节点
// 移除指定子节点
parent.removeChild(child);
// 移除指定索引的子节点
parent.removeChildAt(0);
// 移除指定范围的子节点
parent.removeChildren(0, 5); // 移除索引 0-4
// 移除所有子节点
parent.removeChildren();
检查与查找
// 检查是否包含某个子节点
if (parent.contains(child)) {
console.log('已包含');
}
// 获取子节点数量
console.log(parent.children.length);
// 通过索引获取子节点
const first = parent.children[0];
场景树结构
典型的游戏场景树:
app.stage (根容器)
├── background (Container) — 背景层
│ ├── sky (Sprite)
│ ├── mountains (Sprite)
│ └── ground (Sprite)
├── gameWorld (Container) — 游戏世界层
│ ├── enemies (Container) — 敌人容器
│ │ ├── enemy1 (Sprite)
│ │ └── enemy2 (Sprite)
│ ├── player (Sprite)
│ └── bullets (Container) — 子弹容器
│ ├── bullet1 (Sprite)
│ └── bullet2 (Sprite)
└── uiLayer (Container) — UI 层
├── scoreText (Text)
├── hpBar (Graphics)
└── minimap (Container)
💡 提示: 分层管理场景是游戏开发的最佳实践。UI 层不受游戏世界相机变换影响,便于独立管理。
// 分层管理示例
class GameScene {
stage: Container;
bgLayer: Container;
worldLayer: Container;
uiLayer: Container;
constructor() {
this.stage = new Container();
this.bgLayer = new Container();
this.worldLayer = new Container();
this.uiLayer = new Container();
// 渲染顺序:背景 → 世界 → UI
this.stage.addChild(this.bgLayer);
this.stage.addChild(this.worldLayer);
this.stage.addChild(this.uiLayer);
}
}
容器变换属性
容器继承自 Container,具有与 Sprite 相同的变换属性,且变换会传递给所有子节点。
position
const group = new Container();
group.position.set(200, 150);
const child = new Sprite(texture);
child.position.set(50, 50); // 相对于父容器
// 子节点在世界坐标中的实际位置:(250, 200)
console.log(child.getGlobalPosition()); // { x: 250, y: 200 }
scale
const group = new Container();
group.scale.set(2, 2);
const child = new Sprite(texture);
child.position.set(100, 100);
// 子节点的实际位置:(200, 200),实际大小:2倍
rotation
const pivot = new Container();
pivot.position.set(400, 300);
pivot.pivot.set(0, 0); // 旋转中心
const arm = new Sprite(texture);
arm.position.set(50, 0); // 距离旋转中心 50px
pivot.addChild(arm);
// 旋转容器 → 所有子节点围绕容器 pivot 旋转
app.ticker.add(() => {
pivot.rotation += 0.01;
});
pivot
const box = new Container();
box.position.set(400, 300);
// 设置 pivot 为容器中心
box.pivot.set(50, 50); // 假设容器内容为 100x100
// 旋转和缩放都围绕 pivot 点
box.rotation = Math.PI / 4;
局部坐标 vs 世界坐标
世界坐标(World): stage 的坐标系,原点在 canvas 左上角
局部坐标(Local): 相对于父容器的坐标,受父容器 position/scale/rotation 影响
// 设置世界坐标
const child = new Sprite(texture);
child.position.set(100, 200); // 这是局部坐标
// 获取世界坐标
const globalPos = child.getGlobalPosition();
console.log(globalPos.x, globalPos.y);
// 将世界坐标转换为某容器的局部坐标
const localPos = container.toLocal(globalPos);
console.log(localPos.x, localPos.y);
// 将局部坐标转换为世界坐标
const worldPos = container.toWorld(localPos);
坐标转换示例
const gameWorld = new Container();
gameWorld.position.set(100, 50);
gameWorld.scale.set(0.5);
app.stage.addChild(gameWorld);
const enemy = new Sprite(texture);
enemy.position.set(200, 300); // 在 gameWorld 的局部坐标
gameWorld.addChild(enemy);
// 获取 enemy 在屏幕上的真实位置
const screenPos = enemy.getGlobalPosition();
console.log(screenPos); // { x: 200, y: 200 } → 100+200*0.5, 50+300*0.5
// 鼠标点击位置转换到 gameWorld 局部坐标
app.stage.on('pointerdown', (e) => {
const worldLocal = gameWorld.toLocal(e.global);
console.log('点击位置(世界局部坐标):', worldLocal);
});
getBounds / getLocalBounds
// 获取容器在世界坐标系中的包围盒
const bounds = container.getBounds();
console.log(bounds.x, bounds.y, bounds.width, bounds.height);
// 获取容器在自身坐标系中的包围盒
const localBounds = container.getLocalBounds();
console.log(localBounds.x, localBounds.y, localBounds.width, localBounds.height);
⚠️ 注意: getBounds() 会遍历所有子节点计算,对大场景可能有性能开销。缓存结果而非每帧调用。
zIndex 排序
默认情况下,子节点按添加顺序渲染(后添加的在上层)。可通过 zIndex 改变排序。
const container = new Container();
container.sortableChildren = true; // 启用排序
const bg = new Sprite(bgTexture);
bg.zIndex = 0;
const player = new Sprite(playerTexture);
player.zIndex = 10;
const foreground = new Sprite(fgTexture);
foreground.zIndex = 20;
container.addChild(bg, player, foreground);
// 渲染顺序:bg → player → foreground
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
sortableChildren | boolean | false | 是否启用子节点排序 |
zIndex | number | 0 | 排序值(越大越在上层) |
容器销毁(destroy)
// 销毁容器及其所有子节点
container.destroy({
children: true, // 同时销毁子节点(默认 false)
texture: false, // 是否销毁纹理(默认 false)
baseTexture: false, // 是否销毁 BaseTexture(默认 false)
});
// 仅从父节点移除(不销毁)
parent.removeChild(container);
⚠️ 注意: 如果多个 Sprite 共享同一纹理,销毁 Sprite 时不要销毁纹理:
// ❌ 可能导致其他使用相同纹理的精灵出错
sprite.destroy({ texture: true });
// ✅ 安全销毁精灵,纹理留给其他精灵使用
sprite.destroy();
场景图遍历
// 递归遍历场景树
function traverse(node: Container, depth: number = 0) {
const indent = ' '.repeat(depth);
const type = node.constructor.name;
const pos = `(${Math.round(node.x)}, ${Math.round(node.y)})`;
console.log(`${indent}${type} ${pos}`);
for (const child of node.children) {
if (child instanceof Container) {
traverse(child, depth + 1);
}
}
}
traverse(app.stage);
输出示例:
Container (0, 0)
Container (0, 0)
Sprite (100, 200)
Sprite (300, 200)
Container (0, 0)
Text (20, 20)
游戏开发场景
场景:相机跟随系统
import { Application, Container, Sprite, Assets } from 'pixi.js';
const app = new Application();
await app.init({ width: 800, height: 600, background: '#2c3e50' });
document.body.appendChild(app.canvas);
// 世界容器(相机控制此容器的 position)
const world = new Container();
app.stage.addChild(world);
// 创建大量游戏对象
const texture = await Assets.load('/image/tile.png');
for (let x = 0; x < 20; x++) {
for (let y = 0; y < 20; y++) {
const tile = new Sprite(texture);
tile.x = x * 64;
tile.y = y * 64;
world.addChild(tile);
}
}
// 玩家
const player = new Sprite(await Assets.load('/image/hero.png'));
player.anchor.set(0.5);
player.position.set(640, 640);
world.addChild(player);
// 相机跟随
app.ticker.add(() => {
// 使玩家始终在屏幕中央
world.x = app.screen.width / 2 - player.x;
world.y = app.screen.height / 2 - player.y;
});
场景:HUD 叠加层
// HUD 不受世界变换影响
const hud = new Container();
app.stage.addChild(hud);
const scoreText = new Text({
text: 'Score: 0',
style: { fontSize: 24, fill: '#ffffff' },
});
scoreText.position.set(20, 20);
hud.addChild(scoreText);
// HUD 始终在最上层,不受世界相机移动影响
⚠️ 常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 子节点位置异常 | 父容器有变换 | 检查父容器的 position/scale/rotation |
| 移除后仍渲染 | 未正确从父节点移除 | 确认调用 removeChild() |
| 内存泄漏 | 容器未销毁或未从父节点移除 | 使用 destroy() 彻底释放 |
| zIndex 不生效 | 未启用 sortableChildren | 设置 container.sortableChildren = true |
| getBounds 返回空 | 容器没有可见子节点 | 确保有子节点且 visible = true |
💡 进阶提示
对象池复用 Container:
class ContainerPool<T extends Container> { private pool: T[] = []; private factory: () => T; constructor(factory: () => T) { this.factory = factory; } get(): T { const obj = this.pool.pop() || this.factory(); obj.visible = true; return obj; } release(obj: T) { obj.visible = false; obj.removeFromParent(); this.pool.push(obj); } }缓存容器为纹理(静态内容优化):
// 将复杂静态容器缓存为单张纹理,减少 draw call const bounds = complexContainer.getBounds(); const renderTexture = app.renderer.generateTexture(complexContainer); const cached = new Sprite(renderTexture); cached.position.set(bounds.x, bounds.y); complexContainer.visible = false; app.stage.addChild(cached);层级调试: 开发时打印容器层级辅助调试:
function printTree(node: Container, depth = 0) { console.log(`${' '.repeat(depth)}[${node.constructor.name}] children=${node.children.length}`); node.children.forEach(c => { if (c instanceof Container) printTree(c, depth + 1); }); } printTree(app.stage);
扩展阅读
上一章:03 - Sprite 与纹理 下一章:05 - 图形绘制(Graphics)