這是第249篇UWA技術知識分享的推送。今天我們繼續為大家精選了若干和開發、優化相關的問題,建議閱讀時間10分鍾,認真讀完必有收獲。
UWA 問答社區:answer.uwa4d.com
UWA QQ群2:793972859(原群已滿員)
本期目錄:
- Unity內置資源如何打包避免冗余
- SpriteAtlas的“冗余”問題
- 關於Mesh占用內存的問題
- UGUI.Rendering.UpdateBatches耗時較高
- Plugins的DLL是如何影響Package的
AssetBundle
Q:現在打包AssetBundle,Unity中內置資源能指定AssetBundle名稱嗎?內置資源在AssetBundle中的冗余一般是怎么解決的?
查了現有資料,針對內置資源打包AssetBundle冗余的處理都是將內置資源提取或者下載到本地,然后修改資源的引用關系,這樣打包就可以指定內置資源的AssetBundle名稱。想了解下Unity現在支持腳本打包AssetBunle時指定內置資源的Bundle名稱嗎?從而防止多個資源依賴同一個內置資源,導致冗余嗎?
A:可以使用Scriptable Build Pipeline來實現題主想要的功能,具體的方法可以參考Addressable中打包內置Shader的思路。
public static IList<IBuildTask> AssetBundleBuiltInResourcesExtraction() { var buildTasks = new List<IBuildTask>(); // Setup buildTasks.Add(new SwitchToBuildPlatform()); buildTasks.Add(new RebuildSpriteAtlasCache()); // Player Scripts buildTasks.Add(new BuildPlayerScripts()); buildTasks.Add(new PostScriptsCallback()); // Dependency buildTasks.Add(new CalculateSceneDependencyData()); #if UNITY_2019_3_OR_NEWER buildTasks.Add(new CalculateCustomDependencyData()); #endif buildTasks.Add(new CalculateAssetDependencyData()); buildTasks.Add(new StripUnusedSpriteSources()); buildTasks.Add(new CreateBuiltInResourcesBundle("UnityBuiltInResources")); //將CreateBuiltInShadersBundle改成自己創建的類 buildTasks.Add(new PostDependencyCallback()); // Packing buildTasks.Add(new GenerateBundlePacking()); buildTasks.Add(new UpdateBundleObjectLayout()); buildTasks.Add(new GenerateBundleCommands()); buildTasks.Add(new GenerateSubAssetPathMaps()); buildTasks.Add(new GenerateBundleMaps()); buildTasks.Add(new PostPackingCallback()); // Writing buildTasks.Add(new WriteSerializedFiles()); buildTasks.Add(new ArchiveAndCompressBundles()); buildTasks.Add(new AppendBundleHash()); buildTasks.Add(new PostWritingCallback()); // Generate manifest files // TODO: IMPL manifest generation return buildTasks; }
[MenuItem("AssetBundles/GenerateAB")] public static void GenerateAB() { var outputPath = "Assets/AssetBundles"; if (!Directory.Exists(outputPath)) Directory.CreateDirectory(outputPath); BuildTarget targetPlatform = BuildTarget.StandaloneWindows; var group = BuildPipeline.GetBuildTargetGroup(targetPlatform); var parameters = new BundleBuildParameters(targetPlatform, group, outputPath); var buildInput = ContentBuildInterface.GenerateAssetBundleBuilds(); IBundleBuildContent content = new BundleBuildContent(buildInput); var taskList = AssetBundleBuiltInResourcesExtraction(); //創建自己的task ReturnCode exitCode = ContentPipeline.BuildAssetBundles(parameters, content, out result, taskList); if (exitCode < ReturnCode.Success) return; var manifest = ScriptableObject.CreateInstance<CompatibilityAssetBundleManifest>(); manifest.SetResults(result.BundleInfos); File.WriteAllText(parameters.GetOutputFilePathForIdentifier(Path.GetFileName(outputPath) + ".manifest"), manifest.ToString()); }
下面的代碼是CreateBuiltInResourcesBundle.cs里面的,從CreateBuiltInShadersBundle.cs里面復制一個類,並修改一點點代碼:
public ReturnCode Run() { HashSet<ObjectIdentifier> buildInObjects = new HashSet<ObjectIdentifier>(); foreach (AssetLoadInfo dependencyInfo in m_DependencyData.AssetInfo.Values) buildInObjects.UnionWith(dependencyInfo.referencedObjects.Where(x => x.guid == k_BuiltInGuid)); foreach (SceneDependencyInfo dependencyInfo in m_DependencyData.SceneInfo.Values) buildInObjects.UnionWith(dependencyInfo.referencedObjects.Where(x => x.guid == k_BuiltInGuid)); ObjectIdentifier[] usedSet = buildInObjects.ToArray(); Type[] usedTypes = ContentBuildInterface.GetTypeForObjects(usedSet); if (m_Layout == null) m_Layout = new BundleExplictObjectLayout(); //Type shader = typeof(Shader); //for (int i = 0; i < usedTypes.Length; i++) //{ // if (usedTypes[i] != shader) // continue; // m_Layout.ExplicitObjectLocation.Add(usedSet[i], ShaderBundleName); //} //上面是打包內置Shader的操作,改成全部資源就可以了 foreach (ObjectIdentifier identifier in usedSet) { m_Layout.ExplicitObjectLocation.Add(identifier, ShaderBundleName); } if (m_Layout.ExplicitObjectLocation.Count == 0) m_Layout = null; return ReturnCode.Success; }
測試如下:
將Unity默認的Effect做成prefab並打包成AssetBundle(包名為ps),可以看到特效用到的內置資源都在這個AssetBundle中:![]()
使用SBP打包后,如下:
可以看到ps的AssetBundle中已經沒有資源,生成的內置的AssetBundle中有ps用到的3個內置的資源,並且ps依賴UnityBuiltInResources這個AssetBundle:
![]()
![]()
感謝Xuan@UWA問答社區提供了回答
AssetBundle
Q:我之前對SpriteAtlas的理解是,UI在渲染的時候會根據關聯的Sprite信息找到對應的SpriteAtlas,這樣每個UI渲染都用了同樣的Atlas,這樣可以減少Draw Call。所以理論來看,應該只需要Atlas這一張圖就好了。可為什么我看到打Atlas的AssetBundle包里除了Atlas圖還有引用的原圖。在UI的AssetBundle包里,也有引用的原圖。
請問為什么有這樣的“冗余”,以及到底Image引用的是圖集還是圖片,原理是什么?
如果Atlas包里放進原圖則Atlas包里有原圖但是UI包里沒有,如果原圖放到AssetBundle Package外面,則Atlas包和UI包里都有原圖,這是什么原理?
A1:建議你先看一下這篇文章:
【Unity游戲開發】SpriteAtlas與AssetBundle最佳食用方案![]()
在合理使用SpriteAtlas的情況下,當我們把AssetBundle包解開以后,會發現里面會包含一張Texture和若干個Sprite這兩種資產。Texture是紋理,顯示的文件大小較大;而Sprite可以理解為一個描述了精靈在整張紋理上的偏移量位置信息的數據文件,顯示的文件大小較小。
因此這個不是冗余,是正常現象。
感謝馬三小伙兒@UWA問答社區提供了回答
A2:不過確實存在一個冗余的問題:如果Prefab1和Prefab2引用了同一個Atlas的Sprite,那么這個Atlas至少要主動包含在一個AssetBundle中,否則會被動打入兩個包中,造成冗余。
Atlas沒有設置AssetBundle包:
![]()
![]()
Atlas打到其中一個AssetBundle包中:
![]()
![]()
感謝Prin@UWA問答社區提供了回答
Mesh
Q:關於Mesh占用內存的問題,在Unity 2020中能看見Mesh中包含哪些信息:

導入骨骼
這里如果不導入骨骼,頂點信息占用空間會減少一倍,請問導入骨骼后頂點信息增加了什么?

未導入骨骼
第二個問題是:在導入骨骼的情況下內存中這個Mesh占用了0.6MB剛好差不多是上面Inspector中顯示的兩倍,做過其它模型的測試也是兩倍:

本來以為是開啟Read/Write Enabled的問題,但是發現開不開啟這個選項占用內存都不變。這個內存占用是怎么來的,開啟Read/Write Enabled是否具體會造成兩倍的內存開銷?
經過測試發現,在不導入骨骼的情況下,開啟Read/Write Enabled是不開啟占用內存的兩倍,不開啟Read/Write Enabled占用內存和Inspector相同。在導入骨骼的情況下開不開啟Read/Write Enabled都是Inspector界面顯示內存的兩倍。推測是導入了骨骼之后,默認會修改模型頂點,相當於默認開啟了Read/Write Enabled。
A:第一個問題:
Inspector面板的Vertices一欄指的是Mesh的頂點屬性(或通道),如果該Mesh包含某個通道的數據,就意味着每個頂點都有一份該頂點屬性。![]()
而Unity定義的頂點通道一共有14個:
![]()
如果導入了骨骼,多出來的頂點屬性是頂點的骨骼權重和骨骼索引。
![]()
可通過Mesh.boneWeights()和Mesh.GetBonesPerVertex()訪問。
然而,一個BoneWeight屬性存了4個float32和4個int32,一共8x4=32Bytes。一個Bones index是一個Byte。
(8x4+1)Byte/Vert x 5512Vert = 177.63KB
![]()
第二個問題:導入骨骼動畫,要在CPU端做蒙皮計算,就是要在CPU獲取頂點屬性。
感謝Prin@UWA問答社區提供了回答
UGUI
Q:我的測評報告中UGUI.Rendering.UpdateBatches占用較高,想問問是什么原因導致的呢?

A1:場景中Transform發生改變的UI元素太多了。看起來,場景中發生改變的Canvas有3個,而這3個Canvas下Transform發生改變的元素有312個。
感謝Prin@UWA問答社區提供了回答
A2:當Canvas中的UI元素觸發CanvasRenderer.SyncTransform次數較多(幾百次的量級)的時候,父節點UGUI.Rendering.UpdateBatches的耗時也會比較高。
測試后發現,在Unity 2018、2019和2020的版本中,調用SetActive(true)將UI元素從Deactive的狀態變成Active狀態,會導致UI元素所在的Canvas中的所有UI元素都觸發CanvasRenderer.SyncTransform。在Unity 2017的版本中這樣的操作只會影響這個SetActive(true)的元素本身,不知道是Unity的Bug還是本身就是這么設計的。不過在Unity 2018、2019和2020的版本中,可以使用設置Scale為0或者1的方法來隱藏顯示UI,這樣就只有Scanle變化的那個UI元素本身觸發CanvasRenderer.SyncTransform了。
感謝Xuan@UWA問答社區提供了回答
Script
Q:如下圖所示,工程一打開就會報Timeline的異常,后面發現是跟Plugins下某個DLL有關,刪除該DLL Timeline就能正常。那么為什么DLL會影響Timeline。Timeline這幾個重載函數肯定都在UnityEditor.CoreModule里面,怎么會找不到?

A:Unity的腳本有個嚴格的編譯順序:
precompiled DLL -> asmdefs -> StandardAsset -> Plugins -> Plugins/Editor -> Assets -> Editor。你的預編譯DLL里面很有可能寫了一個同名的PlayableBehaviour類,然后里面實現了同樣的方法。
這個DLL優先於Packages下的Timeline被Unity加載編譯,然后等到Timeline編譯的時候,會發現有兩個PlayableBehaviour,因此它就會找不到合適的方法進行重寫。
按照上面這個思路,我在本地也復現了你的這種錯誤。
![]()
把這個DLL(ConsoleApp1.dll)扔到Plugins目錄下就能看到同樣的報錯(DLL可戳原問答獲取)。
感謝馬三小伙兒@UWA問答社區提供了回答
封面圖來源於網絡
今天的分享就到這里。當然,生有涯而知無涯。在漫漫的開發周期中,您看到的這些問題也許都只是冰山一角,我們早已在UWA問答網站上准備了更多的技術話題等你一起來探索和分享。歡迎熱愛進步的你加入,也許你的方法恰能解別人的燃眉之急;而他山之“石”,也能攻你之“玉”。
官網:www.uwa4d.com
官方技術博客:blog.uwa4d.com
官方問答社區:answer.uwa4d.com
UWA學堂:edu.uwa4d.com
官方技術QQ群:793972859(原群已滿員)