[程式] Unity Asset Bundle 讨论

楼主: NDark (溺于黑暗)   2019-04-09 22:49:12
网志版本:https://wp.me/pBAPd-A2
大纲
基础概念
程式差异:使用资源的方法
静态管理器,动态读档器
包体的规范
检查版本
本地包
版本更新流程
variant 功能
检查下载资源
重复资源
新手教学包
基础概念
Asset Bundle 是 Unity 的动态媒体包机制 以下简称 (AB)
简单来讲 Unity很难直接使用媒体(如贴图) (实际要用当然可以但是从原始素材到实际使
用的资源有非常多Unity本身的设定要做,很难做的跟先在Unity编好后打包为素材后使用
一样方便)
打比方说材质设定(Material)是一种Unity的特有资源.这是无法在外部自行制作的.
AB 就是素材打包后 Unity 可以认得的各种资源.
如果不使用 AB. 多半是 使用以下两种方法来存取资源.
1. 将资源拉到场景物件中,直接使用.
2. 使用 Resource.Load 从 Resources/ 资料夹中读取.
(1.) 的这种方法当读入那个场景. 资源就会顺带读进来. 也就是资源其实包在场景内.
(2.) 的这种方法 其实就可以想像 Resources/ 下的所有东西是一个大的资源包, 会随着
游戏本体打包. 而且当游戏读入的时候会一并加载(不管有没有读取里面的东西).
AB的目的.除了能够不更新本体来更换资源之外,很重要一部分就是减少本体的大小.
也就是说正确使用AB必须达到一种效果. AB之内的资源必须 2. 不在 Resources\ 内 1.
不被任何场景所索引. (否则资源已被包入本体,那么同时使用AB就会有两份类似的资源)
也就是本体完全不知道有这份资源的存在. 动态透过 AB 取得其中的资源.
程式差异:使用资源的方法
使用 AB 在程式上最大的差异就是从同步处理到异步处理.
原本直接使用索引或是呼叫 Resources.Load() 因为资源已经读入在内存,
所以可以立即取得资源,而使用 AB 因为原本并非与本体一并读入的原因,
必须先读入 AB (资源包),然后再从资源包中读入资源使用.也就是需要资源的时间点
与能够使用资源的时间点并不相同(异步)导致程式码架构写作习惯有大大的不同.
对已经完成的系统架构改动起来影响非常巨大.
原本得程式架构可能是
void CreateCharacter()
{
GameObject prefab = Resource.Load(...);
GameObject object = GameObject.Instanciate( prefab );
m_Character = object;
// 然后物件在函式完成已经可以正常使用.
m_Anim = m_Character.AddComponent<AnimScript>();
}
void PlayAnim(...)
{
m_Anim.Play(...);
}
改变成
IEnumerator StartCreateCharacter()
{
StartFetchAB(
...
AsyncFunc( bundle )
{
GameObject prefab = bundle.Load(...);
GameObject object = GameObject.Instanciate( prefab );
m_Character = object;
m_Anim = m_Character.AddComponent<AnimScript>();
// frame B 才产生索引
}
) ;
// frame A 物件在函式完成还没有出现,无法 使用 m_Character 及 m_Anim
}
void PlayAnim(...)
{
// 如果在 frame B之前呼叫到此函式,则 m_Anim 还未取得脚本
m_Anim.Play(...);
}
又或者因为取得 AB 需要一点时间,导致场景开始时必须等待取得会有数个画格的空档,
画面上会出现没有正确初始化的画面.甚至如果特效物件是在 AB 之中,那么拨放特效的
时间就会延迟.
静态管理器,动态读档器
5.6 版本以前 Unity 的 AB 其实非常阳春.虽然整个流程直到资源取得是可以串起来了
.但是没有一个官方规范或推荐的机制.也就是说 Unity 官方只提供取得 Bundle 之后
的资源取用,却没有规范怎么打包,取得及管理这些 Bundle.
约 5.6 版本前后,官方也只有在 Asset Store 上提供一个插件(而不是内建管理器脚本
在引擎中).最近的版本才陆续提供了 AB 的一些预览机制.也就是因为如此,各家工作
室就会自行设计各派别中继层(同间公司都能有两三种作法).
中继层大概会包括打包脚本,一个静态管理器,负责组织及储存下载中,已下载的
Bundle.一个动态下载器,负责执行 Coroutine 呼叫网络要求,等待下载(当然也可用
游戏的脚本来负责下载,下载完档案再交给管理器,就如同上面阐述了各门各派的问题)

