图集与包体优化

3.1 图集的合并原则

Creator 3.8 提供两条合图路径:一条是 TexturePacker 这类外部工具产出 plist + png,导入后是 Atlas 资源;另一条是编辑器内置的自动图集(Auto Atlas),在资源管理器里新建一个 auto-atlas.pac 文件,它会自动把当前文件夹下的所有 SpriteFrame 当作碎图。

自动图集的优势是它跟着文件夹走。你把一批图标丢进 icons/ 目录,建一个 pac 文件,构包时自动合并。美术新增一个图标,不用改任何配置。

外部 TexturePacker 的优势是控制力精细:Trim mode、Padding、Extrude、允许旋转,这些都能精确指定。自动图集也提供大部分参数,但多图集、特殊排布做不了。

我的合并原则是按"使用时机和场景"分组,而不是按"资源类型"分组。UI 图标一个图集,关卡瓦片一个图集,无人机序列帧一个图集。这么分是因为图集有个隐性代价:加载了一张图集,它里面所有的小图都进显存了。你把 40 个散落在三个关卡里的图标塞进一个"通用"图集,玩家在第一关只用到 3 个,但显存里躺着 40 张。所以图集要细。

我把自动图集的这些剔除项都勾上了:剔除未使用的图片、剔除在 Bundle 内未被使用的 Texture2D 资源、剔除在 Bundle 内未被使用的图片资源、剔除在 Bundle 内未被使用的 Sprite 图集资源。注意这些只在构建后生效,预览时不生效。

这些剔除项之所以重要,是因为图集放在 Bundle 目录下的时候,除了正常生成图集资源以外,也会同时生成原始 spriteFrame 的 texture 以及 image 资源。官方文档在 Asset Bundle 的 FAQ 里专门提了这条,说这样会增大包体,构建时会有警告提示。

3.2 常见坑:图集过大、边缘采样、重复资源

图集过大是最容易犯的错。我的第一版把 assets/ui 整个目录合成一张 2048×2048 的图集,构建日志警告说图集超出了移动设备支持的最大纹理尺寸,Creator 把它切成多张,运行时表现正常但包体没省下来。现在的规矩是单张图集不超过 1024×1024,超出之后纹理内存占用是阶梯式上涨的。

边缘采样是另一个坑,表现为图集里某张图旁边出现别的图的边缘像素。原因是两张图紧挨着,在某些机型上双线性采样会插值到邻图的颜色。两个解决办法:给图集留间距(自动图集的"间距"参数,我设 4 像素),或者打开"扩边"。扩边这个功能官方文档里也叫 Extrude,在碎图的边框外扩展出一像素外框,并复制相邻碎图像素到外框中。

重复资源则更隐蔽。同一个图标文件被放在两个目录里,如果你在不同目录放了同名同图的两个副本,它就是两个不同的资源,各占一份图集空间。我做过一次全项目排查,方式是看构建产物里每个 bundle 的 config.json,把资源清单导出来按名字排序,人工扫一遍重名项。48 个 UI 图标里有 3 个是重复的。

3.3 纹理压缩预设与目标平台匹配

压缩纹理在 项目设置 -> 压缩纹理 面板里配置预设,然后在图片资源的属性检查器里勾 useCompressTexture、选 presetId。presetId 是按平台分组配置的:Web(包括 Web-Mobile 和 Web-Desktop)、iOS、Mini Game(包括 Creator 支持的所有小游戏平台)、Android。

各平台支持的压缩格式,官方文档给了一张表(我只留了用得上的几个):

平台 支持的 GPU 压缩格式
Web Mobile ASTC / ETC1 / ETC2 / PVR / WEBP
微信小游戏 ASTC / ETC1 / ETC2 / PVR
OPPO 小游戏 ETC1
vivo 小游戏 ETC1 / ASTC
华为快游戏 ETC1
iOS ASTC / ETC1 / ETC2 / PVR / WEBP
Android / 华为 AGC ASTC / ETC1 / ETC2 / WEBP

注意 OPPO 和华为快游戏只支持 ETC1。目前小游戏平台的通用配置里加 ASTC 暂时不支持 vivo,但可以用平台覆盖配置让它支持。

关于 ASTC,官方文档的推荐是:需要 GPU 压缩纹理的项目推荐用 ASTC,因为它压缩率高、质量好、还支持 HDR,2012 年发布至今在移动端接近 100% 支持率。画质要求高的项目,2D 元素和 UI 用 astc 5x5,模型纹理用 astc 6x6;画质要求低的,2D 元素和 UI 用 6x6,模型用 8x8。

我的项目是 2D,所以全用 6x6 的 ASTC,UI 里的数字和文字用 5x5 保持清晰。开发者工具不支持 ASTC 等压缩格式,需要真机调试,在编辑器里预览压缩后的效果可能会看到异常,别被骗了。

配置了压缩纹理之后,只有对应格式的图片会在构建时生成,如果某些图片格式在部分设备上不支持就会导致显示异常。为避免这个问题,添加纹理压缩预设时,要额外选择一些通用图片格式(例如 PNG、JPG)作为默认图。这个坑我踩过。早期我只配了 ETC1 和 PVR,没配 PNG,结果在某些低端 Android 机上整块 UI 白屏。后来每个预设都加上了 PNG 兜底。

关于 Mipmaps,项目设置 -> 压缩纹理 面板里有个 "Mipmaps for compressed textures" 选项,可以选 Allow Pre-generation(默认,预生成)或 Disabled。如果贴图勾了 generate mip maps 又配了需要第三方工具压缩的格式(pvr、etc 等),就得在项目设置里开这个选项。

