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

Pixi.js 游戏开发教程 / Pixi.js 容器嵌套与变换

11. 容器嵌套与变换

概述

容器(Container)是 Pixi.js 场景图的核心。所有显示对象都是容器的子节点,容器的变换会向下传递给子节点。理解容器嵌套和变换系统是构建复杂游戏场景的基础。

Container 基础嵌套

容器可以无限嵌套,子节点继承父节点的变换(位置、旋转、缩放):

import { Container, Sprite, Texture } from 'pixi.js';

const app = new Application();
await app.init({ width: 800, height: 600 });

// 创建父容器
const world = new Container();
app.stage.addChild(world);

// 创建子容器
const playerGroup = new Container();
playerGroup.x = 200;
playerGroup.y = 150;
world.addChild(playerGroup);

// 添加精灵到子容器
const body = Sprite.from('/image/hero.png');
body.x = -16; // 相对于 playerGroup 的局部坐标
body.y = -16;
playerGroup.addChild(body);

const weapon = Sprite.from('/image/sword.png');
weapon.x = 16;
weapon.y = 0;
playerGroup.addChild(weapon);

// 移动父容器,所有子节点一起移动
playerGroup.x = 400; // body 和 weapon 都会移动

嵌套最佳实践

原则说明
层级清晰使用语义化命名:worldLayeruiLayereffectLayer
避免过深层级过深影响变换计算性能,建议不超过 8 层
批量操作移动一组对象时用容器,而非逐个移动
按需渲染不可见的容器设 visible = false 跳过渲染
// 推荐的层级结构
const stage = new Container();
const backgroundLayer = new Container(); // 背景层
const gameLayer = new Container();       // 游戏层(地图、角色、特效)
const uiLayer = new Container();         // UI 层(不受相机影响)
const overlayLayer = new Container();    // 覆盖层(过渡动画、对话框)

stage.addChild(backgroundLayer, gameLayer, uiLayer, overlayLayer);

局部变换 vs 世界变换

每个显示对象有两套变换:

  • 局部变换(Local Transform):相对于父容器的位置/旋转/缩放
  • 世界变换(World Transform):相对于场景根节点的最终变换
const parent = new Container();
parent.x = 100;
parent.y = 50;
parent.rotation = Math.PI / 6; // 30 度
app.stage.addChild(parent);

const child = new Container();
child.x = 50;
child.y = 0;
parent.addChild(child);

// 获取世界坐标
child.updateWorldTransform();
const worldPos = child.position; // 局部坐标 (50, 0)
const globalMatrix = child.worldTransform;
console.log('世界 X:', globalMatrix.tx, '世界 Y:', globalMatrix.ty);

💡 提示:Pixi.js v8 中可以使用 toGlobal()toLocal() 方法进行坐标转换,无需手动计算矩阵。

toLocal / toGlobal 坐标转换

这两个方法是游戏开发中最常用的坐标转换工具:

import { Container, Graphics, Application } from 'pixi.js';

const app = new Application();
await app.init({ width: 800, height: 600 });

const world = new Container();
world.x = 200;
world.y = 100;
world.scale.set(2);
app.stage.addChild(world);

const entity = new Container();
entity.x = 50;
entity.y = 80;
world.addChild(entity);

// toGlobal: 局部坐标 → 屏幕坐标
const screenPos = entity.toGlobal({ x: 0, y: 0 });
console.log('屏幕坐标:', screenPos.x, screenPos.y);
// 结果: (300, 260) → (200 + 50*2, 100 + 80*2)

// toGlobal: 也可以转换非原点的坐标
const screenPos2 = entity.toGlobal({ x: 10, y: 10 });
console.log('偏移屏幕坐标:', screenPos2.x, screenPos2.y);
// 结果: (320, 280) → (200 + (50+10)*2, 100 + (80+10)*2)

// toLocal: 屏幕坐标 → 局部坐标
const mouseGlobal = { x: 400, y: 300 };
const localPos = entity.toLocal(mouseGlobal);
console.log('局部坐标:', localPos.x, localPos.y);

游戏场景:鼠标点击世界坐标

// 将鼠标点击转换为世界坐标(用于 RTS 游戏选择单位)
app.stage.eventMode = 'static';
app.stage.hitArea = app.screen;

app.stage.on('pointerdown', (event) => {
    const worldPoint = world.toLocal(event.global);
    console.log('世界坐标:', worldPoint.x, worldPoint.y);
    // 在世界坐标处检测是否有单位
    selectUnitAt(worldPoint.x, worldPoint.y);
});

