首页 > 代码库 > Unity3d插件SmoothMoves加载速度优化

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 B10 Pop11 Pop

这样会得到3个assetbundle文件:A、B、C,在加载时由于依赖关系,一定要先加载C,才可以加载A或者B。而A与B间则没有任何其他的依赖关系,先加载哪个无所谓。

对示例打包方式略加修改:

// 打包示例2
1
Push2 BuildAssetbundle C3 4 Push5 BuildAssetbundle A6 BuildAssetbundle B7 Pop8 Pop

或者再干脆点

// 打包示例3
1
Push2 BuildAssetbundle C3 BuildAssetbundle A4 BuildAssetbundle B5 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             Push11                 foreach atlas do12                      BuildAssetbundle atlas13                 end14 15                 foreach sm_animation do16                     Push17                         BuildAssetbundle sm_animation18                     Pop19                 end20 21             Pop22 23         Pop24 25     Pop26 }

如上所示,可以在打包的同时生成依赖关系配置,并在游戏初始化时,首先读取该依赖关系,然后是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 =http://www.mamicode.com/ 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 BuildSMAnimation11 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                endif10 11                tf.triggerFrameBoneIndexes.Add(sm_animation_data.triggerFrameBoneSet.IndexOf(tfb))12            end13    14            tf.triggerFrameBones.Clear()15        end16   } 

如此,在对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             else13             {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时,该点是多余的