AI 参与说明(Agent:Codex):本文由 Codex 根据 PixiJS 官方文档、8.22.0 版本 API 与源码辅助调研、撰写和校验,资料整理于 2026-10-13。技术栈分工、场景选择和实施顺序是本文的工程建议,中文正文使用
humanizer-zh编辑。运行记录:模型gpt-6.1-sol,reasoning effortultra,执行入口 Codex Desktop,提供方openai,CLI 版本0.162.0-alpha.17.2(不代表桌面 App 版本)。
PixiJS 适合承担网页版游戏的 2D 画面与画布交互:它让前端开发者用 JavaScript 或 TypeScript 组织角色、地图和动画,并通过浏览器图形接口绘制。 菜单和表单可以继续用熟悉的网页技术;游戏规则、关卡切换、碰撞和联机机制则需要应用自行组织。
它提供对象管理与渲染机制,让开发者专注于“这些对象如何变化”。如果需要一套已经组合好相机、物理、音频和关卡生命周期的游戏框架,可以接着阅读 Phaser 与 PixiJS 对比。本文围绕 PixiJS 本身展开。
本文使用 PixiJS 8.22.0 作为示例基线;核验日官方 Releases 将其列为最新稳定版。版本可能继续变化,复现时应保留示例版本,升级时再对照相应 API。官方 Releases · 8.22.0 API
先认识本文的术语
| 英文术语 | 中文名称 | 简要解释 |
|---|---|---|
| Rendering Engine | 渲染引擎 | 把对象及其显示属性转换成画面,PixiJS 的核心定位 |
| Renderer | 渲染器 | 执行具体绘制的组件,可以使用不同浏览器图形接口 |
| Scene Graph | 场景图 | 组织显示对象的父子关系,以及位置、缩放、透明度等属性 |
| Container | 容器 | 把多个对象组合起来,移动父对象时子对象跟着移动 |
| Sprite | 精灵 | 用纹理显示角色、卡牌、道具等图像的对象 |
| Texture | 纹理 | 对象绘制时使用的图像信息,可以对应一张图中的某个区域 |
| Graphics | 图形对象 | 保存矩形、圆和路径等形状数据的显示对象 |
| Ticker | 帧更新器 | 在每帧调用更新函数,并提供时间间隔 |
| Batching | 批量绘制 | 把兼容的绘制任务合并,减少向图形接口提交的次数 |
| Draw Call | 绘制调用 | 向图形接口提交的一次绘制操作 |
| WebGL / WebGPU | Web 图形接口 | 浏览器访问 GPU 的两套接口,支持条件和实现不同 |
产品名 PixiJS 没有必要另造中文译名。表中的中文名称帮助识别概念,正文继续使用官方英文名称。
PixiJS 在网页游戏技术栈中的位置
PixiJS is a 2D rendering engine, not a complete game engine. 官方 FAQ 明确区分了两者。它也不是 React、Vue 那样负责一般网页界面组织的框架。PixiJS FAQ
网页版游戏依然是网页:页面需要加载资源,接收鼠标、触摸和键盘输入,显示界面,保存进度,有时还要与服务器通信。PixiJS 主要接管其中需要连续绘制的区域。
| 层次 | 在一个网页版游戏中做什么 | PixiJS 的职责 |
|---|---|---|
| 页面与 DOM 界面 | 登录、菜单、设置、背包列表、文字输入、页面导航 | 可与这些界面组合;不替代它们的全部职责 |
| 游戏状态与规则 | 血量、回合、移动目标、胜负、物品使用条件 | 读取状态并表现结果;规则由应用编写 |
| 2D 画面 | 地图、角色、动画、选中效果、画布内交互 | 核心职责 |
| 游戏运行系统 | 关卡生命周期、碰撞、寻路、音频控制 | 根据需求自行实现或选择其他库 |
| 网络与持久化 | 房间、存档、服务器判定、消息同步 | 不提供完整联机协议或服务端运行方案 |
PixiJS 提供的基础能力包括 Application、Scene Graph、Assets、Ticker、指针事件、纹理、文字、遮罩与 Filters。Application 把初始化与常用更新机制组合成一个入口;这比直接编写 WebGL 绘制代码省事得多。Architecture · Application
但“能绘制对象”与“管理游戏全过程”是两件事。例如离开战斗后,要恢复探索状态、关闭战斗音效、释放专属素材,并保留玩家进度。Scene Graph 只描述显示对象关系,不会替应用完成这些规则。官方生态提供独立的 Sound、UI、Spine 等扩展,按需求接入即可。Ecosystem
前端开发者最容易理解的一种组合
下面是本文建议的职责划分,不是 PixiJS 强制规定的架构:
flowchart TD
Input["玩家输入"] --> DOM["DOM 界面<br/>菜单、设置、表单"]
Input --> Events["PixiJS Events<br/>画布内点击与触摸"]
DOM --> Commands["操作指令"]
Events --> Commands
Commands --> State["游戏状态与规则<br/>回合、移动、胜负"]
State --> View["PixiJS Scene Graph<br/>角色、地图、效果"]
State --> UI["DOM 界面状态<br/>分数、物品列表"]
View --> Renderer["Renderer"]
Renderer --> Canvas["Canvas 画面"]
以一个小镇模拟为例,居民的身份、位置、任务和行程放在游戏状态里;PixiJS 根据这些数据移动 Sprite、播放动画和显示选中效果。设置面板、任务列表和聊天输入继续使用 DOM。居民改变目的地是一条业务事件,无须把每一帧的画面变化都当成页面状态更新。
与 React 配合的两种方式
一种方式是在组件挂载后创建 PixiJS Application,把 app.canvas 放入一个 DOM 容器;React 管页面,PixiJS 的 Ticker 管连续变化的对象。另一种方式是使用官方生态的 @pixi/react,用 React 的声明式组件描述 PixiJS 对象。PixiJS React 官方仓库
@pixi/react renders PixiJS objects through React; ordinary Sprite and Graphics objects are not DOM elements. 因此普通 Sprite 不能直接套用 CSS Flexbox;样式、布局和事件应按对象所在的系统处理。PixiJS 的专用 DOM 整合功能另有适用范围,不能据此把所有显示对象都视为网页节点。
本文建议把高频的位置、速度和动画更新放在游戏循环中,把菜单开关、回合结束、物品变化等界面事件传给 React。这样能减少不必要的界面更新,也更容易分清由谁管理各类状态。这是一种组织方式,不是“React 不能做动画”或“必须绕过 React 才能快”的结论。
无论选哪种方式,都应做好离开页面后的清理,并处理组件在异步 init() 结束前已经卸载的情况。服务端可以输出菜单和画布占位;创建浏览器画布与 Renderer 的过程放在客户端。
原理一:保留对象,再改变对象的属性
PixiJS 使用 Scene Graph 保存显示对象。app.stage 是根 Container,可以在下面放地图层、角色层和效果层;角色本身也可以是一个 Container,里面包含身体、装备和名字。Scene Graph
假设 world.x = 100,其子对象 hero.x = 20;没有其他变换时,角色出现在横坐标 120。把 world 缩放或旋转,子对象也会随之变化。因此平移整个世界可以表现镜头移动,单独移动角色则改变它在世界里的位置。实际项目还要处理视口边界、缩放中心和镜头跟随规则。
这个结构与 DOM 的父子层级有相似之处,但普通 PixiJS 对象由 Renderer 绘制,浏览器不为每个角色单独创建一个 HTML 节点。
Sprite、Texture 与 Graphics 分别保存什么
Sprite 保存位置、缩放、朝向、颜色等显示属性,并引用 Texture;Texture 可以对应整张图片,也可以引用同一图像中的一个区域。多个角色可以共用图像资源,而拥有各自的位置和动画状态。Textures
例如把角色的多个动作帧打包到一张图,再通过帧信息选择不同区域,就可以组织逐帧动画。这种资源组织常称为 Texture Atlas;在 PixiJS 中可以通过 Spritesheet 管理。资源共享减少重复加载,也有利于合并兼容的绘制任务,但不保证整张地图只需要一个 Draw Call。
Graphics 记录形状及其填充、描边数据。GPU 后端把这些形状转换为可绘制的几何数据;它的 rect().fill() 虽然看起来接近 Canvas 2D 的画图操作,使用方式却是创建对象后持续保留它。Graphics
一个方块建好后,只改变 x、y 就可以移动。若形状没有变化,不必每帧 clear() 再画同一个矩形。经常重建复杂形状,会增加几何处理的工作量。
原理二:每帧更新,再把画面交给 Renderer
下面描述的是采用 WebGL 或 WebGPU 时的概念流程,省略了具体缓存和内部优化:
flowchart TD
RAF["requestAnimationFrame<br/>浏览器安排下一帧"] --> Ticker["Ticker callbacks<br/>执行应用更新"]
Ticker --> State["更新游戏状态<br/>位置、动画、计时"]
State --> Graph["更新 Scene Graph<br/>变换与显示属性"]
Graph --> Batch["准备渲染数据与 Batching"]
Batch --> API["WebGL 或 WebGPU<br/>提交 Draw Call"]
API --> GPU["GPU 绘制到 Canvas"]
The default Application loop uses a Ticker driven by requestAnimationFrame. 在 Ticker 回调中修改对象后,Application 的渲染机制绘制更新后的画面;一般不需要在同一循环里再额外调用一次 app.render()。Render Loop
这里的 GPU 主要承担绘制工作。角色应去哪、能否穿过墙、这次攻击是否命中,并不会因为使用 WebGL 就自动转为 GPU 计算。大量寻路、业务对象遍历或临时对象分配,仍可能占用 JavaScript 主线程。
Ticker 提供时间信息,不保证固定帧率
PixiJS v8 的 Ticker 回调接收 Ticker 实例。deltaMS 是经过时间限制和 speed 缩放的毫秒间隔;deltaTime 是无量纲的缩放值,不能把它直接当作毫秒。实际帧率受设备、页面负载和浏览器调度影响。Ticker 8.22.0 API
按“每帧向右移动 3 像素”写代码,速度会随帧率变化。按“每秒移动 180 个逻辑像素”计算,再乘以时间间隔,更适合简单动画。涉及物理、回放或联机判定时,本文建议另外设计固定步长或明确的模拟时钟,并把显示更新与模拟更新区分开;Ticker 本身不保证可复现的游戏结果。
Batching 为什么能减少开销
多个兼容对象可以合并提交,减少 CPU 与图形接口之间的调用。实际能否合并,取决于绘制顺序、纹理、Blend Mode、遮罩和 Filters 等条件。在保持画面正确的前提下,合理组织对象与资源有利于 Batching;随意重排透明对象可能改变叠加结果。Performance Tips
v8 的 Render Groups 还可以为较大的稳定子树组织渲染工作。不过将每个对象都拆成独立 Render Group,可能失去跨组的 Batching 机会。应根据实际场景使用,不把它当作必须开启的性能开关。Render Groups
原理三:画布输入需要坐标与命中检测
PixiJS 的事件系统把鼠标和触摸等输入转换为对象交互,例如 pointerdown、pointerover。eventMode 控制对象是否参与交互;hitArea 可以明确指定命中区域。命中检测解决的是“点中了谁”,不是游戏中的物理碰撞。Events / Interaction
这里有三个容易混淆的位置:浏览器窗口里的位置、PixiJS 的全局位置,以及某个 Container 内的局部位置。若地图已经平移或缩放,直接把 event.global.x 写进地图内角色的 x,目标通常会偏移。应使用 world.toLocal(event.global) 转换到角色所在的坐标系,再交给规则代码。
DOM 菜单和画布也需要明确的输入边界:打开背包时,是否仍允许角色移动;输入聊天文字时,方向键是否仍控制镜头。这些是应用规则,不能只靠给画布绑定几个事件完成。
WebGL、WebGPU 和 Canvas 后端如何选
以下能力按 8.22.0 版本核验,不能泛化为所有 v8 小版本。 该版本 API 将 WebGL 标为推荐后端,将 WebGPU 和 Canvas 标为 Experimental。Canvas 后端已经存在,但功能支持有限,需要另外检查所用效果;官网部分滚动指南仍保留了较早的“Coming-soon”描述。8.22.0 Rendering API · CanvasRenderer API
8.22.0 的默认候选顺序是 WebGL → WebGPU → Canvas。preference 的字符串形式调整优先顺序;数组形式限定候选范围。例如:
// Prefer WebGL, while retaining other fallback candidates.
await app.init({ preference: 'webgl' });
// Try only WebGL; fail when it is unavailable.
await app.init({ preference: ['webgl'] });
两段是初始化配置的替代写法,选择其中一种使用,不要对同一个 Application 连续初始化。以上行为依据固定版本的 autoDetectRenderer 源码。候选后端的检测与选择不代表任意初始化错误都能自动恢复,也不代表切换后端后所有效果都保持一致。
面向普通网页版游戏,可以先采用 WebGL,再按目标浏览器和实际场景评估 WebGPU。WebGPU 是另一套图形接口,不会自动消除 JavaScript 更新、资源解码或业务算法的瓶颈;不能仅凭接口名称认定游戏一定更快。PixiJS v8 发布说明
哪些网页版游戏适合 PixiJS
以下是根据职责边界作出的场景判断,不是官方性能承诺。
| 场景 | PixiJS 能发挥作用的部分 | 选型时继续确认什么 |
|---|---|---|
| 卡牌、棋盘、消除与休闲小游戏 | 对象排布、拖拽、翻牌与消除动画、视觉效果 | 规则能否独立实现;简单界面是否用 DOM 就足够 |
| 小镇、农场、经营模拟 | 地图、角色、装饰、状态变化的连续表现 | 寻路与模拟开销、资源规模、对象生命周期 |
| 2D 地图或关卡编辑器 | 大量对象的显示、选择、平移与缩放 | 撤销、保存、坐标转换、视野外对象管理 |
| 教学、互动广告、轻量互动内容 | 把可操作画面嵌入普通网页 | 首次加载、触摸体验、文本与无障碍需求 |
| 平台动作或即时战斗 | 角色与效果的渲染 | 是否愿意承担物理、相机、关卡管理等系统 |
PixiJS 支持游戏之外的互动内容,官方 Introduction 也列出教育和可视化等用途。因此采用它的理由可以是“需要一块容易嵌入网页的 2D 互动区域”,不必先把产品定义为传统游戏。Introduction
若需求主要是表单、列表、可选中文字和标准控件,优先使用 DOM;若只有少量简单图形,原生 Canvas 2D 或 SVG 也值得评估。若主线是真正的 3D 模型、相机与光照,通常应评估 Three.js 等 3D 方案。若项目需要成熟的游戏编辑器和大量现成游戏系统,选择完整引擎或游戏框架可能减少自行组合的工作。这些取舍取决于需求,而不是框架名气。Three.js 与 Blender 入门
一个完整示例:DOM 控制,游戏状态驱动画面
先打开官方 Container 示例 和 PixiJS Examples,可以直接观察对象与动画,无须安装环境。下面给出完整源文件,便于对照职责分工和复现示例,不是性能基准。
示例采用 JavaScript ES Module 和 PixiJS 8.22.0,不加载图片素材。点击深色区域设置移动目标,绿色方块向目标移动;DOM 按钮负责暂停与重置。游戏状态保存位置,Ticker 更新状态,Graphics 只负责表现。世界 Container 特意偏移了 40 像素,便于观察坐标转换的作用。
保存为 pixi-game.html,通过 HTTP 静态服务打开;需要启用 JavaScript、支持 WebGL,并能访问示例中的 CDN。使用现有开发环境的静态预览服务即可。
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PixiJS: state and rendering</title>
<style>
body { margin: 24px; font-family: system-ui, sans-serif; }
canvas { display: block; margin-top: 12px; }
</style>
</head>
<body>
<p>点击深色区域设置目标,方块会向目标移动。</p>
<button id="pause" disabled>暂停</button>
<button id="reset" disabled>重置</button>
<p id="status" role="status">加载中……</p>
<div id="game"></div>
<script type="module">
import { Application, Container, Graphics, Rectangle } from
'https://cdn.jsdelivr.net/npm/pixi.js@8.22.0/dist/pixi.min.mjs';
const app = new Application();
const pause = document.querySelector('#pause');
const reset = document.querySelector('#reset');
const status = document.querySelector('#status');
async function main() {
await app.init({
width: 640, height: 400,
background: '#e2e8f0',
preference: ['webgl'],
resolution: Math.min(window.devicePixelRatio || 1, 2),
autoDensity: true,
});
document.querySelector('#game').appendChild(app.canvas);
const world = new Container();
world.position.set(40, 40);
app.stage.addChild(world);
const ground = new Graphics().rect(0, 0, 560, 320).fill(0x172231);
const marker = new Graphics().circle(0, 0, 5).fill(0xfbbf24);
const actor = new Graphics().rect(-12, -12, 24, 24).fill(0x68d391);
world.addChild(ground, marker, actor);
ground.eventMode = marker.eventMode = actor.eventMode = 'none';
world.eventMode = 'static';
world.hitArea = new Rectangle(0, 0, 560, 320);
world.cursor = 'pointer';
const state = {
x: 80, y: 160, targetX: 80, targetY: 160, paused: false,
};
function syncView() {
actor.position.set(state.x, state.y);
marker.position.set(state.targetX, state.targetY);
}
syncView();
world.on('pointerdown', (event) => {
const point = world.toLocal(event.global);
state.targetX = Math.max(12, Math.min(548, point.x));
state.targetY = Math.max(12, Math.min(308, point.y));
syncView();
status.textContent = `目标:${state.targetX.toFixed(0)}, ${state.targetY.toFixed(0)}`;
});
app.ticker.add((ticker) => {
if (state.paused) return;
const dx = state.targetX - state.x;
const dy = state.targetY - state.y;
const distance = Math.hypot(dx, dy);
if (distance === 0) return;
const dt = Math.min(ticker.deltaMS, 50) / 1000;
const step = Math.min(distance, 180 * dt);
state.x += dx / distance * step;
state.y += dy / distance * step;
syncView();
});
pause.disabled = reset.disabled = false;
status.textContent = '就绪';
pause.addEventListener('click', () => {
state.paused = !state.paused;
pause.textContent = state.paused ? '继续' : '暂停';
status.textContent = state.paused ? '已暂停' : '移动已继续';
});
reset.addEventListener('click', () => {
Object.assign(state, {
x: 80, y: 160, targetX: 80, targetY: 160, paused: false,
});
pause.textContent = '暂停';
status.textContent = '已重置';
syncView();
});
}
main().catch((error) => {
status.textContent = '初始化失败,请检查网络和 WebGL 支持。';
console.error(error);
});
</script>
</body>
</html>
这段例子有几个值得对照的地方:
await app.init()是 v8 的异步初始化方式;Renderer 就绪后再插入app.canvas。Applicationworld.toLocal()将点击转换为世界内的位置;去掉这一步,偏移后的目标会与点击不一致。state是普通 JavaScript 数据,没有把 Graphics 当作完整游戏状态。以后加入存档或规则测试时,可以继续围绕数据组织。- 每秒 180 个逻辑像素是示例速度;单次使用的时间上限为 50 毫秒,用于避免卡顿后大幅跳动。这会丢弃部分时间,不适合直接用作权威模拟时钟。
- 暂停只跳过移动更新,Application 继续绘制,便于显示新目标;这不是节能模式。完全静止的页面可以另行停止 Ticker,状态变化时再按需绘制。
验收时分别点击左右和斜方向,确认方块停在黄色标记中心;点击边缘,确认方块不会越出世界;暂停后设置新目标,再继续移动;重置后确认方块回到初始位置。本文示例已于 2026-10-13 在 Chromium 151.0.7922.34 的 WebGL 路径实际运行,点击坐标转换、横向与斜向移动、暂停与继续、边缘限制和重置通过检查,未发现页面脚本错误。这些结果不代表其他浏览器或大型游戏负载的表现。
从小样走向完整游戏,还要补齐什么
本文建议先完成一条真实玩法流程,例如“点击地块 → 移动 → 消耗资源 → 更新背包 → 保存 → 重新进入后恢复”,再按需要加入系统。
| 工作 | 具体产物与通过标准 |
|---|---|
| 明确状态拥有者 | 用数据表达规则;保存并恢复后,画面能从状态重建 |
| 组织关卡生命周期 | 进入、暂停、退出和重建各有入口;离开后没有遗留的更新回调与事件监听 |
| 管理资源 | 记录哪些资源共用、哪些只属于当前关卡;切换关卡时不误销毁共用 Texture |
| 划分输入 | 菜单、文字输入与游戏操作互不误触;镜头平移后仍能准确选择对象 |
| 适配画面 | 区分逻辑尺寸、CSS 显示尺寸和像素密度;调整窗口后布局和点击仍对应 |
| 处理音频与网络 | 按实际需求接入;联机时明确谁判定规则、如何处理迟到或重复的消息 |
| 用真实负载检查性能 | 使用计划中的地图、对象和效果,测更新、绘制、加载与内存,再定位瓶颈 |
Assets 提供加载与缓存入口,但应用仍需决定何时加载、重用与卸载。销毁显示对象与销毁共用 Texture 是不同操作;不要为了清空一个关卡,把其他关卡还在使用的资源一起销毁。Assets · Garbage Collection
普通 Text 频繁改变、复杂 Graphics 反复重建、大面积 Filters 和过高的渲染分辨率,都可能增加成本。像素密度提高后,渲染像素数也会增加;可以按设备能力限制 resolution,但上限应由画质与性能测量决定。对象数量本身不足以判断性能,仍要看可见范围、变化频率与效果复杂度。Performance Tips
普通 Sprite 和 Graphics 显示在画布上,也不会自动具备 HTML 表单、文字选择和完整键盘操作的行为。重要文字输入和标准操作可以保留在 DOM;需要画布无障碍支持时,再核对 PixiJS 提供的 Accessibility 功能,并实际检查操作流程。Accessibility
建议的学习顺序
先理解 Application、Container、Sprite 和 Texture,能把对象放上画布、移动并正确处理局部坐标;再学习 Ticker、事件和资源加载,完成一个可以暂停、退出、重新进入的小样。接着按玩法需求补充动画、地图、音频或碰撞,最后针对测到的瓶颈研究 Batching、Render Groups、Filters 和更复杂的后端能力。
如果最吸引人的是“把 2D 互动区域放进一个现有网页”,PixiJS 的职责边界很合适。如果开发时间主要花在补齐相机、关卡、物理和音频管理,应该重新比较完整游戏框架能省下多少工作,而不必坚持把所有系统都自己拼起来。
关联阅读
- Frontend 工具链专题:查看其他网页图形与开发工具。
- Phaser 与 PixiJS:Web 2D 框架的能力、开发状态与选型:比较两者承担的游戏职责。
- Three.js 与 Blender 入门:适用场景、3D 基础概念与协作流程:理解网页 3D 的对象与素材分工。