PixiJS 动画系统 #

动画概述 #

PixiJS 提供了强大的动画系统,包括基于帧的 Ticker 和补间动画 Tween。

text
┌─────────────────────────────────────────────────────────────┐
│                    动画系统架构                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Ticker(帧循环)                                          │
│   ├── 核心计时系统                                          │
│   ├── 每帧执行回调                                          │
│   └── 适合游戏循环、物理模拟                                │
│                                                             │
│   Tween(补间动画)                                         │
│   ├── 自动插值计算                                          │
│   ├── 缓动函数支持                                          │
│   └── 适合属性动画、过渡效果                                │
│                                                             │
│   AnimatedSprite(帧动画)                                   │
│   ├── 精灵表动画                                            │
│   ├── 序列帧播放                                            │
│   └── 适合角色动画                                          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Ticker(帧循环) #

基本使用 #

javascript
import { Application } from 'pixi.js';

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

// 添加帧回调
app.ticker.add((ticker) => {
  // ticker.deltaTime - 归一化帧时间(60fps 为基准)
  // ticker.deltaMS - 帧时间(毫秒)
  // ticker.FPS - 当前帧率
  
  console.log('FPS:', ticker.FPS);
  console.log('Delta:', ticker.deltaTime);
});

动画循环 #

javascript
const sprite = PIXI.Sprite.from('player.png');
sprite.x = 0;
app.stage.addChild(sprite);

// 使用 ticker 创建动画
app.ticker.add((ticker) => {
  sprite.x += 2 * ticker.deltaTime;
  
  // 循环移动
  if (sprite.x > app.screen.width) {
    sprite.x = -sprite.width;
  }
});

Ticker 属性 #

javascript
// 当前帧率
console.log('FPS:', app.ticker.FPS);

// 帧时间(毫秒)
console.log('Delta MS:', app.ticker.deltaMS);

// 归一化帧时间
console.log('Delta Time:', app.ticker.deltaTime);

// 是否运行中
console.log('Started:', app.ticker.started);

// 最小帧率
app.ticker.minFPS = 30;

// 最大帧率
app.ticker.maxFPS = 60;

控制帧循环 #

javascript
// 停止
app.ticker.stop();

// 启动
app.ticker.start();

// 添加回调
const callback = (ticker) => {
  console.log('更新');
};
app.ticker.add(callback);

// 移除回调
app.ticker.remove(callback);

// 单次回调
app.ticker.addOnce((ticker) => {
  console.log('下一帧执行');
});

独立 Ticker #

javascript
import { Ticker } from 'pixi.js';

// 创建独立的 Ticker
const myTicker = new Ticker();

myTicker.add((ticker) => {
  // 独立的更新循环
});

myTicker.start();

// 停止
myTicker.stop();

// 销毁
myTicker.destroy();

帧率控制 #

javascript
// 固定帧率更新
const fixedFPS = 30;
const frameTime = 1000 / fixedFPS;
let accumulator = 0;

app.ticker.add((ticker) => {
  accumulator += ticker.deltaMS;
  
  while (accumulator >= frameTime) {
    update(frameTime / 1000);
    accumulator -= frameTime;
  }
  
  render();
});

function update(delta) {
  // 固定帧率的更新逻辑
}

function render() {
  // 渲染逻辑
}

Tween(补间动画) #

基本补间 #

javascript
import { Tween } from 'pixi.js';

// 创建补间动画
const tween = new Tween(sprite)
  .to({ x: 500, y: 300 }, 1000)  // 目标属性,持续时间(毫秒)
  .start();  // 开始动画

使用 Tween 库 #

PixiJS 8 推荐使用第三方 Tween 库:

javascript
// 安装
// npm install @tweenjs/tween.js

import * as TWEEN from '@tweenjs/tween.js';

// 创建补间
const tween = new TWEEN.Tween(sprite)
  .to({ x: 500, y: 300 }, 1000)
  .easing(TWEEN.Easing.Quadratic.Out)
  .onStart(() => console.log('开始'))
  .onUpdate((object, elapsed) => {
    console.log('进度:', elapsed);
  })
  .onComplete(() => console.log('完成'))
  .start();

// 在 ticker 中更新
app.ticker.add(() => {
  TWEEN.update();
});

GSAP 集成 #

javascript
// 安装
// npm install gsap

import { gsap } from 'gsap';

// 基本补间
gsap.to(sprite, {
  x: 500,
  y: 300,
  duration: 1,
  ease: 'power2.out'
});

// 从当前值到目标值
gsap.from(sprite, {
  alpha: 0,
  duration: 0.5
});

// 从起始值到目标值
gsap.fromTo(sprite, 
  { alpha: 0, scale: 0 },
  { alpha: 1, scale: 1, duration: 0.5 }
);

