ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Unity AssetBundle入门:手动打包与加载实战,避免资源冗余

Unity AssetBundle入门:手动打包与加载实战,避免资源冗余 Unity AssetBundle 入门别再把资源全塞进包里了一分钟学会手动打包AB很多Unity开发者尤其是做单机或者小体量项目的朋友最初接触资源管理时多半是直接往Resources文件夹里一丢或者干脆用Scene引用就完事了。项目小的时候还好说一旦项目开始膨胀打出来的安装包几百MB起步每次更新版本都要重新下载整个包策划改个UI图都要等半天完整构建那时候你才会意识到AssetBundle以下简称AB包这个东西是绕不开的。这篇博文就做一件最简单的事把手上的一个资源模型、贴图、Prefab或者UI图打成AB包再在运行时加载出来。基于我在实际项目里的操作把整个流程拆到最细不讲虚的。适合刚接触AB包、被各种教程绕晕的入门者也适合已经会用但想回头把原理补扎实的开发者。先说好这只是一个入门流程。AB包体系里还有依赖管理、变体、增量构建、加密热更这些大型模块我当年也是从这里一步步踩出来的。搞懂今天这套最小流程后面那些复杂玩法你才有能力接住。1. 为什么每个项目最终都会用到AB包很多新手不理解Unity自带Resources文件夹也是动态加载资源为什么非要搞一套AB包体系还真不是Unity官方在制造复杂的东西。两者解决的核心问题完全不同。Resources的核心特征是随包发布、只读、全量加载。你放进Resources里的所有东西在构建后都会被完整地打进安装包里没有任何例外。这意味着你没法做增量更新——想修一个模型必须重新出包。另外Resources还牵涉启动性能和内存占用资源太多时Unity会维护一个庞大的索引列表拖慢启动。一个小技巧是Resources目录建议只放启动所必需的最少量资源。而AB包解决的核心痛点有两个包体瘦身与增量更新AB包是独立于主程序的文件。你可以把资源打包后扔在StreamingAssets里随首包分发也可以放服务器上运行时下载。更新游戏时玩家只需要下载变化的那几个AB文件不用重新打包整个App。资源复用与按需加载同一个AB包可以被多个配置表、多个场景引用内存层面可以做到只加载一份。比如UI图集、公共模型、角色动画抽成AB包后所有业务模块统一从AB包加载避免散落各处导致资源冗余。还有一个经常被忽略的点AB包让“数据与逻辑分离”成为可能。策划改完数值表、美术换完贴图不需要开发介入直接走打包机产出AB包程序逻辑不动运营热更就能生效。在商业游戏团队这个流程是刚需。所以说学习AB包不只是学一个API的问题它背后是一整套资源管线的思维。今天这篇先解决最基础的手工打包流程把管线跑通后面你再去看那些“AB包管理器”“依赖分析工具”的源码脉络会清晰得多。2. 把第一个资源打成AB包最小可用打包脚本先讲一个常见认知误区很多教程一上来就让你用第三方插件、或者搭建一整套带界面的打包管理器对刚入门的人来说完全是劝退。其实Unity官网早就提供了最基础的打包API也就是BuildPipeline.BuildAssetBundles。你只需要一个Editor脚本、一个目录规划就能完成打包。2.1 AssetBundle名称与后缀规划在Unity工程里选中任意资源在Inspector面板最下方能看到一个AssetBundle的选项。点开左边的下拉框选择New给它起个名字比如我这里就把一个角色模型资源命名为model/character。这里有两个容易忽略的细节直接说结论名称实际就是包的相对路径。model/character会被解析成两层目录model/character最后生成的文件名也是character。你可以利用这个特性给AB包做目录分组也方便在加载时按路径去查。后缀跟种类多没关系纯粹是给加载时区分类型用的。同一个名称下可以有多个资源但通常建议一个资源一个包避免后期的依赖图变得太复杂。我这里写的model/character则是把同一个角色模型相关的Prefab、Mesh、Material都放一起。这是第一个需要你手动做的操作给每个想打包的资源设置AB包名称。2.2 编写Editor打包脚本下面是我在实际项目里用的最简版本去掉了所有花哨逻辑只保留核心。using System.IO; using UnityEditor; using UnityEngine; public class BuildAB { [MenuItem(Tools/Build AssetBundles)] public static void BuildAllAssetBundles() { // 输出目录工程根目录下的 AssetBundles 文件夹 string outputPath AssetBundles; if (!Directory.Exists(outputPath)) { Directory.CreateDirectory(outputPath); } // 第二个参数传 BuildAssetBundleOptions.None表示最基本压缩方式LZMA // 第三个参数传 BuildTarget.StandaloneWindows64要按你的实际发布平台切换 BuildPipeline.BuildAssetBundles(outputPath, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows64); AssetDatabase.Refresh(); Debug.Log(AB包构建完成输出目录 outputPath); } }脚本很简短但是有几个地方我觉得值得展开说一下这也是实际开发中最容易出问题的位置。第一BuildTarget的作用。AB包跟平台强相关你在Windows上打出来的AB包Android设备基本用不了。原因在于AB包内部序列化格式与纹理压缩格式都是按目标平台生成的。所以实际项目里打包脚本一定要做成可配置平台的方式或者做成菜单按钮对应不同平台。我见过不少新人打包后换平台就出问题就是这个参数没变。第二BuildAssetBundleOptions参数。这里我传的是NoneUnity会用LZMA压缩优点是压缩率高缺点是加载时要有一次完整解压初次加载比较慢。实际线上项目大多用ChunkBasedCompression也就是LZ4介于压缩率和加载性能之间这我在后面第4章细说。第三输出路径。这个路径不必非得在工程内完全可以输出到工程外面。但作为入门放在工程根目录下比较直观方便你看输出结果。注意如果你打算把AB包放在StreamingAssets下随首包发布那StreamingAssets的路径在移动平台上是个只读目录运行期不能往里写。真要热更下载的AB包应该放在Application.persistentDataPath下。这个知识点请先记住后面踩坑我能讲一天。2.3 输出目录里的三个文件分别是什么点下菜单后去工程目录的AssetBundles文件夹下看你会发现除了刚才设置的那些资源文件外多出来三个特殊文件AssetBundles不带后缀、AssetBundles.manifest、AssetBundles/AssetBundles.manifest。这里直接说结论这是每个初学者绕不开的困惑AssetBundles这个不带后缀的文件是整个AB包组的总入口它记录了包与包之间、包与资源之间的依赖关系。AssetBundles.manifest是主清单的文本形式人可读但运行时加载靠的还是前面那个无后缀文件。每个具体的AB包还会生成一个同名.manifest文件比如model/character.manifest里面列出了这个包内所有资源、依赖、CRC等。那极容易被忽略的最终产物其实是那个没有后缀的AssetBundles文件。它和各个资源文件本身就是真正的AB包其余manifest文件在运行时只有在你需要做依赖加载时才用得上。顺便说一句你最终上线时可以不把.manifest文件上传到CDN。具体看你的加载方案。3. 运行时加载AB包资源LoadFromFile与异步加载打完包下一步就是运行时把它加载进来。这是入门阶段最重要的一块也是最容易踩坑的地方。AB包的各种加载API网上说法不一我按“最常用、最符合实际需求”的程度给你梳理一遍。3.1 同步加载AssetBundle.LoadFromFile核心API就是这一个AssetBundle ab AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, model/character)); GameObject prefab ab.LoadAssetGameObject(Assets/Prefabs/Character.prefab); Instantiate(prefab);这里有两个容易出错的地方展开讲。第一LoadFromFile加载的是包文件不是Asset名称。LoadFromFile的参数是AB包文件路径而不是你在Unity编辑器里看到的那个资源文件名。有些新手会误以为可以直接LoadFromFile(character.prefab)这是错的。你要加载的资源名是包内资源通过ab.LoadAsset再来查询。第二加载Prefab时资源名必须与原始路径有关。上面例子中我传给LoadAssetGameObject的是Assets/Prefabs/Character.prefab——注意这里要用相对Assets根的路径不是仅用文件名。这一点经常让新手崩溃明明包里有这个资源为什么加载出来是null其实很可能是路径写错了。有一个更简便但不太推荐的办法是在打包资源时给资源AssetBundle名称时给资源起一个别名然后在加载时直接按别名找。但最保险、最传统的做法还是用完整路径。再说一个轻量的技巧如果资源在包里唯一的你还可以用ab.LoadAllAssetsGameObject()一次性取出来。但如果你包里塞了十个资源这么干容易搞混建议包里资源越单一越好。3.2 异步加载AssetBundle.LoadFromFileAsync在移动端同步加载大文件会引起卡顿掉帧业界常规操作是使用异步API。using System.Collections; using UnityEngine; using UnityEngine.Networking; public class ABLoadExample : MonoBehaviour { IEnumerator Start() { string path Path.Combine(Application.streamingAssetsPath, model/character); AssetBundleCreateRequest request AssetBundle.LoadFromFileAsync(path); yield return request; AssetBundle ab request.assetBundle; AssetBundleRequest assetRequest ab.LoadAssetAsyncGameObject(Assets/Prefabs/Character.prefab); yield return assetRequest; GameObject prefab assetRequest.asset as GameObject; Instantiate(prefab); } }注意这里有个关键点LoadFromFileAsync只是“加载AB包文件”这个动作变成了异步但真正加载具体资源时我同样用的是异步的LoadAssetAsync。两者配合才能避免主线程卡顿。不过在实际项目中包文件加载一般不是瓶颈资源反序列化和实例化才是大头所以LoadAssetAsync的收益更明显。3.3 释放问题Unload(false) 与 Unload(true) 的天壤之别当资源用完你需要调用ab.Unload(false)或ab.Unload(true)但很多人选错了参数导致内存莫名其妙地爆掉或者资源明明释放了但纹理还在。区别是这样的Unload(false)卸载AB包内存中的元数据但不会销毁已经实例化出来的对象及其纹理网格。也就是说场景里已经生成的角色还能继续显示但后续无法再从该AB包加载新资源。Unload(true)直接销毁AB包内的所有资产包括已经实例化出来的对象引用的资源场景里如果还有人在用会出现模型变粉或物体消失。入门阶段我建议用一个简单策略如果你只把AB包当一次性资源加载用加载完生成物体后直接用Unload(false)。因为场景里的Prefab实例已经在内存里了AB包本身可以安全卸载。等你需要完全清理再配合引用计数去管理。但如果是“大世界地图”那种持续从AB包加载资源、而且频繁切换的场景Unload(false)会让内存碎片堆积这时候必须上引用计数策略。具体方案以后写一篇单独展开今天就先记住这个坑。3.4 依赖加载一个包引用了另一个包里的资源怎么办这是新手最容易懵的地方。我打包时把角色Prefab放在一个包但把它的贴图放在了另一个包。运行时只加载了角色包一运行模型是粉的。原因AssetBundle不会自动帮你把“被依赖的其他AB包”也加载进来。你需要手动加载依赖包。最常用的做法是借助主manifest文件。AssetBundle mainAB AssetBundle.LoadFromFile(Path.Combine(path, AssetBundles)); AssetBundleManifest manifest mainAB.LoadAssetAssetBundleManifest(AssetBundleManifest); string[] dependencies manifest.GetAllDependencies(model/character); foreach (string dep in dependencies) { AssetBundle.LoadFromFile(Path.Combine(path, dep)); }重点在最后一行GetAllDependencies返回的是被依赖的AB包名称数组比如依赖的贴图包名。你得逐个加载这些依赖的AB包然后被依赖的包里面的资源资源也就能被查找和引用了。需要特别注意的是被依赖包的加载必须在主包资源加载之前完成否则仍然会解析失败。这也是为什么很多项目会做一个“依赖预加载管理器”。4. 压缩格式与打包选项LZ4和LZMA怎么选刚开始学AB包时很容易忽略BuildAssetBundleOptions。但这个参数直接决定了AB包体积、加载速度、内存表现选错后果很严重。所以单独拿出来说。4.1 三种模式对比选项压缩算法包体大小加载方式适用场景BuildAssetBundleOptions.NoneLZMA最小整体读入内存并解压首次下载安装包、只加载一次的资源BuildAssetBundleOptions.Uncompressed无压缩最大直接内存映射本地效率极高但包体大BuildAssetBundleOptions.ChunkBasedCompressionLZ4中等按需解压独立Chunk90%以上的线上项目推荐LZMA是Unity的默认选项压缩率最高但坏处是它把整个AB包当一坨数据压在一起加载时必须整体解压一旦这个包有几十上百MB启动加载就会有明显的白屏等待。LZ4则聪明很多把包内资源切成块每个块单独压缩加载时按需求解压对应的Chunk不需要统统解压。代价是压缩率比LZMA差点但换来的是“用多少解多少”的灵活性。4.2 我的选型经验先给一个反向例子。我早期做项目时图省事所有包都用默认的NoneLZMA抽卡界面加载特效资源时每次都要先花一两秒去解压巨大的特效包画面愣住体验很差。后来改成ChunkBasedCompression包体只膨胀了不到20%但加载特效秒开整个手感完全不一样。这里有一个取舍首包场景资源可以用LZMA因为只在安装后解压一次后续一直使用解压后的缓存热更下载的后续资源如果经常用尽量用LZ4。另外还有一个关键词是BuildAssetBundleOptions.DisableWriteTypeTree在某些限制平台上有优化但新手先别碰容易引发生成物加载报错。我个人的建议是除非你有极端的包体压缩需求否则默认都使用ChunkBasedCompression。压缩率和体验兼顾不会有明显短板。5. 实际项目中绕不开的坑依赖、缓存与重复打包打包流程很简单难是难在各种隐性坑。我按频率排几个最有必要提前知道的。5.1 同名资源被重复打进多个AB包这是资源冗余最典型的场景。比如你有Assets/Prefabs/Enemy.prefab和Assets/Prefabs/Boss.prefab都引用了同一个Assets/Models/Sword.fbx。如果你的AB包划分是“每个Prefab一个包”而且没有把Sword单独设成一个公共包Unity会自动把它打进两个AB包里。最后出来的包体翻倍构建时间也变长。解决思路把公共资源模型、贴图、图集单独设成一个AB包比如public/shared_assets。用AssetBundle Browser插件Unity官方出过看依赖检查哪些资源被多个包引用了。直接用依赖分析工具提前在打包阶段就生成资源依赖报告。5.2 打包目录里的旧包残留随着开发迭代AB包的名称和数量是经常变化的。如果直接用同一个输出目录反复打包Unity默认不会清理旧的不再使用的包文件。这会导致一个问题热更CDN上残留了一堆客户端根本不会用到的垃圾包白白占空间也容易让排查问题的人迷惑。我的习惯是打包脚本开头先清空输出目录再生成尤其到组内协作阶段大家一拉最新代码就打包这样能保证最终产物的纯净。if (Directory.Exists(outputPath)) { Directory.Delete(outputPath, true); } Directory.CreateDirectory(outputPath);5.3 WebGL平台的IDBFS写入失败看过热搜词的人可能也搜过“unity 发布 webgl 使用 idbfs 写入失败”这个跟AB包也有直接关系。WebGL平台比较特殊文件系统完全依赖浏览器IndexedDB模拟而且默认没有开启IDBFS支持。如果代码里用AssetBundle.LoadFromFile去StreamingAssets目录读AB包很多WebGL项目会把AB包塞在StreamingAssets里随包发布在部分浏览器上会报文件写入失败。标准做法是WebGL上不要用File读取AB包改成用UnityWebRequest加载。UnityWebRequest request UnityWebRequestAssetBundle.GetAssetBundle(uri); yield return request.SendWebRequest(); AssetBundle ab DownloadHandlerAssetBundle.GetContent(request);并且浏览器本身有跨域限制AB包放服务器时需要配置好CORS。另外一定要留意浏览器缓存有时候你明明更新了服务器上的AB包但浏览器还给你旧缓存加载出来内容是旧的。常见绕法是在URL后面拼一个版本号参数。5.4 Android与iOS路径差异在移动平台上Application.streamingAssetsPath在Android上是一个jar包内的路径不能直接用FileAPI。很多新手在Android上做AB包本地加载用了LoadFromFile(Application.streamingAssetsPath /xxx)结果返回null。标准方案还是UnityWebRequestAssetBundle.GetAssetBundle配合file://协议或者把AB包先拷贝到Application.persistentDataPath再LoadFromFile。iOS相对好一些但AppStore的安装包会被重新签名压缩StreamingAssets下的大文件也可能是只读的所以热更AB包存放路径永远建议走persistentDataPath。5.5 增量构建为什么你只想打一个包却把所有包都重打了BuildPipeline的增量构建是自己判断依赖关系的但前提是你的AB包名称没有变、资源没有改变。如果你只是改了一个Prefab的Position理论上它所属的那个AB包需要重新打但Unity会把引用到它的所有包也一并标记为过期并重建。这是由依赖链决定的不是Unity笨。真要实现“改谁打谁”得引入“分目录分批打包”或者“AB包内容hash比对”的机制这些都是大型项目的做法。入门阶段先接受这个行为别硬改。6. 从手动打包到自动化这里可以延伸的思路再讲一个我经常被问到的问题打包脚本是跑通了一个但策划和美术不可能每次都能自己选资源、点菜单产品上线后也没有人力手动去打包怎么办两种常见思路。6.1 自动化批处理脚本打AB包这个动作本身可以放进CI流程比如Jenkins或者GitLab CI。触发条件可以是定时检查、代码提交、或者手动点击一个“出包”按钮。构建机上执行的是同一个BuildAssetBundles方法只不过把BuildTarget通过命令行参数传入。实现上有两个关键点在编辑器下通过[MenuItem]拿到菜单入口后其实也是封装了一层命令行调用而已。所有的包名、输出目录、平台、压缩方式、是否打版本号后缀都应该抽成可配置参数不要让逻辑散落在脚本各处。这样美术在提测时只需要走迭代流程脚本自动打包、上传CDN、通知测试不需要碰Unity编辑器。解放生产力从脚本化开始。6.2 标准化AB包命名与资源目录规范第二种是“工程规范先行”。打包脚本再强大如果美术给资源起的名字乱七八糟最后出来的AB包依然是一团乱麻。所以稍大一点的项目建议把AB包名跟资源目录路径强绑定例如assets_bundles/ui/main_menu就对应Assets/UI/MainMenu/下的所有资源脚本自动遍历目录生成AB包名而不是靠人手动去Inspector里配置。这么做至少有几个好处新成员入职后只要往对应目录放资源不用学打包知识也能提交正确的AB包。代码和资源在目录上就能对齐排查问题更快。脚本生成AB包名时顺便能做资源依赖查重避免冗余包。我后来做项目都是先把目录结构画好再写生成AB包名的脚本最后才是打包入口。顺序反了后面全是坑。6.3 更上层为什么不先学Addressable再回来学AB包我知道现在很多教程都在推Addressable官方也一直在完善它它在AB包基础上做了引用计数、依赖管理、远程加载策略等一系列封装。很多人问那我是不是直接学Addressable就不需要碰裸的AB包了我个人的体会是Addressable确实能解决大部分场景但底层仍然是AssetBundle很多高级问题比如资源加载时序、内存分析、加载失败排查最终还是得落回到AB包的机制上。所以先把AB包的底层原理捋一遍再看Addressable你会觉得它的设计是那么自然直接上手Addressable遇到奇怪的内存问题会一头雾水。从学习路径来说建议先照着本文步骤手动打一个AB包跑通全流程对输出文件和加载API有体感之后再进Addressable。这跟学编程先写指针、再上框架是一个道理。基础夯实了上层工具只是表层的便利。正式入门之后再去看热搜词里那些“unity游戏优化”“unity数字孪生”“unity全栈开发工程师”相关的问题你就知道AB包只是资源管理这棵大树的一根枝干往上还有性能优化、热更体系、多语言、增量发布……路还很长但方向对了踩坑都会变成经验。最后多说一句我自己在实际项目里总结下来的最笨也最有效的学习方式不管你用什么加载框架先别急着封装用最原始的API写一个能跑通的全流程Demo然后故意把路径写错、把压缩格式改错、把释放参数调反亲眼看一下报错和现象。这一套坑踩下来你在这个知识点上就比大多数人扎实了。
返回列表