预设是可以导出成 json 再导入的,跨项目复用很方便。下面是我现在的预设长什么样(结构来自官方文档):

{
    "default": {
        "name": "default",
        "options": {
            "miniGame": { "etc1_rgb": "fast", "pvrtc_4bits_rgb": "fast" },
            "android": { "astc_6x6": "-medium", "etc1_rgb": "fast" },
            "ios": { "astc_6x6": "-medium", "pvrtc_4bits_rgb": "fast" }
        }
    }
}

3.4 资源冗余排查与压缩后的画质问题

包体优化的第一步是知道包体花在哪儿。

微信小游戏的主包体积不能超过 4 MB,整包(主包加所有分包)不超过 30 M,单个普通分包不限大小,独立分包则要卡在 4 M 以内。构建完打开发布包目录,assets、remote、subpackages 三个文件夹里每个子文件夹就是一个 Asset Bundle。每个 bundle 里有 import(资源)、native(原生资源)、config.json(资源配置信息)。

我排查的顺序是这样:第一,看 config.json 的大小,特别大说明这个 bundle 里塞了太多东西。第二,看 import 目录里的文件,按大小排序,最大的前二十个文件基本就是问题所在。第三,对这些大文件问三个问题:真的用到吗?尺寸和格式是最优的吗?是不是该放进远程 bundle?

我实际砍掉的东西:一张 4096×4096 的城市全景背景,改成了 2048 加模糊;一套 12 帧的无人机引擎火焰序列帧,缩到 6 帧;40 个音效里 11 个功能重复的合并了。

压缩后变模糊有两种原因,处理方式相反。

第一种是压缩格式本身的质量档位。ASTC 是有损的,块越大质量越低,astc 8x8 比 5x5 模糊得多。我的 UI 图标从 5x5 改到 6x6 时,个别细线条图标就糊了,只能改回去。

第二种是 mipmap 缺失。如果贴图在远处缩小后出现闪烁和噪点,是 mipmap 没生成。但对小尺寸的 UI 图标,mipmap 反而会让它变糊,UI 通常是 1:1 或 2:1 缩放,mipmap 的高层反而引入了模糊。

我的处理规则:UI 图标和文字关掉 mipmap,关卡瓦片和背景开 mipmap。另一个变模糊的原因是纹理过滤,默认双线性过滤在非整数倍缩放下会插值出模糊,像素风项目要改成最近邻过滤;我这个项目不做像素风优化,用默认的。

压缩后出错通常是格式不匹配。用 ETC1 要注意 RGB 和 RGB_A 的区别,ETC1 本身不支持 alpha 通道,需要 alpha 的图要用 etc1_rgb_a。

还有一个坑是显存的,不是包体。ASTC 6x6 的一块 1024×1024 图集占的显存和未压缩的 RGBA 差不多,都是 4 MB,但 2048×2048 就变成 16 MB。低配机上显存比包体更容易先爆。

3.5 构建面板里的相关选项

我常用的构建选项集中在 构建发布 面板。

通用部分:参与构建场景 的勾选直接决定包体,除了 resources 目录和 bundle 里的资源,其他资源都按参与构建的场景和 bundle 引用情况按需打包。Bundles 分组可以决定哪些 bundle 参与构建,正常构建模式下主包和 internal 包不可取消。调试模式 不勾就是 release 模式,会压缩混淆 UUID、引擎脚本和项目脚本。MD5 缓存 给所有资源文件名加 MD5,解决 CDN 缓存问题。

主包压缩类型 和 配置主包为远程包 这两项配合使用。勾了远程包之后,主包会被构建到发布包的 remote 目录下的内置 bundle main 里,需要把整个 remote 文件夹上传到服务器,并填 资源服务器地址。还有 使用构建内置服务器,勾了之后不用自己填地址,这项官方标注是实验室功能,仅用于开发测试。

性能相关的两个选项:跳过纹理压缩流程 默认 false,勾上之后构建时跳过整个纹理压缩流程,能大幅缩短构建时间。调试期我用它,发布前一定关掉。压缩引擎内部属性(v3.8.6 起支持)和 内联枚举(v3.8.6 起支持)这两项能有效减少代码体积,内联枚举会把引擎 TS 代码里的枚举值替换成具体数值。

原生代码打包模式那项,文档说主要影响物理、Spine 等原生模块的打包格式。我的项目开了 引擎原生代码分包,把引擎的 WASM/Asm.js 代码放进小游戏分包,减少主包。

顺手说清工程根目录长什么样,免得找文件时摸错地方。3.8 的工程根目录是 assets(资源)、build(构建产物)、library(导入缓存)、local(本机编辑器配置)、profiles(各平台构建配置)、settings(项目设置)、temp(临时文件)、extensions(自定义扩展插件),加上 package.json。自定义纹理压缩、构建流程定制这类东西都装在 extensions 目录下。

版本控制上官方建议提交 assets、extensions、settings、package.json,其余交给 gitignore。settings 一定要提交,纹理压缩预设就存在这个目录里。

包体检查的节奏我给自己定了规矩:每加一批美术资源就构建一次看体积,别攒到上线前。第一版构建出来主包 3.6 MB,第二版拆成四个 bundle 之后主包 1.1 MB,配了 ASTC 压缩又降到 780 KB。这个数字是慢慢磨出来的。

3.6 相关阅读