打包脚本是一个编辑器脚本(非游戏脚本,不会打包到游戏内),简而言之就是那些档案
要包到哪个包.是的!大概是2015~2016年之前,哪些档案要包必须使用者自己建表去打
包.在某一个版本之后官方终于在各资源 inspector 下方加上了一个 assetBundleName
的选择框.但这段过渡时期也导致了有些仍使用了自订清单打包的团队,觉得用官方这时
候提供的选择框的手动方案十分"不自动化"/"不客制化".
在选择框决定了各项资源要打包的包体档案名称之后,打包脚本最终就会产出 Bundle 的
档案.
也就是说原本资源
1.) Assets/Texture/ABC.png 原本直接使用索引取得该资源,变成打包到一个档案
<bundle1> 中的 ABC.png.取得包体读取时要这样 <bundle1>.Load("ABC").
2.) Assets/Resources/Weapon/bullet.prefab,原本使用
Resources.Load("Weapon/bullet"),变成打包到一个档案 <bundle2> 中的
bullet.prefab 读取时 <bundle2>.Load("bullet").
官方提供的插件除了管理以下载,下载中的物件之外,还有负责组合路径的功能.因应不
同平台的包体.包体应该会放置在网络上如下:
<url>/<platform>/<bundle1>
<platform>就代表了不同的平台必须使用不同的包体(因不同平台得使用不同的贴图压缩
格式,所以不能互通)
如果不使用官方的函式 WWW.LoadFromCacheOrDownload() 来下载,则必须自行下载档案
,存放在平台可放存盘的位置(Application.streamingAssetsPath 或
application.datapath)然后在使用时再从那些已经存盘的本地路径读出那些包体档案后
使用.
而这些路径规则在编辑器执行时也要能正常,注意各平台间路径斜线,以及前坠的差异
file:// http://
补充备注:由于编辑器的执行是在 PC 平台上,他使用的包体应当是 PC 平台.如果使用
到其他平台的包体,有可能会有贴图错误的问题.这点是开发行动游戏(常常将平台切换
维持在手机平台,或是根本就没有打 PC 包体来使用)常见会发生的错误.
官方提供的插件还有一个值得称赞的特点,除了在编辑器执行时也能正常下载使用 AB 之
外,还提供了模拟模式,也就使用包体不需要下载,而是直接取得资料夹中的资源的功能
.省去开发时间变更资源后打包的动作(但是决定各资源到哪一包,以及取得下载包及其
中资源的手续还是不能省).
下载包体
不管是使用档案建表,资料夹建表,还是编辑器选择,哪些资源要打包在哪一包中个专案
有自己的不同作法.
某些工作室习惯大包体的做法优点是因为网络要求少,这样下载会快点.但是
缺点之一就是不能分批更新,一次必须全部更新.
缺点之二在于因为资源都在少数包体中,所以一旦必须取得其中一个资源,
就必须把整个包体加载,占用相当大的内存(除非频繁卸载).
缺点之三就是由于大包体一次需下载的档案大,所以在网络品质不佳的状况下有可
能会有下载不全失败变残档必须重新下载的问题.
注:这边特别注意内存的使用,包体内(可视为压缩)算一份,取出来的资源又算一份

