强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Pixi.js 游戏开发教程 / Pixi.js 性能优化

性能优化

让你的 Pixi.js 游戏流畅运行在各种设备上。


一、渲染原理

1.1 WebGL 渲染管线

Pixi.js 底层使用 WebGL 渲染,核心流程:

CPU 端准备顶点数据 → 提交 Draw Call → GPU 执行渲染 → 输出到 Framebuffer

每次 drawArraysdrawElements 调用就是一个 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 / ETC2Android(OpenGL ES)6:1
ASTC现代移动 GPU灵活
PVRTCiOS(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, tintmask, 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 + 共享纹理

扩展阅读