GSAP 高级用法 #

javascript
// 序列动画
const tl = gsap.timeline();

tl.to(sprite, { x: 500, duration: 1 })
  .to(sprite, { y: 300, duration: 0.5 })
  .to(sprite, { rotation: Math.PI, duration: 0.5 });

// 并行动画
gsap.to([sprite1, sprite2, sprite3], {
  x: 400,
  duration: 1,
  stagger: 0.2  // 依次延迟
});

// 重复和往返
gsap.to(sprite, {
  x: 500,
  duration: 1,
  repeat: -1,     // 无限重复
  yoyo: true,     // 往返
  ease: 'power1.inOut'
});

// 回调函数
gsap.to(sprite, {
  x: 500,
  duration: 1,
  onStart: () => console.log('开始'),
  onUpdate: () => console.log('更新'),
  onComplete: () => console.log('完成'),
  onReverseComplete: () => console.log('反向完成')
});

缓动函数 #

text
┌─────────────────────────────────────────────────────────────┐
│                    常用缓动函数                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Linear                                                    │
│   └── 匀速运动                                              │
│                                                             │
│   Quad(二次方)                                            │
│   ├── Quad.In    - 开始慢                                   │
│   ├── Quad.Out   - 结束慢                                   │
│   └── Quad.InOut - 两头慢中间快                             │
│                                                             │
│   Cubic(三次方)                                            │
│   └── 比 Quad 更明显的加速/减速                             │
│                                                             │
│   Elastic(弹性)                                            │
│   └── 弹性效果,像弹簧                                      │
│                                                             │
│   Bounce(弹跳)                                             │
│   └── 弹跳效果                                              │
│                                                             │
│   Back                                                      │
│   └── 超出目标后回弹                                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
javascript
// GSAP 缓动
gsap.to(sprite, {
  x: 500,
  duration: 1,
  ease: 'power2.out'      // Quad.Out
});

gsap.to(sprite, {
  x: 500,
  duration: 1,
  ease: 'elastic.out(1, 0.3)'  // 弹性
});

gsap.to(sprite, {
  x: 500,
  duration: 1,
  ease: 'bounce.out'      // 弹跳
});

AnimatedSprite(帧动画) #

创建帧动画 #

javascript
import { AnimatedSprite, Assets } from 'pixi.js';

// 从精灵表创建
const sheet = await Assets.load('spritesheet.json');
const frames = sheet.animations['walk'];

const animatedSprite = new AnimatedSprite(frames);
animatedSprite.play();
app.stage.addChild(animatedSprite);

手动创建帧序列 #

javascript
// 从单独的图片创建
const frames = [];
for (let i = 0; i < 8; i++) {
  frames.push(PIXI.Texture.from(`frame_${i}.png`));
}

const animatedSprite = new AnimatedSprite(frames);
app.stage.addChild(animatedSprite);

控制播放 #

javascript
const sprite = new AnimatedSprite(frames);

// 播放
sprite.play();

// 暂停
sprite.stop();

// 跳转到特定帧
sprite.gotoAndPlay(5);
sprite.gotoAndStop(0);

// 动画速度
sprite.animationSpeed = 0.1;  // 默认 1

// 循环
sprite.loop = true;

// 是否播放
sprite.playing;

动画事件 #

javascript
const sprite = new AnimatedSprite(frames);

// 动画完成(非循环时)
sprite.onComplete = () => {
  console.log('动画完成');
};

// 帧变化
sprite.onFrameChange = (currentFrame) => {
  console.log('当前帧:', currentFrame);
};

多动画管理 #

javascript
class Character extends Container {
  constructor() {
    super();
    
    this.animations = {};
    this.currentAnimation = null;
  }
  
  async loadAnimations(sheetPath) {
    const sheet = await Assets.load(sheetPath);
    
    // 加载多个动画
    this.animations.idle = new AnimatedSprite(sheet.animations['idle']);
    this.animations.walk = new AnimatedSprite(sheet.animations['walk']);
    this.animations.run = new AnimatedSprite(sheet.animations['run']);
    this.animations.jump = new AnimatedSprite(sheet.animations['jump']);
    
    // 设置默认动画
    this.play('idle');
  }
  
  play(name) {
    if (this.currentAnimation) {
      this.removeChild(this.currentAnimation);
      this.currentAnimation.stop();
    }
    
    this.currentAnimation = this.animations[name];
    this.currentAnimation.play();
    this.addChild(this.currentAnimation);
  }
}

自定义动画 #

属性动画类 #

javascript
class PropertyTween {
  constructor(target, property, endValue, duration, options = {}) {
    this.target = target;
    this.property = property;
    this.startValue = target[property];
    this.endValue = endValue;
    this.duration = duration;
    this.elapsed = 0;
    this.ease = options.ease || ((t) => t);
    this.onUpdate = options.onUpdate;
    this.onComplete = options.onComplete;
    
    this.active = true;
  }
  
