Unity3d插件SmoothMoves加载速度优化

我们游戏是使用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时,该点是多余的

 

你可能感兴趣的:(unity3d)