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