  update(deltaMS) {
    if (!this.active) return false;
    
    this.elapsed += deltaMS;
    const progress = Math.min(this.elapsed / this.duration, 1);
    const easedProgress = this.ease(progress);
    
    this.target[this.property] = 
      this.startValue + (this.endValue - this.startValue) * easedProgress;
    
    if (this.onUpdate) {
      this.onUpdate(progress);
    }
    
    if (progress >= 1) {
      this.active = false;
      if (this.onComplete) {
        this.onComplete();
      }
      return false;
    }
    
    return true;
  }
  
  stop() {
    this.active = false;
  }
}

动画管理器 #

javascript
class AnimationManager {
  constructor() {
    this.animations = [];
  }
  
  tween(target, properties, duration, options = {}) {
    const tweens = [];
    
    for (const [prop, endValue] of Object.entries(properties)) {
      const tween = new PropertyTween(target, prop, endValue, duration, options);
      tweens.push(tween);
      this.animations.push(tween);
    }
    
    return {
      stop: () => tweens.forEach(t => t.stop())
    };
  }
  
  update(deltaMS) {
    this.animations = this.animations.filter(anim => anim.update(deltaMS));
  }
}

const animManager = new AnimationManager();

app.ticker.add((ticker) => {
  animManager.update(ticker.deltaMS);
});

// 使用
animManager.tween(sprite, { x: 500, y: 300 }, 1000, {
  ease: (t) => t * t,  // ease in
  onComplete: () => console.log('完成')
});

路径动画 #

javascript
class PathAnimation {
  constructor(target, path, duration, options = {}) {
    this.target = target;
    this.path = path;  // 点数组 [{x, y}, ...]
    this.duration = duration;
    this.elapsed = 0;
    this.ease = options.ease || ((t) => t);
    this.loop = options.loop || false;
    
    this.active = true;
  }
  
  update(deltaMS) {
    if (!this.active) return false;
    
    this.elapsed += deltaMS;
    let progress = this.elapsed / this.duration;
    
    if (this.loop) {
      progress = progress % 1;
    } else if (progress >= 1) {
      progress = 1;
      this.active = false;
    }
    
    const easedProgress = this.ease(progress);
    const pos = this.getPositionOnPath(easedProgress);
    
    this.target.x = pos.x;
    this.target.y = pos.y;
    
    return this.active;
  }
  
  getPositionOnPath(t) {
    const totalLength = this.path.length - 1;
    const segment = t * totalLength;
    const index = Math.floor(segment);
    const localT = segment - index;
    
    const p0 = this.path[Math.max(0, index)];
    const p1 = this.path[Math.min(totalLength, index + 1)];
    
    return {
      x: p0.x + (p1.x - p0.x) * localT,
      y: p0.y + (p1.y - p0.y) * localT
    };
  }
  
  stop() {
    this.active = false;
  }
}

实战示例 #

弹跳球 #

javascript
class BouncingBall {
  constructor(x, y, radius) {
    this.sprite = new Graphics();
    this.sprite.circle(0, 0, radius);
    this.sprite.fill({ color: 0xff6600 });
    this.sprite.x = x;
    this.sprite.y = y;
    
    this.velocity = { x: 0, y: 0 };
    this.gravity = 0.5;
    this.bounce = 0.7;
    this.friction = 0.99;
  }
  
  update(delta) {
    // 应用重力
    this.velocity.y += this.gravity * delta;
    
    // 应用摩擦力
    this.velocity.x *= this.friction;
    
    // 更新位置
    this.sprite.x += this.velocity.x * delta;
    this.sprite.y += this.velocity.y * delta;
    
    // 边界碰撞
    if (this.sprite.y > app.screen.height - 20) {
      this.sprite.y = app.screen.height - 20;
      this.velocity.y *= -this.bounce;
    }
    
    if (this.sprite.x < 20 || this.sprite.x > app.screen.width - 20) {
      this.velocity.x *= -1;
    }
  }
}

const ball = new BouncingBall(400, 100, 20);
app.stage.addChild(ball.sprite);

app.ticker.add((ticker) => {
  ball.update(ticker.deltaTime);
});

粒子系统 #

javascript
class Particle {
  constructor(x, y, options = {}) {
    this.sprite = new Graphics();
    this.sprite.circle(0, 0, options.size || 5);
    this.sprite.fill({ color: options.color || 0xffffff });
    this.sprite.x = x;
    this.sprite.y = y;
    
    this.velocity = {
      x: (Math.random() - 0.5) * (options.speed || 5),
      y: (Math.random() - 0.5) * (options.speed || 5)
    };
    
    this.life = options.life || 60;
    this.maxLife = this.life;
    this.gravity = options.gravity || 0;
  }
  