小而大量的包体的优缺点则相反,网络下载时必须考虑一次发多少个 Request 是最佳化
的问题.太多或太少的 Request 都会造成下载时间过长.
而小到极致一个资源一个包体的作法就是把每个资源的读取都下载化(异步),优点是
在取得资源时不用再思考这个资源在哪一包(多半使用资源名称编码的方式来决定包体名
称),但缺点是整个游戏可能会有几百到几千个包体,要特别小心前述下载 Request 的
问题.
最后有一种工作室是使用压缩及 AB 的混和型.也就是 AB 打包完成后,再透过压缩方式
把档案压成更少数量的包体,然后下载时先下载压缩包,解压缩之后放在本地资料夹,然
后取用 AB 时一律透过本地路径来取得包体.
检查版本
前面说到 AB 的作用之一就是可以小部分更新,不需要更换本体.也因此更新时会碰到版
本检查的问题.官方的函式提供了版号及 hash 两种做法,也就是在
WWW.LoadFromCacheOrDownload() 中指定版号或 hash,如果本地已取得(cache)的包体的
版号比较小,或是 hash 不一致,那么代表有新档案,此时 Unity 就会重新下载.否则
优先使用曾取得的包体.然而版号及 hash 要怎么在呼叫下载前让 client 知道,这点没
有规范.
如果是使用 hash 的做法,那么在打包完成之后必须使用
AssetBundleManifest.GetAssetBundleHash() 来取得个档案的 hash 值,在游戏 呼叫下
载之前先取得,然后下载之时就知道应该要抓到的档案的 hash 值是多少.来做出正确判
断.
使用版本号的方法也是类似,呼叫 WWW.LoadFromCacheOrDownload() 时依照传入的版号
,系统记录此次下载的档案的版号,下一次呼叫的时候依照传入的版号来做比较.也就是
说在呼叫该下载函式之前,就必须先知道预期取得的档案版号为何?
这个版号或 hash 值通常必须透过服务器在 "取得AB之前" 优先取得算是一个开发上比较
需要注意的地方.
备注:如果还没有建立 AB 版本控制的开发者,当打包更新了 AB 档案后,请记得曾下载
的 AB 会 cache 在本机,有可能会发生一直抓不到更新版的问题,这种情况请使用命令
强制清除 cache.
另外,如果是前述下载自订压缩包的做法,因为先下载的是压缩包,所以下载前则自行检
查之前是否已经有下载过?是否需要再下载?已下载的是哪一个版本?是否要透过网络要
求(request)的更新机制来检查新版(网页的规范中,会带参数来检查网页档案的更新属
性,让浏览器决定是否会优先用 cache 的页面)?
而如果对官方的 cache 机制有意见,也可以以类似压缩包的作法来操作,完全透过自订
的下载方式下载 AB 档案到本地端,对其检查增删,等档案正确后才用 Unity 的函式自
本地取得 AB 包体.
本地包
前段落提到先下载包体存放在本地,然后再使用的作法.而关于本地包还有一种作法是
将 AB 包体随着本体一起打包的作法.这种做法的优点是当玩家从商城下载完成之后,因
为本体内部已经有下载包,所以不需要再花费额外的流量(或时间)从第三方空间下载
AB.(附带好处就是本地包体的流量算商城的,这就是暗黑兵法)
会出现这种作法的原因是因为某些区域市场,玩家习惯在固定网络的地方先用wifi下
载.等到回家在开始玩的时候不希望再花费数据传输费用来下载下载包.本地包的作法同
时增进游戏的首日留存率,也兼顾更新能力.
前面段落描写使用 AB 程式的写作方式不同(同步与异步),从开发的角度来看,同样
是本体内含所有资源,使用本地包,然后再更新 AB 的作法会比使用 Resources.Load()
然后再改用 AB 呼叫上比较一致,避免开发中程式使用两种不同的资源取用方式.
本地包的打包方式就是将要打入本地包的包体(打包完成)放到 Assets/StreamAssets/
资料夹中(而非放在专案外),这样打包时此资料夹内的档案会被放到游戏运行中的
Application.streamingAssetsPath 路径下.取出本地包的路径也就与网络包的路径不相
同.使用本地包必须额外处理不同路径的问题.(这种做法就很类似前述先下载档案到本
地再取用的作法)
而由于路径的不相同,使用 WWW.LoadFromCacheOrDownload() 的版本机制就会失效,因
为两个不同路径的档案被视为不相同的档案.同时因应本地包的取用,本地包打包的时
候 client 还必须知道本地包的版本.也就是放在本体内的本地版本表.方便取用时检查
如果本地版本比较高,则优先使用本地包(不下载),如果网络版本比较高,则下载取得
网络包体来使用.
什么时候会出现本地版本比较高的情况?接着说明实务上会遇到的版本更新流程.
首先哪个本地版本配上那些包体版本(甚至是服务器版本)是每个团队版本更新时必须自
行处理的问题.
理想中 AB 更新的流程是
1. 服务器进维修
2. 更新 AB 档案,更新版本表
3. 服务器开机,玩家重新连上,检查版本后取得新 AB 包体.
当本体也随之更新会遇到的问题
1. 本体(带新版本地包)更新,玩家取得新版本体
2. 玩家使用新版本本体连到旧服务器(此时会遇到上述本地版本比较高的情形,如果没
有本地包,则要考虑新版本也要能吃旧包体的相容问题)
3. 服务器进维修
4. 更新 AB 档案,更新版本表
5. 服务器开机,玩家重新连上,检查版本后判断是否要取得 AB 包体.(新本体则因为
网络版本一致,所以不需使用网络包,旧本体则因为网络版号更新,所以下载了新版
本)
注意以上这两种情形的假设是
本体版本会先释出(因此才要考虑新本体连接到旧服务器,旧包体的情形)
有维修时间,因此版本更新与 AB 包体档案的更新对 Client 来说会是同时.
如果是类似 PC 商城(Steam)的更新方式,新版服务器上线后会让本体强制更新.
如果没有维修时间,或是维修时间少到可以忽视,注意 AB 档案的更新要早于服务器版本
表更新.如果更新的顺序相反.会发生一个情形是:
1. 服务器版本更新
2. 玩家连入服务器取得网络 AB 版本为2,但此时实际上的包体其实是1.
3. 网络包体更新
4. 已经 cache 版本 1 的玩家本地端 cache 的版号是2,所以也不会再去检查更新后的
网络包体.导致一直无法更新新包体的问题.
这种情况建议透过
A) 服务器版本二次更新,在步骤 3 之后再更新一次服务器版本进到 3 版这样即便已经
吃到 2 版版号(却使用 1 版档案)的玩家也会再度下载.
B) 使用 hash 来检查档案不正确.
实务上放包体的网络空间,也必须要注意 cache 问题.在某些网络空间上,会有 cache
的设定.即便是档案已经在空间上更新了,本地下载时还是持续拿到旧的档案,那是因为
下载的过程中被服务器的 cache 机制提供了旧档,这时必须强制网络空间清除 cache,
再更新网络版号让本地端下载新版.一直无法下载到新版的客户端只好建立清除旧版包体
的机制,或是建议玩家使用安卓才有的清除资料功能.
最后,假设专案必需处理 AB 的相容版本问题(旧本体要用旧包体游玩,同一时间新本体
使用新包体,新旧包体必须同时存在的情况).那么服务器提供的 AB 网址就必须依照不
同的 Client 版本号而不同,此时必须小心新旧包体被视为不同档案,而累积存在本地
cache 的问题.(这种情形就建议自行管理下载,而不要让 Unity 依照网址来认下载包
的唯一性)
variant 功能
Unity 的 AB 还有一个比较少人用的 variant 功能.就是可以打成相同名称但是不同副
档名的包体.这个 variant 的功能主要是用来制作多国语系,或是在不同情形下的类似
资源.作法也很简单就是编辑相同名称标签,同时加上 variant 的设定即可.最后打包
的时候就会产出这样的档案
<Bundle1>.<variant1>
<Bundle1>.<variant2>
<Bundle1>.<variant3>
而下载读档的时候,可以自行设定目前使用的 variant 关键字,也可以透过插件设定目
前 variant.来达到组合完整 AB 档案的目的.
检查下载资源
为了避免使用当下必须等下载的窘境,一般来说在游戏开始会有资源准备的一段流程,确
保各下载包都能正常取得.
为了在这个流程,透过上述所描述的版本表,本地端就知道有那些下载包要下载.将其全
部下载一遍,并检查,就完成了资源检查的流程.
而如同前述,AB 下载的时候是异步的,要考虑到 WebRequest 同时要发出多少的网络
要求.如果一次请求一个 AB 档案,则可照表逐个检查.如果同时发出多个要求,则要追
踪目前下载中的数量,直到全部档案都取得为止.当然,如果有 AB 档案无法完成下载.
系统该怎么显示应对?由于并不是每个下载包都必须同时使用,为了避免内存的浪费,
下载检查资源后,是否要针对某些 AB 档案进行卸载,哪些 AB 档案在哪些情况不卸载,
哪些 AB 档案使用完在什么情况必须卸载,或是哪些 AB 可以是否要透过静态索引来储存
,方便使用.都是专案必须要考虑的细节.
重复资源
如何能知道 Unity 的打包的状况.其实每次打包的时候 Unity 都会产生 Log.在其中纪
录打包的资源(甚至本体打包时所引到的素材档案都会有纪录.)
而 AB 打包完毕后这些的资讯在哪里可以取得?答案是在 AB 包体档案的旁边会产生相对
应的 .manifest 档案.里面就会记录这个包体内的资源.(有经过包体编辑纪录的)
备注:manifest 不需要释出给玩家,在 AB 档案内已经有那些资讯.
因为本体理论上并不应该知道包体的资讯,各包体之间为独立也不互相沟通,所以否则如
果没有妥善规划 AB 的包体资源,其实有可能会发生重复打包的现象.
打比方说,prefab1 及 prefab2 被分别打包入 bundle1 及 bundle2.查看这
bundle1 及 bundle2 的 manifest 就会分别看到 prefab1 及 prefab2 等资源.
但 Unity 的 prefab 通常是 GameObject,在其下那些 mesh 及 texture 理当被一并打
包进去,方能保证 prefab1 及 prefab2 分别透过不同的包体独立读入后都能正常运作.
然而假如 prefab1 及 prefab2 都包含相同的一张 textureA,就会发生一份资源被重复
打包的现象.
解决方式就是,将这种共享的资源也打入特定的包体中,以此为例则是标定 textureA 的
包体为 bundleTexture.
所以最后打包后就会产生:
bundle1
bundle2
bundleTexture
三个包体.而此时如果我们查看 manifest 档案的内容,则会看到 bundle1 及 bundle2
内引述了 其他的包体 bundleTexture.
而在 Unity 的官方插件的运作中,则会依照这些包体的依存关系,在取得特定包体时,
优先取得其引述依存的包体.进而减少重复资源打包的现象.
新手教学包
上面已说到,AB 的作用之一就是减少本体的大小.但是上述段落也提到,在游戏开始之
前为了确保游戏运作顺畅,必须先把所有包体下载一遍.(否则会面临当使用的时候必须
等下载的窘境)随着游戏越大,第一次开始游戏要等待的时间就越久,不利于首日留存.
为了解决这个问题,某些专案就设计了二次下载的流程.这个部份关系到 Unity AB 内的
资源相关性.在此顺带简单说明.
整个游戏包含了以下几个部分:
A) 清除掉所有索引的本体,
B) 新手教学包.使用到部分的素材.如部分的角色.初始要显示的接口图片或字型
C) 游戏资源包.包含基础素材,如所有的角色.
二次下载游戏流程如下:
1) 本体开启,先检查是否玩家已通过新手教学.
2) 如果已通过新手教学,则直接进入检查 C) ,检查完毕就进入正常游戏流程.
3) 如果未通过新手教学,第一次游玩,则进入检查 B) ,检查完毕开始新手教学.新手
教学完毕后回到 2)
这边有几个值得注意的地方.
首先是 玩家资讯 如何取得?是否存在本机端?如果要透过网络取得,那么本体的脚本就
必须有连网验证帐号功能.(否则就要把登入画面包在本体中)
B) C) 两个包体中如果有重复的资源(如角色) 要怎么处理?尤其是假如玩家使用旧帐号
在新手机上游玩,应该要能够不需要重新执行一次新手教学,也就是可以不需要下载新手
教学这部分的包体.减少下载的流量.
以我看过的某游戏来说,他们的设计是这样的.首先帐号的 ID 是由本地随机产生及提供
给服务器,本体包含登入接口提供继承或社交连动等功能.所以进入游戏后就可以判断此
帐号是否要进行新手教学.如果是新帐号.那么这个时候会播放开头动画(因此动画被包
在本体中),在开头动画约数十秒的播放时间中,背景会开始下载新手教学包.等到开头
动画播放完毕,正常情况新手教学包已经下载完毕.无缝接轨进入新手教学.新手教学是
一个事先设计好能互动但没有太多选择看似正常的流程,譬如说只有一种特技.当新手教
学进行的时候,背景则开始下载完整的游戏下载包.新手教学结束的时则可以开始正常游
戏.
理想中,开头动画播放如果没有被中断,玩家是看不到下载条棒(检查资源)的画面.新
手教学的长度如果不足于全部下载包下载的时间长度,则新手教学结束的时候还会有一段
时间可以看到下载条棒(检查资源)的画面.
总结
到此为止,以上谈论的架构大致上涵盖了使用 AB 会遇到的议题.但是我想 Unity 编辑
器的版本一直在推进.也有越来越多的函式库及工具不断出现.也有更多团队设计出更优
秀的框架及规范.本文抛砖引玉,希望在这个议题上稍加讨论.
作者: CarpeDiemAL (CarpeDiemAL)   2019-04-10 15:45:00
推推~
楼主: NDark (溺于黑暗)   2019-04-11 02:41:00
更新第二段 程式差异:使用资源的方法
作者: WorkForFree (---)   2019-04-12 18:32:00
感谢分享
楼主: NDark (溺于黑暗)   2019-04-12 18:34:00
改赘字
作者: Lhmstu (lhmstu)   2019-04-13 15:41:00
推,感谢分享
楼主: NDark (溺于黑暗)   2019-04-13 17:38:00
新增第三段 静态管理器,动态读档器,下载包体修排版
作者: ninimiga (nini)   2019-04-14 20:10:00
推传教士
作者: superweak   2019-04-14 21:26:00
作者: willier15987 (Tuantuan)   2019-04-15 00:29:00
作者: CarpeDiemAL (CarpeDiemAL)   2019-04-15 15:07:00
继续推~
作者: supercube (方方)   2019-04-15 20:45:00
推阿推~
作者: juicefish (果汁鱼)   2019-04-16 08:15:00
推个
作者: Julibea (<(^ヮ^)>)   2019-04-17 00:15:00
推推
楼主: NDark (溺于黑暗)   2019-04-17 00:28:00
补充 一个静态管理器 的 备注. 补充 下载包体 的分类

Links booklink

Contact Us: admin [ a t ] ucptt.com