~/ ?.log $
返回文章列表
8 min read
更新于 2026年3月6日

我不知道的 Pixi.js(05)— 动画系统的 Ticker 解析

Pixi.js 的 Ticker 是整个渲染循环的心跳。每一帧画面的更新——无论是角色移动、粒子飘散还是滤镜动画——都由 Ticker 驱动。很多人以为 Ticker 就是 requestAnimationFrame 的封装,但实际上它还管理着回调优先级、时间步长计算和帧率控制…

Pixi.js 的 Ticker 是整个渲染循环的心跳。每一帧画面的更新——无论是角色移动、粒子飘散还是滤镜动画——都由 Ticker 驱动。很多人以为 Ticker 就是 requestAnimationFrame 的封装,但实际上它还管理着回调优先级、时间步长计算和帧率控制,比 rAF 复杂得多。


一、Ticker 的核心:requestAnimationFrame 之上的调度层

Pixi.js 的 Ticker 底层确实基于 requestAnimationFrame(rAF),但它不是简单的 rAF(callback)。Ticker 在 rAF 之上叠加了三层能力:

  • 多回调管理(一个 Ticker 可注册多个回调,按优先级排序执行)
  • 时间步长标准化(把不稳定的帧间隔转换成平滑的 delta 值)
  • 帧率限制(可以把 60 FPS 的 rAF 降频到 30 FPS)
import { Ticker } from 'pixi.js';

const ticker = Ticker.shared;

ticker.add((ticker) => {
  // ticker.deltaTime — 相对于目标帧率的倍率,60FPS 下约等于 1
  // ticker.deltaMS   — 实际经过的毫秒数
  // ticker.elapsedMS — 从 Ticker 启动到现在的总毫秒数
  sprite.rotation += 0.05 * ticker.deltaTime;
});

说白了,deltaTime 是一个”标准化帧时间”。目标帧率是 60 FPS 时,一帧耗时 16.67ms,deltaTime 约等于 1。如果某帧卡了 33ms(帧率掉到 30 FPS),deltaTime 约等于 2。乘以 deltaTime,动画速度就不会因帧率波动而忽快忽慢。


二、delta 的两种含义:deltaTime vs deltaMS

这里有一个很多人会忽略的细节——ticker.deltaTimeticker.deltaMS 是两个完全不同的值。

下面这段代码展示了区别:

ticker.add((ticker) => {
  // 方式一:基于标准化 delta(推荐)
  sprite.x += 5 * ticker.deltaTime;
  // 60FPS 时每帧移 5 像素,30FPS 时每帧移 10 像素
  // 视觉效果:始终以 300 像素/秒 的速度移动

  // 方式二:基于真实毫秒数
  sprite.y += 0.3 * ticker.deltaMS;
  // 更精确的时间驱动,不依赖目标帧率设定
});

两种方式的效果类似,但适用场景不同。deltaTime 以目标帧率为锚点,适合大多数游戏动画;deltaMS 以真实时间为锚点,适合需要精确时间控制的场景(比如和物理引擎同步)。

如果你只记住一句话:动画逻辑乘以 delta,帧率波动就不会影响动画速度。不乘 delta 的动画在高刷屏上会跑得更快,在低帧率时会变慢。


三、回调优先级:谁先执行,谁后执行

Ticker 不是简单地”谁先注册谁先执行”。它支持优先级管理,保证关键回调(比如渲染)总是在正确的时机执行。

import { Ticker, UPDATE_PRIORITY } from 'pixi.js';

const ticker = Ticker.shared;

// 高优先级:物理计算
ticker.add(
  () => {
    updatePhysics();
  },
  undefined,
  UPDATE_PRIORITY.HIGH,
); // 25

// 普通优先级:游戏逻辑(默认)
ticker.add(
  () => {
    updateGameLogic();
  },
  undefined,
  UPDATE_PRIORITY.NORMAL,
); // 0

// 低优先级:渲染后的 UI 更新
ticker.add(
  () => {
    updateDebugPanel();
  },
  undefined,
  UPDATE_PRIORITY.LOW,
); // -25

UPDATE_PRIORITY 的值越大越先执行。Pixi.js 内部用这个机制确保 renderer.render() 在所有用户逻辑之后执行——先更新状态,再画到屏幕上。

换句话说,一帧内的执行顺序是:高优先级回调(物理、AI)→ 普通优先级回调(游戏逻辑)→ 渲染更新 → 低优先级回调(调试面板)。


四、帧率控制:maxFPS 和 minFPS

Ticker 支持限制最大帧率和设定最小帧率阈值:

const ticker = Ticker.shared;

// 限制最大帧率为 30 FPS
ticker.maxFPS = 30;
// rAF 仍然以 60 FPS 触发,但 Ticker 会跳过偶数帧
// 适用于不需要高帧率的场景(如策略游戏),减少 GPU 负载

// 设定最小帧率
ticker.minFPS = 15;
// 当帧率低于 15 FPS 时,deltaTime 会被钳制(clamped)
// 防止因单帧耗时过长导致物体"穿墙"

minFPS 的作用经常被忽略。假设某一帧因为 GC 暂停了 200ms,如果不钳制 delta,这一帧的 deltaTime 会是 12(200ms / 16.67ms),物体会一下子跳很远。设了 minFPS = 15,delta 最大只会是 4(约 66ms),避免了极端跳跃。

问题的关键在于——maxFPS 不是精确的帧率控制。因为 rAF 本身不保证固定间隔,Ticker 的帧率限制是”尽力而为”式的。实际帧率可能在目标值附近波动。如果需要精确的固定时间步进(fixed timestep),需要自己在 Ticker 回调中实现累加器模式。


五、暂停与恢复:Ticker 的生命周期

Ticker 支持暂停和恢复,适合游戏暂停、切换标签页等场景:

// 暂停 — 停止 rAF 循环
ticker.stop();

// 恢复 — 重新启动 rAF 循环
ticker.start();
// 恢复时 delta 会从暂停前的时间点重新计算
// 不会出现一次性补偿大量 delta 的问题

v8 的 Application 还支持在页面不可见时自动暂停 Ticker:

await app.init({
  autoStart: true, // 创建后自动启动 Ticker
  sharedTicker: false, // 使用独立 Ticker 而非 Ticker.shared
});

这里有一个实际开发中常见的坑——Ticker.shared 是全局单例。如果你的应用有多个 Pixi 实例或需要独立控制帧率,应该创建独立的 Ticker 实例(new Ticker()),而不是都往 Ticker.shared 上注册。否则暂停一个游戏会把所有实例的动画都停了。


总结

Pixi.js 的 Ticker 不只是 rAF 的封装。它提供了三层核心能力:标准化时间步长(deltaTime 消除帧率波动影响)、回调优先级(保证物理→逻辑→渲染的执行顺序)、帧率控制(maxFPS 降频省资源,minFPS 防止极端跳跃)。动画逻辑记得乘以 delta,这一条就能解决 90% 的帧率相关问题。


本系列其他文章:

延伸阅读:

share.ts

// 觉得这篇文章有帮助?

// 欢迎分享给更多人

export const  subscribe  =  "/rss.xml" ;