  update(delta) {
    this.velocity.y += this.gravity * delta;
    this.sprite.x += this.velocity.x * delta;
    this.sprite.y += this.velocity.y * delta;
    this.life -= delta;
    
    this.sprite.alpha = this.life / this.maxLife;
    
    return this.life > 0;
  }
}

class ParticleEmitter {
  constructor(x, y, options = {}) {
    this.x = x;
    this.y = y;
    this.options = options;
    this.particles = [];
    this.container = new Container();
    
    this.emitRate = options.emitRate || 1;
    this.emitCounter = 0;
  }
  
  update(delta) {
    // 发射新粒子
    this.emitCounter += this.emitRate * delta;
    while (this.emitCounter >= 1) {
      this.emit();
      this.emitCounter--;
    }
    
    // 更新现有粒子
    for (let i = this.particles.length - 1; i >= 0; i--) {
      if (!this.particles[i].update(delta)) {
        this.container.removeChild(this.particles[i].sprite);
        this.particles.splice(i, 1);
      }
    }
  }
  
  emit() {
    const particle = new Particle(this.x, this.y, this.options);
    this.particles.push(particle);
    this.container.addChild(particle.sprite);
  }
}

const emitter = new ParticleEmitter(400, 300, {
  speed: 3,
  size: 4,
  color: 0xff9900,
  life: 100,
  gravity: 0.1,
  emitRate: 2
});

app.stage.addChild(emitter.container);

app.ticker.add((ticker) => {
  emitter.update(ticker.deltaTime);
});

平滑跟随 #

javascript
class SmoothFollow {
  constructor(follower, target, smoothness = 0.1) {
    this.follower = follower;
    this.target = target;
    this.smoothness = smoothness;
  }
  
  update() {
    this.follower.x += (this.target.x - this.follower.x) * this.smoothness;
    this.follower.y += (this.target.y - this.follower.y) * this.smoothness;
  }
}

const follower = Sprite.from('follower.png');
const target = { x: 400, y: 300 };

const smoothFollow = new SmoothFollow(follower, target, 0.05);

app.ticker.add(() => {
  smoothFollow.update();
});

// 更新目标位置
app.stage.eventMode = 'static';
app.stage.on('pointermove', (event) => {
  target.x = event.global.x;
  target.y = event.global.y;
});

闪烁效果 #

javascript
function blink(sprite, duration = 0.5, times = 3) {
  const halfDuration = duration / (times * 2);
  const tl = gsap.timeline({ repeat: times - 1 });
  
  tl.to(sprite, { alpha: 0, duration: halfDuration })
    .to(sprite, { alpha: 1, duration: halfDuration });
  
  return tl;
}

blink(sprite, 0.3, 5);

抖动效果 #

javascript
function shake(target, intensity = 10, duration = 0.5) {
  const originalX = target.x;
  const originalY = target.y;
  
  const tl = gsap.timeline({
    onComplete: () => {
      target.x = originalX;
      target.y = originalY;
    }
  });
  
  const steps = 10;
  const stepDuration = duration / steps;
  
  for (let i = 0; i < steps; i++) {
    tl.to(target, {
      x: originalX + (Math.random() - 0.5) * intensity,
      y: originalY + (Math.random() - 0.5) * intensity,
      duration: stepDuration
    });
  }
  
  return tl;
}

shake(sprite, 15, 0.3);

性能优化 #

减少动画数量 #

javascript
// 不推荐:每个对象独立动画
objects.forEach(obj => {
  gsap.to(obj, { x: 500, duration: 1 });
});

// 推荐:使用容器统一处理
const container = new Container();
objects.forEach(obj => container.addChild(obj));

gsap.to(container, { x: 500, duration: 1 });

对象池 #

javascript
class ObjectPool {
  constructor(createFn, initialSize = 10) {
    this.createFn = createFn;
    this.pool = [];
    
    for (let i = 0; i < initialSize; i++) {
      this.pool.push(this.createFn());
    }
  }
  
  get() {
    return this.pool.length > 0 ? this.pool.pop() : this.createFn();
  }
  
  release(obj) {
    this.pool.push(obj);
  }
}

const particlePool = new ObjectPool(() => {
  const sprite = Sprite.from('particle.png');
  sprite.visible = false;
  return sprite;
});

条件更新 #

javascript
// 只在可见时更新动画
app.ticker.add((ticker) => {
  if (sprite.visible && sprite.alpha > 0) {
    sprite.rotation += 0.01 * ticker.deltaTime;
  }
});

下一步 #

掌握了动画系统后,接下来学习 资源加载,了解如何高效加载和管理游戏资源!

最后更新:2026-03-29