我们游戏是使用Unity3d做的2D游戏,角色特效等都使用SmoothMoves来制作(在国内估计也算奇葩一朵吧,据说燃烧的蔬菜也是SmoothMoves作的),游戏中的所有的资源--角色、特效、技能ICON、角色ICON、音效等几乎都使用assetbundles来实现。
问题:加载一场战斗的时间大概要30s左右!!!
解决方案关键字:依赖打包、数据块共享、冗余数据剔除
优化后:5s左右 :)
1. 依赖打包
1.1 使用AssetDatabase.GetDependencies()接口可以查看资源的依赖引用情况,利用这些依赖信息,就可以设计如何规划依赖打包了
PS: 曾猜测unity是在meta文件存储了资源间的依赖关系,结果在meta中没有找到什么痕迹... 有了解的兄弟请分享~
PS: GetDependencies 对prefab不起作用?我的打开方式不对?
1.2 依赖打包指的是使用BuildPipeline.BuildAssetbundle()打包资源时,使用BuildPipeline.PushAssetDependencies() 和 PopAssetDependencies()两接口,将资源间共享的引用资源抽离,避免重复资源。比如A、B两资源都引用了C资源,如果不使用依赖打包,A、B对应生成的assetbundle中都会有C资源的拷贝,在内存中也就有两份C资源,这样既增大了资源包,也浪费了宝贵的内存空间。于是,Push/Pop组合就可以派上用场了。
// 打包示例1
1 Push 2 BuildAssetbundle C 3 4 Push 5 BuildAssetbundle A 6 Pop 7 8 Push 9 BuildAssetbundle B 10 Pop 11 Pop
这样会得到3个assetbundle文件:A、B、C,在加载时由于依赖关系,一定要先加载C,才可以加载A或者B。而A与B间则没有任何其他的依赖关系,先加载哪个无所谓。
对示例打包方式略加修改:
// 打包示例2
1 Push 2 BuildAssetbundle C 3 4 Push 5 BuildAssetbundle A 6 BuildAssetbundle B 7 Pop 8 Pop
或者再干脆点
// 打包示例3
1 Push 2 BuildAssetbundle C 3 BuildAssetbundle A 4 BuildAssetbundle B 5 Pop
这两种打包方式,A和B仍然依赖于C,最后一种B同时还潜在的依赖于A。如果A B间除了共同引用了C资源之外,还有其他共同的依赖项D(善用AssetDatabase.GetDependencies),则在加载B之前还必须要先加载A。所以推荐使用第1种打包方式,以避免类似情况的发生。
还是上面ABC的例子,如果A、B两文件本身没有更新,而C有修改,此时,可以只重新打包C,无须重新打包A或B,但一定要使用BuildAssetBundleOptions.DeterministicAssetBundle选项。
另外,依赖关系本身我们使用ScriptableObject来存储,当然也可以考虑使用XML等其它方式。
1.3 SmoothMoves动画打包
前面说到我们使用了SmoothMoves来制作2D动画:
a. 由于动画文件间会交叉引用atlas文件,为避免atlas的重复,将所有的atlas都由动画文件中抽离,单独打包,然后再打包动画文件。
b. 所有atlas都引用同样的Shader,使用Profiler可以看到每个atlas的shader都要解析一次,为避免shader的重复解析,shader也抽离后单独打包
c. 每个动画文件都挂载有BoneAnimation脚本,其atlas信息则由TextureAtlas来存储,这两个脚本都在SmoothMoves_Runtime.dll中。实测中发现,DLL文件的依赖打包一定要小心处理。我们的方法是建立一个无用的TextureAtlas(仅仅为引用SmoothMoves_Runtime.dll),而所有的动画文件和相应的atlas文件都依赖于此进行打包。
大致打包流程如下:
1 // SmoothMoves动画打包示例 2 BuildSMAnimation() 3 { 4 Push 5 BuildAssetbundle shared_shader 6 7 Push 8 BuildAssetbundle SmoothMoves_Runtime.dll 9 10 Push 11 foreach atlas do 12 BuildAssetbundle atlas 13 end 14 15 foreach sm_animation do 16 Push 17 BuildAssetbundle sm_animation 18 Pop 19 end 20 21 Pop 22 23 Pop 24 25 Pop 26 }
如上所示,可以在打包的同时生成依赖关系配置,并在游戏初始化时,首先读取该依赖关系,然后是shared_shader、SmoothMoves_Runtime.dll,并且将两者常驻内存,即不对其assetbundle执行unload操作,因为任何的动画文件加载时对BoneAnimation脚本的处理都依赖于SmoothMoves_Runtime.dll,任何atlas加载时其shader都依赖于share_shader。
2. SmoothMoves.BoneAnimation 中的大数据块共享
使用依赖打包后,加载速度有提升,但依旧需要近20s!!! 继续使用Profiler查看分析,最终确定是动画文件挂载的脚本BoneAnimation中的有大量数据,引起了大量GC Alloc。而且在动画实例化时,BoneAnimation也会进行深度复制,进一步测试后确定是BoneAnimation中的triggerFrames和mAnimationClips两成员变量占用绝大多数内存(在较大的动画文件中,这两项竟占到1MB)。阅读SmoothMoves_Runtime的源代码后,确定游戏中这两个成员变量是只读的(事实上triggerFrames会有写操作,只是我们的游戏不会用到),所以决定将这两项数据抽离BoneAnimation,并保存在SMAnimationData(自定义的ScriptableObject) 中单独加载,并在需要的时候为BoneAnimation添加引用。这样保证了同一动画文件的各实例化对象共享同一份数据,减少了GC Alloc的次数,同时也减少了内存占用。如下:
1 // 抽离BoneAnimation.triggerFrames 和 mAnimationCips 2 sm_animation_data = ScriptableObject createInstance of SMAnimationData 3 4 sm_animation_data.triggerFrames = bone_animation.triggerFrames 5 sm_animation_data.mAnimationClips = bone_animation.mAnimationClips 6 7 bone_animation.triggerFrames = null 8 bone_animation.mAnimationClips = null 9 10 BuildSMAnimation 11 12 BuildAssetbundle sm_animation_data
虽然已经减少了实例化SmoothMoves动画时的大数据块复制带来的GC Alloc,但由于SMAnimationData中的triggerFrames 和 mAnimationClips两大数据,加载时的大量GC Alloc依旧不可避免。继续想办法压缩这两个大数据块。
3. 优化BoneAnimation.triggerFrames
a. 出发点
BoneAnimation.triggerFrames 是以clipIndex & frame 为键值的TriggerFrame数组,每个TriggerFrame都存储了相应clip的相应frame上所有骨骼关键帧的信息,即TriggerFrameBone列表(TriggerFrame.triggerFrameBones)。详细分析后发现,这些TriggerFrameBone列表间或列表内部,存在大量属性完全一致的TriggerFrameBone。以此为出发点,建立一个无重复的TriggerFrameBone集合,并让每个TriggerFrame中的TriggerFrameBone列表都引用该集合中的元素。
b. 建立TriggerFrameBone集合和索引表
b.1 在SMAnimationData中添加新成员List<TriggerFrameBone> triggerFrameBoneSet,作为TriggerFrameBone集合使用;
b.2 在TriggerFrame中添加新成员List<int> triggerFrameBoneIndexes,存储该TriggerFrame中原有的triggerFrameBones列表在SMAnimationData.triggerFrameBoneSet中的索引;(事实上项目中使用的List<byte>类型,就是这么抠门...)
这样在SMAnimationData.triggerFrames赋值后就可以建立SMAnimationData.triggerFrameBoneSet 和各个 TriggerFrame.triggerFrameBoneIndexes了:
1 // 建立TriggerFrameBone 集合 2 BuildTriggerFrameBoneSet 3 { 4 foreach tf in sm_animation_data.triggerFrames do 5 foreach tfb in tf.triggerFrameBones do 6 //此处遍历整个列表是否有属性完全一致的TriggerFrameBone,即是说需要一个对比两个TriggerFrameBone是否一致的接口 7 if sm_animation_data.triggerFrameBoneSet not contains one TriggerFrameBone equaling tfb 8 sm_animation_data.triggerFrameBoneSet.Add(tfb) 9 endif 10 11 tf.triggerFrameBoneIndexes.Add(sm_animation_data.triggerFrameBoneSet.IndexOf(tfb)) 12 end 13 14 tf.triggerFrameBones.Clear() 15 end 16 }
如此,在对SMAnimationData进行序列化时,TriggerFrame.triggerFrameBones是没有内容的,而所有的TriggerFrameBone都在triggerFrameBoneSet中。实际效果表明,TriggerFrameBone重复率非常高,其数量可减少90%以上。
相应的在加载SMAnimationData完成时,要根据TriggerFrame中的triggerFrameBoneIndexes重建triggerFrameBones列表。
4. 优化BoneAnimation.mAnimationClips
BoneAnimation.mAnimationClips是以clip name为键值的AnimationClipSM_Lite数组,每个AnimationClipSM_Lite存储了该clip所有骨骼的颜色信息,即bones列表
a. 删除空的AnimationClipBone_Lite
AnimationClipSM_Lite.bones 列表中上存在大量没有实际意义的元素,即AnimationClipBone_Lite中的颜色信息为空,也就是下面的函数返回为true。
1 // 判断AnimationClipBone_Lite是否为空 2 public bool IsEmpty() 3 { 4 if (colorACurveSerialized.keyframes.Count > 0 5 || colorRCurveSerialized.keyframes.Count > 0 6 || colorGCurveSerialized.keyframes.Count > 0 7 || colorBCurveSerialized.keyframes.Count > 0 8 || colorBlendWeightCurveSerialized.keyframes.Count > 0) 9 { 10 return false; 11 } 12 else 13 { 14 return true; 15 } 16 }
在AnimationClipSM_Lite的构造函数中加入AnimationClipBone_Lite是否空的判断,如果为空则不添加到bones列表中。如此,还要调整原本对bones的访问:原代码中都使用boneDataIndex来对bones进行读取,即boneDataIndex即是bones列表的索引下标。将缩减后的bones列表要转换为以boneDataIndex为键值的映射表,如下
1 public void BuildBoneDict() 2 { 3 m_bone_dict = new Dictionary<int, AnimationClipBone_Lite>(); 4 5 if (bones != null) 6 { 7 for (int i = 0; i < bones.Count; i++) 8 { 9 AnimationClipBone_Lite each_bone = bones[i]; 10 m_bone_dict.Add(each_bone.boneDataIndex, each_bone); 11 } 12 } 13 }
代码中所有对bones的访问,都改用m_bone_dict,而bones列表仅起到序列化/反序列化的作用。
b. 删除AnimationClipBone_Lite中的无用帧
AnimationClipBone_Lite中的存储是该骨骼的颜色变化曲线,以下两种情况可以优化:
b.1 当颜色变化为一条直线,那除了直线两端的点以外,其它点都是多余的
b.2 当颜色变化仅有一个点,且其blendWeight值为0时,该点是多余的