標籤:style blog color 使用 io strong 檔案 資料
我們遊戲是使用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 = 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時,該點是多餘的