Unity內置資源如何打包避免冗餘

這是第249篇UWA技術知識分享的推送。今天咱們繼續爲你們精選了若干和開發、優化相關的問題,建議閱讀時間10分鐘,認真讀完必有收穫。html

UWA 問答社區:answer.uwa4d.com
UWA QQ羣2:793972859(原羣已滿員)網絡

本期目錄:ide

  • 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:ui


感謝Xuan@UWA問答社區提供了回答spa


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 201八、2019和2020的版本中,調用SetActive(true)將UI元素從Deactive的狀態變成Active狀態,會致使UI元素所在的Canvas中的全部UI元素都觸發CanvasRenderer.SyncTransform。在Unity 2017的版本中這樣的操做只會影響這個SetActive(true)的元素自己,不知道是Unity的Bug仍是自己就是這麼設計的。不過在Unity 201八、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(原羣已滿員)

相關文章
相關標籤/搜索