如何在 Phaser 3 中加载精灵图
Phaser 是三者中的异类。Godot 和 Unity 的切图器长在编辑器里; Phaser 只有两个加载函数和一个配置对象,图是在代码里描述的。 这让它成了三者中唯一原生就能读紧凑图集的—— 也是唯一一个「最常见的故障是什么都没加载出来,而控制台并不明说为什么」的。
0. 用 HTTP 提供它,否则下面全都不成立
把这条放最前面,是因为它耗掉的时间比这一整页其他内容加起来还多。 Phaser 通过网络请求资源,而浏览器在 file:// URL 下会拒绝跨源请求。双击打开 index.html, 每一张图都会失败,画布保持空白或者显示绿框, 而控制台的消息是关于 CORS 的,不是关于你那张图的。
在项目文件夹里随便跑一个本地服务器——npx serve、python -m http.server、 Live Server 扩展,或者你的打包器本来就带的那个—— 然后访问 http://localhost:…。
如果某个资源还是不出现,浏览器的 Network 面板比 Phaser 更快给你答案: 404 意味着路径是相对页面错的,而不是相对源文件错的。
1. 两个加载器,由图本身决定用哪个
后面所有事情都跟着这个选择走,包括用哪个函数来生成动画帧。
load.spritesheet
均匀网格——每一帧一样大
你给它一个格子的像素尺寸。帧按从左到右、从上到下编号,从 0 开始。 不涉及第二个文件。
load.atlas
紧凑图集——帧大小不一
你给它一个 PNG 和一个描述每帧位置的 JSON 文件。帧按名字引用,而不是按下标。
还有更多:load.atlasXML 对应 Starling 和 Adobe 的格式,load.unityAtlas 对应 Unity 的,load.multiatlas 对应拆在好几张图上的图集。 它们是同一个思路配不同的解析器。
2. load.spritesheet —— frameWidth 是尺寸,不是数量
这是最常见的一个错误,而它源自和其他引擎的命名冲突。 Unity 的 Sprite Editor 提供「Grid by Cell Count」,你填的是8 列。 Godot 的切图对话框要的是8 和 4。 Phaser 要的是一帧的像素尺寸。 一张 256×128、每帧 32px 的图,填的是 32 和 32, 不是 8 和 4。
function preload () {
this.load.spritesheet('hero', 'assets/hero.png', {
frameWidth: 32,
frameHeight: 32,
margin: 0, // 网格外围的一圈边距
spacing: 0 // 相邻帧之间的空隙
})
}margin 和 spacing 存在, 是因为导出器常在格子之间留一两个像素,防止 GPU 跨帧采样。 图有留白而你没填这两个值,网格就会每往右一列偏得更多一点—— 检查最后一帧,不是第一帧,因为第 0 帧无论如何都看着是对的。
startFrame 和 endFrame 能把加载限制在图的一部分上,但很少有理由这么做; 全部加载进来、每段动画各自挑下标,更简单。
如果这张图不是均匀网格,这个加载器描述不了它。那是第 4 节的事。
3. 把动画建出来
加载发生在 preload 里,其余一切发生在 create 里。 在 preload 里创建精灵必定得到一个「贴图不存在」的错误, 因为文件还没到。
function create () {
this.anims.create({
key: 'run',
frames: this.anims.generateFrameNumbers('hero', { start: 0, end: 7 }),
frameRate: 10,
repeat: -1 // -1 是无限循环;默认的 0 只播一次
})
const hero = this.add.sprite(160, 120, 'hero')
hero.play('run')
}repeat: -1 是最容易漏掉的一个。默认值是 0, 意思是播一次然后停在最后一帧—— 而当你期待的是一个走路循环时,这看起来就是「动画坏了」。
帧下标按行优先、从 0 开始。在一张 8 列宽的图上,第 2 行是第 8 到第 15 帧。 这里搞错了,结果就是一段帧数对、行数错的动画。
动画是全局的,不是每个场景各一份。this.anims 是整个游戏唯一的 Animation Manager。 重启场景后,第二次用同一个 key 调 anims.create 会警告并且什么也不做。加个保护:
if (!this.anims.exists('run')) {
this.anims.create({ /* … */ })
}图集则把 generateFrameNumbers 换成 generateFrameNames,它拼名字而不是数数:
frames: this.anims.generateFrameNames('hero', {
prefix: 'run_',
start: 1,
end: 8,
zeroPad: 4, // run_0001.png
suffix: '.png'
})这些名字必须和图集 JSON 里的完全一致,补零位数也算——zeroPad 差一位,就会得到一段空动画,而且不报任何错。
在写代码之前先把 frameRate 定下来。 10 是个合理的默认值,8–12 覆盖大多数像素画, 但那个读起来对的数字是看出来的,不是算出来的。精灵动画预览器能在浏览器里以任意 FPS 播放这张图, 这样你只用把答案填一次,而不是改一次刷新一次。
4. 紧凑图集——Phaser 做得到而另外两个做不到的事
Godot 根本没有图集 JSON 的导入器。Unity 只有它自己格式的那一个。 Phaser 能读大多数打包器输出的 TexturePacker JSON, Array 和 Hash 两种排布都行,两个参数搞定:
this.load.atlas('hero', 'assets/hero.png', 'assets/hero.json')那个格式带的不只是位置——trimmed、spriteSourceSize 和 sourceSize 让打包器可以把每一帧的透明边裁掉,再由 Phaser 在绘制时补回去, 于是一张大半是空白的图能小很多,而精灵不会移位。
我们的 精灵图生成器 就导出这种格式。把清单格式设成 TexturePacker(JSON Array)——默认就是它—— 下载 PNG 和 JSON,上面那个两参数的调用就是全部的集成工作。 这时打开裁边也是安全的: 清单记下了裁掉多少,Phaser 会补回去,图变小了而画面上什么都没动。
如果你的清单是别的形状——自己写的导出器,一种没人解析的格式—— 你也不需要转换器。Phaser 可以直接吃帧矩形:
function preload () {
this.load.image('hero', 'assets/hero.png')
this.load.json('heroFrames', 'assets/hero.json')
}
function create () {
const texture = this.textures.get('hero')
for (const f of this.cache.json.get('heroFrames').frames) {
texture.add(f.filename, 0, f.x, f.y, f.w, f.h)
}
// 这些帧现在可以按名字寻址了,和真正的图集一样
this.add.sprite(160, 120, 'hero', 'idle_01.png')
}十二行,不需要构建步骤。它表达不了的是裁边—— 没有地方能说明某一帧被裁过——所以走这条路就别裁边打包。
有一句话值得直说:把图打包好,别发一堆散 PNG。六十次 load.image 就是六十个请求、六十张 GPU 贴图, 而每一次切换贴图的绘制都会打断合批。 一张图是一个请求、一张贴图,Phaser 能把整个场景合批。 如果你的帧现在是分开的文件,那正是精灵图生成器要干的事; 如果别人扔给你一张大图而你需要把帧取出来,精灵图切割工具走的是相反的方向。
5. 让像素画保持锐利
一个配置项干了大半的活。pixelArt: true 会在整个游戏范围内关掉贴图平滑, 这相当于 Godot 的 Nearest 过滤和 Unity 的 Point 过滤模式在 WebGL 里的对应物。
const config = {
type: Phaser.AUTO,
width: 320,
height: 180,
zoom: 4, // 整数 —— 屏幕上是 1280x720
pixelArt: true,
roundPixels: true,
scene: { preload, create }
}按小分辨率设计,然后放大。一个 320×180 的游戏世界在 zoom: 4 下填满 1280×720, 每个像素正好是四个屏幕像素见方。 改成 width: 1280 再把精灵放大 4 倍, 得到的是同一幅画面、到处更难看的数字, 以及大得多的、一不小心就落到小数倍上的空间。
roundPixels 强制精灵绘制在整数像素位置上。 不开它,一个 x = 100.5 的精灵会跨两列像素渲染,移动时看上去在闪—— 动起来看得见,截图里看不见。
留意 Scale Manager。Phaser.Scale.FIT 会把画布拉到填满窗口, 而且很乐意停在 2.37 这样的比例上,把上面做的一切都撤销掉。 像素画要么关掉缩放、挑一个固定 zoom, 要么用 FIT 并接受锐利度只是近似的。没有一个设置能两者兼得。
另外检查 setScale 调用和摄像机 zoom 里有没有非整数—— 一个把精灵从 1 缓动到 1.2 的补间,会经过中间每一个小数值。
常见故障
画布空白,控制台里全是 CORS 错误
页面跑在 file:// 上。用 HTTP 提供这个文件夹。
本该是精灵的地方是绿框
那是 Phaser 的「贴图缺失」占位。key 写错了,或者文件 404 了—— 查代码之前先看 Network 面板。
整张图变成了一个巨大的精灵
frameWidth 和 frameHeight 没填,加载器把这张图当成了单独一帧。
帧有偏移,而且越往后越偏
这张图有没被声明的间距或边距。误差是累积的——去检查最后一列。
动画播一次就停了
repeat 默认是 0。设成 repeat: -1。
重启场景后提示动画 key 已存在
动画活在全局管理器上。把创建包在一个 anims.exists 检查里。
图集动画是空的,而且不报错
generateFrameNames 拼出来的名字在 JSON 里不存在—— 通常是 zeroPad 或者 suffix 的问题。 打印 this.textures.get(key).getFrameNames() 对一下。
像素画是糊的
游戏配置里少了 pixelArt: true。
精灵移动时在闪
位置或缩放是小数。打开 roundPixels, 并用整数 zoom 代替 Scale.FIT。