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

Pixi.js 游戏开发教程 / Sprite 与纹理

Sprite 与纹理

概述

Sprite(精灵)是 2D 游戏和图形应用中最基本的显示对象。它本质上是一张带有变换属性(位置、旋转、缩放等)的纹理矩形。理解 Sprite 和 Texture 是构建任何 Pixi.js 项目的基础。


Sprite 创建方式

方式一:使用 Assets 加载(推荐)

import { Application, Assets, Sprite } from 'pixi.js';

const app = new Application();
await app.init({ width: 800, height: 600, background: '#1a1a2e' });
document.body.appendChild(app.canvas);

// 异步加载纹理并创建精灵
const texture = await Assets.load('/image/hero.png');
const hero = new Sprite(texture);

hero.anchor.set(0.5); // 锚点居中
hero.position.set(400, 300);
app.stage.addChild(hero);

方式二:Texture.from(同步,依赖缓存)

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

// 如果纹理已缓存,直接创建;否则返回空白纹理(异步加载后自动替换)
const texture = Texture.from('/image/enemy.png');
const enemy = new Sprite(texture);
enemy.position.set(600, 300);
app.stage.addChild(enemy);

方式三:从已有纹理裁剪

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

const baseTexture = await Assets.load('/image/spritesheet.png');

// 裁剪出特定区域
const frameRect = new Rectangle(0, 0, 64, 64);
const frameTexture = new Texture({
  source: baseTexture.source,
  frame: frameRect,
});

const sprite = new Sprite(frameTexture);
app.stage.addChild(sprite);

纹理(Texture)详解

Texture 与 BaseTexture 的关系

BaseTexture(基础纹理)
  │
  ├── 存储原始图像数据(Image/Canvas/Video)
  ├── 管理 GPU 纹理资源
  └── 可被多个 Texture 共享
        │
        Texture(纹理)
          ├── 引用一个 BaseTexture
          ├── 定义裁剪区域(frame)
          ├── 可设置旋转、修剪(trim)
          └── 作为 Sprite 的渲染数据源
作用说明
BaseTexture存储原始图像一份图像数据对应一个
Texture定义图像的可视区域一个 BaseTexture 可派生多个

纹理缓存

Pixi.js 自动缓存已加载的纹理,避免重复加载:

// 首次加载 —— 从网络/磁盘读取
const texture1 = await Assets.load('/image/hero.png');

// 再次获取 —— 从缓存返回(O(1))
const texture2 = Assets.get('/image/hero.png');
console.log(texture1 === texture2); // true

// 手动移除缓存
Assets.unload('/image/hero.png');

⚠️ 注意: 纹理缓存会占用显存。不再使用的纹理应及时调用 Assets.unload()texture.destroy() 释放。


Sprite 属性

基础变换属性

const sprite = new Sprite(texture);

// 位置
sprite.x = 100;
sprite.y = 200;
// 或
sprite.position.set(100, 200);

// 缩放
sprite.scale.set(2, 2);        // 2 倍放大
sprite.scale.x = 0.5;          // 水平缩小一半

// 旋转(弧度,顺时针)
sprite.rotation = Math.PI / 4; // 45 度

// 锚点(0-1,相对于自身尺寸)
sprite.anchor.set(0.5, 0.5);  // 中心点
// anchor(0,0) = 左上角, anchor(1,1) = 右下角

// 透明度
sprite.alpha = 0.5;

// 色调叠加
sprite.tint = 0xff0000;        // 红色叠加

// 可见性
sprite.visible = false;        // 完全不渲染

// 是否可交互
sprite.eventMode = 'static';   // 启用事件
sprite.cursor = 'pointer';     // 鼠标样式

属性速查表

属性类型默认值说明
positionObservablePoint{x:0, y:0}位置(像素)
scaleObservablePoint{x:1, y:1}缩放倍数
rotationnumber0旋转(弧度)
anchorObservablePoint{x:0, y:0}锚点(0-1)
alphanumber1透明度(0-1)
tintnumber0xFFFFFF色调叠加
visiblebooleantrue是否可见
widthnumber纹理宽度实际渲染宽度(受 scale 影响)
heightnumber纹理高度实际渲染高度

Sprite 变换矩阵

Pixi.js 内部使用 3×3 仿射变换矩阵处理所有变换:

| a  b  tx |   | scaleX * cos(θ)   -sin(θ)   x |
| c  d  ty | = | sin(θ)   scaleY * cos(θ)   y |
| 0  0  1  |   | 0         0          1       |
// 直接操作矩阵(高级用法)
import { Matrix } from 'pixi.js';

const matrix = new Matrix();
matrix.translate(100, 200);
matrix.rotate(Math.PI / 4);
matrix.scale(2, 2);

sprite.setFromMatrix(matrix);

💡 提示: 日常开发中直接使用 positionrotationscale 即可,无需直接操作矩阵。矩阵主要用于底层渲染和高级特效。


纹理区域(Rectangle)

import { Rectangle, Texture } from 'pixi.js';

const baseTexture = await Assets.load('/image/atlas.png');

// 定义裁剪区域
const frame1 = new Rectangle(0, 0, 32, 32);
const frame2 = new Rectangle(32, 0, 32, 32);
const frame3 = new Rectangle(64, 0, 32, 32);

// 创建裁剪纹理
const tex1 = new Texture({ source: baseTexture.source, frame: frame1 });
const tex2 = new Texture({ source: baseTexture.source, frame: frame2 });
const tex3 = new Texture({ source: baseTexture.source, frame: frame3 });

// 创建精灵并排列
[tex1, tex2, tex3].forEach((tex, i) => {
  const sprite = new Sprite(tex);
  sprite.x = i * 40;
  sprite.y = 100;
  app.stage.addChild(sprite);
});

