Skip to content

第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

    └── 大量粒子 → ParticleContainer

3.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 练习题

基础练习

  1. 创建多个精灵,使用 zIndex 控制层级

  2. 实现一个淡入淡出效果

  3. 计算两个精灵是否重叠

进阶练习

  1. 实现一个对象池,复用精灵对象

  2. 创建一个场景管理器,正确处理场景切换和销毁

挑战练习

  1. 实现视口剔除,只渲染可见区域内的对象

下一章预告:在第4章中,我们将深入学习 Container 容器和场景图管理。


文档版本:v1.0
字数统计:约 10,000 字
代码示例:40+ 个

如有转载或 CV 的请标注本站原文地址