外部导入的资源主要有图片、模型(可能包含动画)、音频等。每种类型的文件都有很多种类型,如图片有png、psd、jpeg、tga等,当导入这些文件的时候Unity会将其处理问自己的数据格式YAML,保存在Library下面,同时在Assets下面在该资源文件的位置生成meta文件,记录其GUID和一些设置信息,如FBX文件的设置、图片导入后用户配置的Sprite的信息。
含有子资源的文件除了GUID外,其子资源还会被分配一些FileID,或者叫LocalID,GUID + FileID在整个Assets下面是唯一的。其中每一个GUID和Library下面的文件夹一一对应,将Library下面的文件后缀添加为.asset,再放到Assets下面,就能理解什么是"Unity自己的数据格式"(YAML格式,数据都放在该文件的一个Data字段种)。平时我们遇到奇葩的资源问题或者报错问题经常ReimportAll其原理就是重新生成一遍Library下面的Unity数据格式。
这里比较复杂的是FBX,导入FBX后Unity会在Assets下面将FBX显示为一个不可编辑的Prefab,其图标是Prefab的图标加上一个白色的正方形,而且存在SubAssets,即子资源,这些资源如Mesh、AnimationClip、Avatar,显示在该不可编辑Prefab的下方。
内部创建的资源全部是序列化的资源,有Prefab、Material、Shader、场景、AnimatorController、OverrideController。这些文件基本是用来保存状态信息或者配置信息。
"序列化"简单来说就是将对象的状态保存在存储设备上,类似XML或者游戏存档;反序列化就是从存储设备上读取并恢复该对象的状态,这也正是Prefab和场景的原理(格式是YAML)。在Unity里将文本设置为ForceText之后,用记事本打开这些序列化文件就可以看到这些对象的关系,如引用关系、Inspector面板上的属性等等,遵循YAML的语法规则。
深入理解Unity序列化系统之后,就可以轻易的解决诸如如何查找Prefab使用了哪些组件?资源被什么Prefab引用了?如何实现Prefab嵌套?等。想深入理解这一部分,免不了使用AssetDataBase下面的方法和研究Library文件夹里面的文件是什么以及研究一遍AssetBundlde得打包流程、去读读官网的YAML ClassID Reference。
打包即是生成AssetBundle,5.X版本以上版本的过程如下(4.X手动再见,王者荣耀都从4.6.9升级到5.X了),首先用户指定要打包的资源和要把资源放到哪个AssetBundle种,随后Unity会自己收集用户指定资源的依赖资源并跟随这些用户指定的资源,接着生成每个AssetBundle的资源对应关系。随后进入打包阶段,Unity会取Library下面的自己数据格式的资源将其加入AssetBundle,并做一些数据剔除操作,如去掉编辑器下使用的数据信息。
我们经常会碰到编辑器下面和打包出来的效果不一致,这时候一般会Reimport一下,其实原因就是Unity在生成AssetBundle的时候取得是Library下面已经生成好的数据。
打包涉及到得技术难点也非常多,如何去除冗余得资源?如何规划AssetBundle得粒度?如何加快打包速度?如何针对AssetBundle做加密防止被UnityStudio轻易地Dump?这里得每个问题挑出来都能再写一篇论文了,本文不再展开叙述。
一个良好的资源加载系统一般有三层Cache,第一层是AssetBundle的Cache,第二层则是Asset(或者称Resource)层,最后一层是GameObject/Material等序列化资源的实例,我们经常听到的资源对象池(区别于Class的对象池)其实就是第三层。
要加载一个资源,第一步是加载该资源所在的AssetBundle,而要加载整个AssetBundle,又要先加载该AssetBundle依赖的AssetBundle,先加载其实这里也不是必须的,只要在LoadAsset之前把依赖的AssetBundle都加载了即可(这里的加载其实就是打开的意思)。第二部是从AssetBundle种加载资源,单个资源直接LoadAsset即可,含有大量子资源的则需要缓存起来免得每次加载都要LoadAllSubAssets,一次性加载缓存起来的速度更快,如Sprite,在合适的释放即可。最后一步,如果你的加载的资源是序列化类型(GameObject、Material等),那么你加载出来的资源是只读的,需要Instantiate一个才能使用(最好配合对象池免去频繁的创建和销毁带来的开销)。如果是Texture、Audio这些跳过第三步直接使用即可。
而上面所说的加载又可以分为同步加载和异步加载,可以用回调、轮询、Coroutine的方式来完成异步加载流程,这里不再展开叙述。没有完美的加载方案,适合项目的才是最好的,比如MOBA这种即时性比较强的比如释放技能立刻就要看见特效这种没法使用异步加载,只能使用同步 + Preload的方式。而MMO游戏由于周围角色的技能释放玩家并不是很关心,可以使用异步,逻辑层在加载过程种可以先行更改数据,在资源加载完毕后校正位置即可。
Demo或者开发阶段很多人习惯把资源放在Resources下面,直接使用Resources.Load(Async)接口来加载,好处是无需打AssetBundle,无冗余资源,缺点Unity官方博客说的挺全面了,启动慢(建立ResourceManager表),无法直接更新资源,默认是LZMA格式,加载速度慢(5.x某个版本之后可以对apk整体使用LZ4格式了),难加密等等。
更多的人则选择了AssetBundle,虽然AssetBundle粒度细的情况下打包速度极慢、虽然资源极易产生冗余、虽然依赖加载很容易出错、虽然Bundle卸载很容易造成空指针....除了小心翼翼地克服这几个问题,还有一些比较纠结的问题,比如要不要第一次启动游戏的时候把AssetBundle拷贝到PersistentDataPath下面?不同的AssetBundle粒度下是Unload(true)还是Unload(false)?解决或者将就了以上问题之后,AssetBundle的好处是显而易见的,热更新修复Bug(特别是采用Lua做脚本的游戏)、无需换安装包更新资源、更快的加载速度、直接offset读取与加密等等。
很棘手的一个问题,试想两个极端,如果AssetBundle粒度非常细,每一个AssetBundle都只包含一个资源,那么在LoadAsset之后直接Unload(false)即可。而另一个极端则是AssetBundle粒度非常粗,所有的资源都打入一个AssetBundle中,那么该AssetBundle就不能Unload,当然这个巨大的AssetBundle也无需在游戏运行时多次被加载。对比关系如下图表格所示:
AssetBundle粒度 | 粒度极细(每个资源一个AssetBundle) | 粒度极粗(一个AssetBundle包含所有资源) |
I/O开销 | 多次加载/卸载(取决于Asset缓存时间),发热严重 | 1次Load,常驻,之后的IO开销相对小 |
内存开销 | 小,加载完毕立刻Unload | 大,AssetBundle常驻 |
补丁大小 | 小,Patch只包含变化的资源 | 大,每次都需要AssetBundle全量更新 |
加密 | 重要数据加密的代价小 | 基本上不能加密(Offest或者加密TextAsset) |
... | ... | ... |