精灵批处理原理

Pixi.js 的 WebGL 渲染器会自动将使用相同纹理的连续精灵合并为一次 draw call。

渲染流程:
  Sprite A (tex1) ─┐
  Sprite B (tex1)  ├── 同一批次 → 1 次 draw call
  Sprite C (tex1) ─┘
  Sprite D (tex2) ─────→ 1 次 draw call
  Sprite E (tex1) ─────→ 1 次 draw call(纹理切换,新批次)

优化建议

策略说明
使用纹理图集(Atlas)将多个小图合并到一张大图,减少纹理切换
相同纹理的精灵连续添加避免交叉排列不同纹理的精灵
使用 ParticleContainer大量相同纹理精灵时使用粒子容器
import { ParticleContainer } from 'pixi.js';

// 粒子容器 —— 专为大量同类精灵优化
const particles = new ParticleContainer(10000, {
  position: true,
  rotation: true,
  scale: true,
  tint: true,
});

for (let i = 0; i < 10000; i++) {
  const p = new Sprite(commonTexture);
  p.x = Math.random() * 800;
  p.y = Math.random() * 600;
  particles.addChild(p);
}

app.stage.addChild(particles);

⚠️ 注意: ParticleContainer 功能受限,不支持嵌套容器、滤镜、遮罩等。仅用于大量简单精灵的高性能渲染。


纹理集(TextureAtlas)

纹理集将多个小图打包到一张大图,并附带 JSON 描述文件。

JSON Atlas 格式示例

{
  "frames": {
    "walk_01.png": {
      "frame": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "rotated": false,
      "sourceSize": { "w": 64, "h": 64 }
    },
    "walk_02.png": {
      "frame": { "x": 64, "y": 0, "w": 64, "h": 64 },
      "rotated": false,
      "sourceSize": { "w": 64, "h": 64 }
    }
  },
  "meta": {
    "image": "spritesheet.png",
    "size": { "w": 256, "h": 256 }
  }
}

使用 Spritesheet 加载

import { Assets, Spritesheet } from 'pixi.js';

const atlasData = await fetch('/image/atlas.json').then(r => r.json());
const atlasTexture = await Assets.load('/image/atlas.png');

const spritesheet = new Spritesheet(atlasTexture, atlasData);
await spritesheet.parse();

// 通过名称获取纹理
const walkFrame1 = spritesheet.animations['walk'][0];
const sprite = new Sprite(walkFrame1);
app.stage.addChild(sprite);

游戏开发场景

场景:角色精灵管理器

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

interface CharacterConfig {
  name: string;
  texturePath: string;
  speed: number;
}

class Character {
  sprite: Sprite;
  speed: number;
  name: string;

  constructor(config: CharacterConfig, texture: Texture) {
    this.name = config.name;
    this.speed = config.speed;
    this.sprite = new Sprite(texture);
    this.sprite.anchor.set(0.5);
  }

  moveTo(targetX: number, targetY: number, delta: number) {
    const dx = targetX - this.sprite.x;
    const dy = targetY - this.sprite.y;
    const dist = Math.sqrt(dx * dx + dy * dy);

    if (dist > 1) {
      this.sprite.x += (dx / dist) * this.speed * delta;
      this.sprite.y += (dy / dist) * this.speed * delta;
      this.sprite.rotation = Math.atan2(dy, dx);
    }
  }
}

// 使用示例
const app = new Application();
await app.init({ width: 800, height: 600, background: '#2c3e50' });
document.body.appendChild(app.canvas);

const heroTexture = await Assets.load('/image/hero.png');
const hero = new Character(
  { name: 'Hero', texturePath: '/image/hero.png', speed: 200 },
  heroTexture
);
hero.sprite.position.set(400, 300);
app.stage.addChild(hero.sprite);

// 点击移动
app.stage.eventMode = 'static';
app.stage.hitArea = app.screen;
app.stage.on('pointerdown', (e) => {
  const target = e.global;
  app.ticker.add(function move(ticker) {
    hero.moveTo(target.x, target.y, ticker.deltaTime);
    const dx = target.x - hero.sprite.x;
    const dy = target.y - hero.sprite.y;
    if (Math.sqrt(dx * dx + dy * dy) < 2) {
      app.ticker.remove(move);
    }
  });
});

⚠️ 常见问题

问题原因解决方案
精灵不显示纹理未加载完成就创建 Sprite使用 await Assets.load()
精灵位置偏移anchor 设置不正确检查 anchor 设置
内存持续增长纹理未释放不用的纹理调用 .destroy()
精灵模糊纹理尺寸与显示尺寸不匹配使用合适分辨率的图片
tint 不生效纹理本身为白色tint 对非白色纹理叠加效果明显

💡 进阶提示

  1. 纹理预乘 Alpha: PNG 带透明通道时,Pixi.js 默认预乘处理。如出现边缘黑边:

    texture.source.alphaMode = 'no-premultiply-alpha';
    
  2. 精灵池(对象复用):

    class SpritePool {
      private pool: Sprite[] = [];
      private texture: Texture;
    
      constructor(texture: Texture) {
        this.texture = texture;
      }
    
      get(): Sprite {
        return this.pool.pop() || new Sprite(this.texture);
      }
    
      release(sprite: Sprite) {
        sprite.visible = false;
        this.pool.push(sprite);
      }
    }
    
  3. 批量设置属性: Pixi.js v8 支持构造时传入配置:

    const sprite = new Sprite({
      texture,
      x: 100,
      y: 200,
      scale: 2,
      anchor: 0.5,
    });
    

扩展阅读


上一章:02 - Application 与渲染流程 下一章:04 - 容器与场景图