Pivot vs Anchor

pivotanchor 经常被混淆,它们的区别如下:

属性适用对象作用值类型
pivotContainer变换的旋转/缩放中心点(像素坐标){x, y} 像素
anchorSprite精灵纹理的对齐锚点(比例值 0~1){x, y} 0-1
import { Sprite, Container, Graphics } from 'pixi.js';

// --- Pivot: 容器的旋转中心 ---
const container = new Container();
container.x = 400;
container.y = 300;
container.pivot.set(50, 50); // 旋转中心偏移 (50, 50)
container.rotation = Math.PI / 4;
// 容器会围绕 (400-50, 300-50) = (350, 250) 旋转

// --- Anchor: 精灵纹理的锚点 ---
const sprite = Sprite.from('/image/character.png');
sprite.x = 400;
sprite.y = 300;
sprite.anchor.set(0.5, 0.5); // 锚点居中
sprite.rotation = Math.PI / 4;
// 精灵会围绕自身中心旋转

⚠️ 注意anchor 只影响纹理的绘制位置,不影响子节点。pivot 影响整个容器及其所有子节点的变换中心。

Skew 倾斜变换

Skew 可以创建倾斜效果(如透视变形):

const skewedSprite = Sprite.from('/image/wall.png');
skewedSprite.x = 400;
skewedSprite.y = 300;
skewedSprite.anchor.set(0.5, 0.5);

// 水平倾斜(伪 3D 地板效果)
skewedSprite.skew.x = Math.PI / 6; // 30 度倾斜

// 垂直倾斜
skewedSprite.skew.y = Math.PI / 8;

app.stage.addChild(skewedSprite);

游戏场景:伪 3D 地面效果

// 创建伪 3D 地面瓦片
function createIsoTile(texture, isoX, isoY) {
    const tile = Sprite.from(texture);
    tile.anchor.set(0.5, 0.5);
    tile.skew.x = Math.PI / 4;
    tile.skew.y = -Math.PI / 4;
    tile.x = (isoX - isoY) * 64; // 等轴测 X
    tile.y = (isoX + isoY) * 32; // 等轴测 Y
    return tile;
}

变换矩阵 Matrix 详解

Pixi.js 使用 3x2 仿射矩阵表示 2D 变换:

| a  b  tx |
| c  d  ty |
| 0  0  1  |
分量对应变换
aX 轴缩放 (scaleX × cos)
bX 轴倾斜 (scaleX × sin)
cY 轴倾斜 (scaleY × -sin)
dY 轴缩放 (scaleY × cos)
txX 轴平移
tyY 轴平移
import { Matrix } from 'pixi.js';

// 手动创建变换矩阵
const matrix = new Matrix();

// 平移
matrix.translate(100, 50);

// 旋转 45 度
matrix.rotate(Math.PI / 4);

// 缩放
matrix.scale(2, 2);

// 追加变换(在右侧乘)
matrix.append(new Matrix().translate(50, 0));

// 前置变换(在左侧乘)
matrix.prepend(new Matrix().rotate(Math.PI / 6));

// 逆矩阵
const inverted = matrix.clone().invert();

// 点变换
const point = { x: 10, y: 20 };
const transformed = matrix.apply(point);
const reversed = inverted.apply(transformed);
// reversed ≈ point

变换脏标记与性能

Pixi.js 使用脏标记(dirty flag)系统避免不必要的矩阵重新计算:

// Pixi.js 内部机制:
// 1. 修改 position/rotation/scale → 标记 transform 为 dirty
// 2. 渲染时检测到 dirty → 重新计算 worldTransform
// 3. 计算完成后清除 dirty 标记

// 不要手动调用 updateTransform(),除非你需要立即获取世界坐标
const sprite = Sprite.from('/image/bullet.png');
sprite.x = 100;
sprite.y = 200;
// 无需手动更新,Pixi.js 渲染时自动处理

// 如果需要在渲染前获取准确的世界坐标:
sprite.updateWorldTransform();
const worldPos = sprite.toGlobal({ x: 0, y: 0 });

💡 提示:在 ticker.update 回调中频繁修改大量对象时,批量修改后再让系统统一更新,避免频繁调用 updateWorldTransform()

容器裁剪 Mask(遮罩)

Mask 可以限制容器的可见区域:

import { Graphics, Container, Sprite } from 'pixi.js';

// 图形遮罩(最常用)
const maskGraphics = new Graphics();
maskGraphics.rect(100, 100, 300, 200);
maskGraphics.fill(0xffffff); // 颜色无意义,只看形状

