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时,该点是多余的

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值