第3章:显示对象体系
3.1 章节概述
PixiJS 的显示对象体系是其核心架构之一。所有可渲染的元素都继承自 DisplayObject,理解这个体系对于掌握 PixiJS 至关重要。
本章将深入讲解:
- DisplayObject 基类:属性、方法
- 继承体系:Container、Sprite、Graphics、Text
- 可见性控制:visible、renderable、alpha
- 层级管理:zIndex、sortableChildren
- 包围盒:getBounds、getLocalBounds
- 销毁机制:destroy、内存回收
3.2 DisplayObject 基类
3.2.1 继承体系结构
PixiJS 显示对象继承体系
DisplayObject
│
│ 基类,所有显示对象的父类
│
┌──────────────┼──────────────┐
│ │ │
▼ ▼ ▼
Container Sprite Graphics
│ │ │
│ │ │
┌─────────┼─────────┐ │ │
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Stage ParticleContainer TilingSprite ...
│
│
┌─────────┼─────────┐
│ │ │
▼ ▼ ▼
Text BitmapText HTMLText
DisplayObject 提供的核心功能:
- 位置(position)
- 缩放(scale)
- 旋转(rotation)
- 透明度(alpha)
- 可见性(visible)
- 变换矩阵(transform)
- 包围盒计算(bounds)3.2.2 核心属性
typescript
/**
* DisplayObject 核心属性
*/
// ============ 变换属性 ============
displayObject.x = 100; // X 位置
displayObject.y = 100; // Y 位置
displayObject.position.set(100, 100); // 同时设置 X 和 Y
displayObject.scale.x = 2; // X 缩放
displayObject.scale.y = 2; // Y 缩放
displayObject.scale.set(2); // 同时设置
displayObject.rotation = Math.PI / 4; // 旋转(弧度)
displayObject.angle = 45; // 旋转(角度)
displayObject.pivot.set(50, 50); // 旋转/缩放中心点
displayObject.skew.set(0.5, 0); // 倾斜
// ============ 可见性属性 ============
displayObject.visible = true; // 是否可见
displayObject.renderable = true; // 是否渲染
displayObject.alpha = 0.5; // 透明度 (0-1)
// ============ 层级属性 ============
displayObject.zIndex = 10; // 层级索引
displayObject.parent; // 父容器
displayObject.worldVisible; // 世界可见性(考虑父级)
displayObject.worldAlpha; // 世界透明度(考虑父级)
// ============ 变换矩阵 ============
displayObject.localTransform; // 本地变换矩阵
displayObject.worldTransform; // 世界变换矩阵
// ============ 其他 ============
displayObject.name = 'mySprite'; // 名称(用于调试)
displayObject.cursor = 'pointer'; // 鼠标样式
displayObject.eventMode = 'static'; // 事件模式3.2.3 核心方法
typescript
/**
* DisplayObject 核心方法
*/
// ============ 包围盒 ============
const bounds = displayObject.getBounds(); // 获取世界空间包围盒
const localBounds = displayObject.getLocalBounds(); // 获取本地空间包围盒
console.log(bounds.x, bounds.y, bounds.width, bounds.height);
// ============ 坐标转换 ============
// 将全局坐标转换为本地坐标
const localPoint = displayObject.toLocal(globalPoint);
// 将本地坐标转换为全局坐标
const globalPoint = displayObject.toGlobal(localPoint);
// 将一个对象的坐标转换到另一个对象的坐标系
const point = displayObject.toLocal(otherPoint, otherObject);
// ============ 变换更新 ============
displayObject.updateTransform(); // 更新变换矩阵
// ============ 销毁 ============
displayObject.destroy(); // 销毁对象
displayObject.destroy({
children: true, // 同时销毁子对象
texture: true, // 销毁纹理
baseTexture: true, // 销毁基础纹理
});3.3 可见性控制
3.3.1 visible vs renderable vs alpha
三种可见性控制的区别
┌─────────────────────────────────────────────────────────────┐
│ visible = false │
├─────────────────────────────────────────────────────────────┤
│ - 不渲染 │
│ - 不参与交互 │
│ - 子对象也不渲染 │
│ - 不计算包围盒 │
│ - 性能最好 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ renderable = false │
├─────────────────────────────────────────────────────────────┤
│ - 不渲染 │
│ - 仍参与交互(如果启用) │
│ - 子对象仍然渲染 │
│ - 仍计算包围盒 │
│ - 适合只需要交互区域的场景 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ alpha = 0 │
├─────────────────────────────────────────────────────────────┤
│ - 仍然渲染(只是完全透明) │
│ - 仍参与交互 │
│ - 子对象也变透明 │
│ - 仍计算包围盒 │
│ - 性能最差(仍有渲染开销) │
└─────────────────────────────────────────────────────────────┘3.3.2 使用示例
typescript
/**
* 可见性控制示例
*/
// 场景:游戏暂停时隐藏游戏内容
function pauseGame() {
// 使用 visible,完全隐藏
gameContainer.visible = false;
// 显示暂停菜单
pauseMenu.visible = true;
}
// 场景:隐藏精灵但保留点击区域
function createInvisibleButton() {
const button = new PIXI.Sprite(texture);
button.renderable = false; // 不渲染
button.eventMode = 'static';
button.on('pointerdown', onClick);
return button;
}
// 场景:淡入淡出动画
function fadeOut(displayObject: PIXI.DisplayObject, duration: number) {
const startAlpha = displayObject.alpha;
const startTime = performance.now();
function update() {
const elapsed = performance.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
displayObject.alpha = startAlpha * (1 - progress);
if (progress < 1) {
requestAnimationFrame(update);
}
}
update();
}3.3.3 worldVisible 和 worldAlpha
typescript
/**
* 世界可见性和透明度
*/
// 父容器
const parent = new PIXI.Container();
parent.alpha = 0.5;
parent.visible = true;
// 子精灵
const child = new PIXI.Sprite(texture);
child.alpha = 0.8;
child.visible = true;
parent.addChild(child);
// 世界透明度 = 父透明度 × 子透明度
console.log(child.worldAlpha); // 0.5 * 0.8 = 0.4
// 世界可见性 = 父可见 && 子可见
console.log(child.worldVisible); // true && true = true
// 如果父不可见
parent.visible = false;
console.log(child.worldVisible); // false(即使子的 visible 是 true)3.4 层级管理
3.4.1 渲染顺序
默认渲染顺序
Container 的子对象按添加顺序渲染(后添加的在上面)
添加顺序:A → B → C
渲染结果:
┌─────────────────┐
│ C │ ← 最后添加,在最上面
│ ┌───────┐ │
│ │ B │ │
│ ┌──┴───────┴──┐ │
│ │ A │ │ ← 最先添加,在最下面
│ └─────────────┘ │
└─────────────────┘3.4.2 使用 zIndex
typescript
/**
* 使用 zIndex 控制层级
*/
const container = new PIXI.Container();
container.sortableChildren = true; // 必须启用!
const spriteA = new PIXI.Sprite(textureA);
spriteA.zIndex = 1;
const spriteB = new PIXI.Sprite(textureB);
spriteB.zIndex = 3;
const spriteC = new PIXI.Sprite(textureC);
spriteC.zIndex = 2;
// 添加顺序:A, B, C
container.addChild(spriteA);
container.addChild(spriteB);
container.addChild(spriteC);
// 渲染顺序(按 zIndex):A(1) → C(2) → B(3)
// B 在最上面
// 动态修改 zIndex
spriteA.zIndex = 10;
container.sortChildren(); // 需要手动触发排序3.4.3 手动调整顺序
typescript
/**
* 手动调整子对象顺序
*/
// 设置子对象索引
container.setChildIndex(sprite, 0); // 移到最底层
container.setChildIndex(sprite, container.children.length - 1); // 移到最顶层
// 交换两个子对象
container.swapChildren(spriteA, spriteB);
// 获取子对象索引
const index = container.getChildIndex(sprite);
// 在指定位置添加
container.addChildAt(sprite, 0); // 添加到最底层3.5 包围盒计算
3.5.1 getBounds vs getLocalBounds
包围盒的区别
getLocalBounds():
- 返回对象在自身坐标系中的包围盒
- 不考虑 position、scale、rotation
- 用于获取对象的原始尺寸
getBounds():
- 返回对象在世界坐标系中的包围盒
- 考虑所有变换(包括父级)
- 用于碰撞检测、视口剔除
示例:
原始精灵(100x100):
┌─────────────────┐
│ │
│ Sprite │ getLocalBounds(): {x:0, y:0, w:100, h:100}
│ │
└─────────────────┘
缩放 2 倍后:
┌─────────────────────────────────┐
│ │
│ │
│ Sprite │ getLocalBounds(): {x:0, y:0, w:100, h:100}
│ │ getBounds(): {x:?, y:?, w:200, h:200}
│ │
└─────────────────────────────────┘3.5.2 包围盒使用示例
typescript
/**
* 包围盒使用示例
*/
// 获取精灵的实际显示尺寸
function getDisplaySize(sprite: PIXI.Sprite) {
const bounds = sprite.getBounds();
return {
width: bounds.width,
height: bounds.height
};
}
// 检查两个对象是否重叠
function isOverlapping(a: PIXI.DisplayObject, b: PIXI.DisplayObject) {
const boundsA = a.getBounds();
const boundsB = b.getBounds();
return boundsA.x < boundsB.x + boundsB.width &&
boundsA.x + boundsA.width > boundsB.x &&
boundsA.y < boundsB.y + boundsB.height &&
boundsA.y + boundsA.height > boundsB.y;
}
// 检查对象是否在视口内
function isInViewport(obj: PIXI.DisplayObject, viewport: PIXI.Rectangle) {
const bounds = obj.getBounds();
return bounds.x + bounds.width > viewport.x &&
bounds.x < viewport.x + viewport.width &&
bounds.y + bounds.height > viewport.y &&
bounds.y < viewport.y + viewport.height;
}
// 将对象居中到容器
function centerInContainer(obj: PIXI.DisplayObject, container: PIXI.Container) {
const bounds = obj.getLocalBounds();
const containerBounds = container.getLocalBounds();
obj.x = (containerBounds.width - bounds.width) / 2 - bounds.x;
obj.y = (containerBounds.height - bounds.height) / 2 - bounds.y;
}3.5.3 包围盒性能注意
typescript
/**
* 包围盒性能优化
*/
// getBounds() 会递归计算所有子对象
// 对于复杂场景,频繁调用会影响性能
// 缓存包围盒
class CachedBoundsSprite extends PIXI.Sprite {
private _cachedBounds: PIXI.Rectangle | null = null;
private _boundsDirty = true;
getCachedBounds() {
if (this._boundsDirty || !this._cachedBounds) {
this._cachedBounds = this.getBounds();
this._boundsDirty = false;
}
return this._cachedBounds;
}
invalidateBounds() {
this._boundsDirty = true;
}
}
// 使用 skipUpdate 参数
const bounds = sprite.getBounds(true); // 跳过变换更新(如果确定没变化)3.6 销毁与内存回收
3.6.1 destroy 方法
typescript
/**
* 销毁显示对象
*/
// 基本销毁
sprite.destroy();
// 完整销毁选项
sprite.destroy({
children: true, // 销毁所有子对象
texture: true, // 销毁关联的纹理
baseTexture: true, // 销毁基础纹理
});
/*
销毁选项说明:
children: true
- 递归销毁所有子对象
- 适合销毁整个场景
texture: true
- 销毁 Sprite 使用的 Texture
- 如果其他对象也使用这个纹理,会出问题
baseTexture: true
- 销毁底层的 BaseTexture
- 释放 GPU 内存
- 更彻底,但要确保没有其他引用
*/3.6.2 正确的销毁流程
typescript
/**
* 正确的销毁流程
*/
// 场景:销毁一个游戏场景
function destroyScene(scene: PIXI.Container) {
// 1. 停止所有动画
scene.children.forEach(child => {
if (child instanceof AnimatedSprite) {
child.stop();
}
});
// 2. 移除所有事件监听
scene.removeAllListeners();
scene.children.forEach(child => {
child.removeAllListeners();
});
// 3. 从父容器移除
scene.parent?.removeChild(scene);
// 4. 销毁
scene.destroy({
children: true,
texture: false, // 纹理可能被其他场景使用
baseTexture: false,
});
}
// 场景:销毁并释放所有资源
function destroyAndCleanup(app: PIXI.Application) {
// 销毁舞台
app.stage.destroy({
children: true,
texture: true,
baseTexture: true,
});
// 清理纹理缓存
PIXI.utils.clearTextureCache();
// 销毁应用
app.destroy(true);
}3.6.3 内存泄漏排查
typescript
/**
* 内存泄漏排查
*/
// 检查纹理缓存
function logTextureCache() {
const cache = PIXI.utils.TextureCache;
console.log('纹理缓存数量:', Object.keys(cache).length);
console.log('纹理列表:', Object.keys(cache));
}
// 检查 BaseTexture 数量
function logBaseTextures() {
// v7
const count = PIXI.BaseTexture._globalBatch;
console.log('BaseTexture 数量:', count);
}
// 监控对象创建
let spriteCount = 0;
const OriginalSprite = PIXI.Sprite;
PIXI.Sprite = class extends OriginalSprite {
constructor(...args) {
super(...args);
spriteCount++;
console.log('Sprite 创建,总数:', spriteCount);
}
destroy(options) {
super.destroy(options);
spriteCount--;
console.log('Sprite 销毁,总数:', spriteCount);
}
};3.7 常用显示对象类型
3.7.1 类型概览
| 类型 | 用途 | 特点 |
|---|---|---|
| Container | 容器,组织子对象 | 不渲染自身 |
| Sprite | 显示图像 | 最常用 |
| Graphics | 矢量图形 | 可绑制形状 |
| Text | 文本 | 动态文本 |
| BitmapText | 位图文本 | 高性能 |
| TilingSprite | 平铺精灵 | 背景、地面 |
| AnimatedSprite | 动画精灵 | 帧动画 |
| Mesh | 网格 | 自定义几何 |
| NineSliceSprite | 九宫格 | UI 边框 |
3.7.2 选择建议
显示对象选择指南
需要显示图片?
│
├── 静态图片 → Sprite
│
├── 帧动画 → AnimatedSprite
│
├── 平铺背景 → TilingSprite
│
└── 可拉伸边框 → NineSliceSprite
需要绘制图形?
│
└── Graphics
需要显示文本?
│
├── 动态文本 → Text
│
└── 大量静态文本 → BitmapText
需要组织对象?
│
├── 普通容器 → Container
│
└── 大量粒子 → ParticleContainer3.8 本章小结
核心概念
| 概念 | 说明 |
|---|---|
| DisplayObject | 所有显示对象的基类 |
| visible | 控制对象及子对象是否渲染 |
| renderable | 控制对象自身是否渲染 |
| alpha | 透明度 |
| zIndex | 层级索引 |
| getBounds | 获取世界空间包围盒 |
| getLocalBounds | 获取本地空间包围盒 |
| destroy | 销毁对象并释放资源 |
关键代码
typescript
// 变换
sprite.position.set(100, 100);
sprite.scale.set(2);
sprite.rotation = Math.PI / 4;
// 可见性
sprite.visible = false;
sprite.alpha = 0.5;
// 层级
container.sortableChildren = true;
sprite.zIndex = 10;
// 包围盒
const bounds = sprite.getBounds();
// 销毁
sprite.destroy({ children: true });3.9 练习题
基础练习
创建多个精灵,使用 zIndex 控制层级
实现一个淡入淡出效果
计算两个精灵是否重叠
进阶练习
实现一个对象池,复用精灵对象
创建一个场景管理器,正确处理场景切换和销毁
挑战练习
- 实现视口剔除,只渲染可见区域内的对象
下一章预告:在第4章中,我们将深入学习 Container 容器和场景图管理。
文档版本:v1.0
字数统计:约 10,000 字
代码示例:40+ 个