const content = new Container();
content.mask = maskGraphics;
app.stage.addChild(content, maskGraphics);

// 添加内容到被遮罩的容器
for (let i = 0; i < 20; i++) {
    const item = Sprite.from('/image/item.png');
    item.x = Math.random() * 600;
    item.y = Math.random() * 400;
    content.addChild(item);
}

圆形遮罩(视野系统)

// Roguelike 游戏的视野遮罩
const fogOfWar = new Graphics();
fogOfWar.circle(0, 0, 120);
fogOfWar.fill(0xffffff);

const gameWorld = new Container();
gameWorld.mask = fogOfWar;
app.stage.addChild(gameWorld);

// 跟随玩家移动遮罩
app.ticker.add(() => {
    fogOfWar.x = player.x;
    fogOfWar.y = player.y;
});

Sprite 作为遮罩

// 使用图片作为遮罩(渐变边缘效果)
const maskSprite = Sprite.from('/image/mask-circle.png');
maskSprite.anchor.set(0.5);
const revealContent = new Container();
revealContent.mask = maskSprite;

⚠️ 注意:Sprite 遮罩比 Graphics 遮罩性能更好,但 Graphics 遮罩更灵活。WebGL 模式下两者都使用 stencil buffer,WebGPU 下使用 clip。

混合模式 blendMode

混合模式控制像素如何与下方像素合成:

import { BLEND_MODES } from 'pixi.js';

const glowEffect = Sprite.from('/image/glow.png');
glowEffect.blendMode = 'add'; // 叠加混合(发光效果)
gameLayer.addChild(glowEffect);

const shadowEffect = Sprite.from('/image/shadow.png');
shadowEffect.blendMode = 'multiply'; // 正片叠底(阴影效果)

const screenBlend = Sprite.from('/image/light.png');
screenBlend.blendMode = 'screen'; // 滤色(光效)

常用混合模式对照:

混合模式效果游戏用途
normal正常绘制默认模式
add叠加变亮火焰、激光、光晕
multiply正片叠底变暗阴影、环境遮蔽
screen滤色变亮光照、闪电
erase擦除纹理擦除效果

容器缓存 cacheAsBitmap

将容器渲染为纹理缓存,避免每帧重绘复杂容器:

const complexUI = new Container();

// 添加大量子元素...
for (let i = 0; i < 100; i++) {
    const icon = Sprite.from(`/image/icon-${i}.png`);
    icon.x = (i % 10) * 40;
    icon.y = Math.floor(i / 10) * 40;
    complexUI.addChild(icon);
}

// 缓存为位图(不再每帧重绘 100 个子元素)
complexUI.cacheAsBitmap = true;

// 当内容变化时,需要更新缓存
function updateUI() {
    complexUI.cacheAsBitmap = false; // 关闭缓存
    // ... 修改子元素 ...
    complexUI.cacheAsBitmap = true;  // 重新开启缓存
}

⚠️ 注意cacheAsBitmap 会创建一张额外的纹理,占用显存。对于频繁变化的容器不适用,仅适合静态或低频变化的复杂 UI。

综合实战:游戏 HUD 系统

import { Application, Container, Graphics, Text, Sprite } from 'pixi.js';

const app = new Application();
await app.init({ width: 800, height: 600 });

// 游戏世界容器(受相机影响)
const world = new Container();
app.stage.addChild(world);

// UI 层(不受相机影响)
const hud = new Container();
app.stage.addChild(hud);

// 血条组件
const healthBar = createHealthBar();
healthBar.x = 10;
healthBar.y = 10;
hud.addChild(healthBar);

function createHealthBar() {
    const bar = new Container();

    const bg = new Graphics();
    bg.roundRect(0, 0, 200, 24, 4);
    bg.fill(0x333333);
    bar.addChild(bg);

    const fill = new Graphics();
    fill.roundRect(2, 2, 196, 20, 3);
    fill.fill(0xff4444);
    bar.addChild(fill);

    bar.updateHealth = (ratio) => {
        fill.clear();
        fill.roundRect(2, 2, 196 * ratio, 20, 3);
        fill.fill(0xff4444);
    };

    return bar;
}

// 使用遮罩显示小地图
const minimap = new Container();
minimap.x = 650;
minimap.y = 10;
hud.addChild(minimap);

const minimapContent = new Container();
const minimapMask = new Graphics();
minimapMask.rect(0, 0, 140, 140);
minimapMask.fill(0xffffff);
minimapContent.mask = minimapMask;
minimap.addChild(minimapContent, minimapMask);

扩展阅读