Pixi.js 游戏开发教程 / Pixi.js 性能优化
性能优化
让你的 Pixi.js 游戏流畅运行在各种设备上。
一、渲染原理
1.1 WebGL 渲染管线
Pixi.js 底层使用 WebGL 渲染,核心流程:
CPU 端准备顶点数据 → 提交 Draw Call → GPU 执行渲染 → 输出到 Framebuffer
每次 drawArrays 或 drawElements 调用就是一个 Draw Call,它是 CPU 与 GPU 之间的通信开销。
1.2 批处理(Batching)
Pixi.js 的 BatchRenderer 自动将使用相同纹理的 Sprite 合并到一次 Draw Call 中:
| 条件 | 结果 |
|---|---|
| 同一纹理、连续渲染 | 合并为 1 次 Draw Call |
| 不同纹理 | 切换纹理 → 新的 Draw Call |
| 混合模式不同 | 强制断批 |
| 父容器有 mask | 强制断批 |
⚠️ 注意:尽量让相同纹理的 Sprite 在场景树中连续排列,避免与其他纹理交错。
二、Draw Call 优化
2.1 减少 Draw Call 的关键策略
import { Application, Sprite, Texture } from 'pixi.js';
const app = new Application({ width: 800, height: 600 });
document.body.appendChild(app.canvas);
// ✅ 好的做法:多个 Sprite 共享同一纹理
const texture = Texture.from('/image/bullet.png');
for (let i = 0; i < 100; i++) {
const bullet = new Sprite(texture);
bullet.x = Math.random() * 800;
bullet.y = Math.random() * 600;
app.stage.addChild(bullet);
}
// ❌ 坏的做法:每个 Sprite 使用不同纹理
// 100 个不同纹理 = 100 次 Draw Call
2.2 查看 Draw Call 数量
// 在渲染循环中查看
app.ticker.add(() => {
// renderer.render 之后可以查看
console.log('Draw Calls:', (app.renderer as any).renderingToScreen
? (app.renderer as any).batcher?.size ?? 'N/A'
: 'N/A');
});
💡 提示:Chrome 浏览器的 Spector.js 扩展可以精确查看 WebGL 调用。
三、纹理图集(Texture Atlas)
3.1 什么是纹理图集
将多个小图合并到一张大图上,所有 Sprite 共享同一纹理,大幅减少纹理切换。
┌─────────────────────────┐
│ player_idle │ player_run │
│───────────────│────────────│
│ enemy_1 │ enemy_2 │
│───────────────│────────────│
│ bullet │ explosion │
└─────────────────────────┘
Texture Atlas (spritesheet.png)
3.2 使用 Spritesheet
import { Assets, Spritesheet } from 'pixi.js';
// 加载精灵表(配合 TexturePacker 导出的 JSON)
const spritesheetData = await fetch('/image/game.json').then(r => r.json());
const texture = await Assets.load('/image/game.png');
const spritesheet = new Spritesheet(texture, spritesheetData);
await spritesheet.parse();
// 使用纹理
const player = new Sprite(spritesheet.textures['player_idle']);
const enemy = new Sprite(spritesheet.textures['enemy_1']);
app.stage.addChild(player, enemy);
3.3 工具推荐
| 工具 | 说明 |
|---|---|
| TexturePacker | 商业工具,支持算法优化 |
| Free Texture Packer | 开源免费 |
| CodeAndWeb | 在线生成 |
四、对象池模式(Object Pool)
4.1 为什么需要对象池
频繁创建和销毁对象会造成 GC(垃圾回收)压力,导致帧率波动。
4.2 实现对象池
class ObjectPool<T> {
private pool: T[] = [];
private factory: () => T;
private reset: (obj: T) => void;
constructor(factory: () => T, reset: (obj: T) => void) {
this.factory = factory;
this.reset = reset;
}
acquire(): T {
if (this.pool.length > 0) {
return this.pool.pop()!;
}
return this.factory();
}
release(obj: T): void {
this.reset(obj);
this.pool.push(obj);
}
get size(): number {
return this.pool.length;
}
}
// 使用示例:子弹对象池
import { Sprite, Texture, Container } from 'pixi.js';
class Bullet extends Sprite {
public vx = 0;
public vy = 0;
public active = false;
constructor() {
super(Texture.from('/image/bullet.png'));
this.anchor.set(0.5);
}
fire(x: number, y: number, vx: number, vy: number) {
this.x = x;
this.y = y;
this.vx = vx;
this.vy = vy;
this.active = true;
this.visible = true;
}
deactivate() {
this.active = false;
this.visible = false;
}
}
const bulletPool = new ObjectPool(
() => new Bullet(),
(b) => { b.deactivate(); }
);
// 发射子弹
function fireBullet(container: Container, x: number, y: number) {
const bullet = bulletPool.acquire();
bullet.fire(x, y, 0, -8);
container.addChild(bullet);
}
// 回收子弹
function recycleBullet(bullet: Bullet, container: Container) {
container.removeChild(bullet);
bulletPool.release(bullet);
}
五、可见性剔除(Culling)
5.1 视口剔除
不在屏幕内的对象不需要渲染:
import { Container, Rectangle } from 'pixi.js';
function cullChildren(container: Container, viewRect: Rectangle) {
for (const child of container.children) {
const bounds = child.getBounds();
// 检查是否与视口相交
const visible = bounds.x + bounds.width > viewRect.x
&& bounds.x < viewRect.x + viewRect.width
&& bounds.y + bounds.height > viewRect.y
&& bounds.y < viewRect.y + viewRect.height;
child.visible = visible;
}
}
// 每帧更新
const viewRect = new Rectangle(0, 0, 800, 600);
app.ticker.add(() => {
viewRect.x = -app.stage.x;
viewRect.y = -app.stage.y;
cullChildren(bulletContainer, viewRect);
});
5.2 分区空间索引
对于大量对象(> 1000),使用四叉树(Quadtree)加速碰撞检测和剔除:
// 简单四叉树概念示意
interface QuadTreeNode {
bounds: Rectangle;
objects: GameObject[];
children: QuadTreeNode[]; // 4 个子节点
}
六、Graphics 优化
6.1 缓存为纹理
Graphics 每次修改都会重新生成几何数据。对静态或变化不频繁的 Graphics,缓存为纹理:
import { Graphics, RenderTexture, Sprite } from 'pixi.js';
const g = new Graphics();
g.rect(0, 0, 64, 64).fill(0xff0000);
g.circle(32, 32, 20).fill(0x00ff00);
// 缓存为 RenderTexture
const rt = RenderTexture.create({ width: 64, height: 64 });
app.renderer.render({ container: g, target: rt });
// 用 Sprite 代替 Graphics(1 次 Draw Call)
const cached = new Sprite(rt);
cached.x = 100;
cached.y = 100;
app.stage.addChild(cached);
// 原始 Graphics 可以销毁
g.destroy();
6.2 减少重建频率
// ❌ 每帧重建
app.ticker.add(() => {
graphics.clear();
graphics.rect(0, 0, player.hp, 20).fill(0x00ff00);
});
// ✅ 仅在值变化时重建
let lastHp = -1;
app.ticker.add(() => {
if (player.hp !== lastHp) {
graphics.clear();
graphics.rect(0, 0, player.hp, 20).fill(0x00ff00);
lastHp = player.hp;
}
});
七、内存管理
7.1 纹理释放
// 销毁纹理释放 GPU 内存
texture.destroy(true); // true = 同时销毁 BaseTexture
// 批量销毁
function destroyAllTextures(container: Container) {
container.children.forEach(child => {
if (child instanceof Sprite && child.texture) {
child.texture.destroy(true);
}
if (child instanceof Container) {
destroyAllTextures(child);
}
});
container.removeChildren();
}
7.2 纹理压缩
GPU 压缩纹理格式可以大幅减少内存占用和加载时间:
| 格式 | 适用平台 | 压缩率 |
|---|---|---|
| ETC1 / ETC2 | Android(OpenGL ES) | 6:1 |
| ASTC | 现代移动 GPU | 灵活 |
| PVRTC | iOS(PowerVR) | 8:1 |
| Basis Universal | 跨平台(转码) | 高 |
⚠️ 注意:纹理压缩需要硬件支持,务必做好格式回退。
八、粒子性能优化
8.1 使用 ParticleContainer
import { ParticleContainer, Sprite, Texture } from 'pixi.js';
// ParticleContainer 比普通 Container 快很多
const particles = new ParticleContainer(10000, {
vertices: true,
position: true,
rotation: true,
uvs: true,
tint: true,
});
const texture = Texture.from('/image/particle.png');
for (let i = 0; i < 5000; i++) {
const p = new Sprite(texture);
p.x = Math.random() * 800;
p.y = Math.random() * 600;
p.anchor.set(0.5);
particles.addChild(p);
}
app.stage.addChild(particles);
8.2 ParticleContainer 限制
| 支持 | 不支持 |
|---|---|
| position, scale, rotation, alpha, tint | mask, filter, children 嵌套 |
| 最大性能 | 功能有限 |
💡 提示:如果不需要特殊效果,优先使用 ParticleContainer。
九、性能分析工具
9.1 PixiJS DevTools
浏览器扩展,可以查看场景树、纹理使用、Draw Call 统计。
9.2 Chrome Performance 面板
1. 打开 Chrome DevTools → Performance 标签
2. 点击 Record 录制游戏过程
3. 分析 Main Thread、Rendering、GPU 活动
4. 关注 Long Task(> 50ms 的任务)
9.3 帧率监控
// 简单 FPS 显示
import { Text } from 'pixi.js';
const fpsText = new Text({ text: 'FPS: 0', style: { fill: 0xffffff } });
app.stage.addChild(fpsText);
app.ticker.add(() => {
fpsText.text = `FPS: ${Math.round(app.ticker.FPS)}`;
});
十、移动端优化策略
| 策略 | 说明 |
|---|---|
| 降低分辨率 | devicePixelRatio 限制为 1 或 2 |
| 限制粒子数量 | 移动端粒子数减半 |
| 减少滤镜 | BlurFilter、ColorMatrixFilter 非常消耗 |
| 使用低精度浮点 | WebGL precision mediump |
| 避免频繁 GC | 使用对象池、避免每帧创建对象 |
| 限制帧率 | 30 FPS 可接受时用 ticker.maxFPS = 30 |
// 移动端检测并降低粒子数
const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent);
const MAX_PARTICLES = isMobile ? 2000 : 5000;
十一、游戏开发场景总结
| 场景 | 优化方案 |
|---|---|
| 大量子弹 | 对象池 + ParticleContainer |
| 地图滚动 | 视口剔除 + 分块加载 |
| UI 界面 | 静态 Graphics 缓存为纹理 |
| 特效粒子 | ParticleContainer + 限制数量 |
| 角色动画 | Spritesheet + 共享纹理 |