關於 web service, unity, blogger 等軟體工程筆記

Unity strip engine code 遇到執行不能之問題與解決

Edit icon 沒有留言
Strip engine cde in Unity

遊戲發布在 WebGL 平台發現檔案還是太大,因此在 IL2CPP 的環境下,開啟 Strip engine code 編譯功能,嘗試看看能不能減少一些檔案容量。

但由於我們另外有載入 Scene stream assetbundles 的機制,因此遇到開啟 Strip engine code 後,無法正常執行的情形。

經過 Kelvin Lo 技術支援以及時間測試後,終究能夠正常執行,留下整件事情的經過、技術問題以及相關解法支援等等資料。

測試環境 Unity5.5.1f1,Windows 10,使用 Chrome 瀏覽器測試

附註:在我們測試的例子中,開啟 Strip engine code 並且成功正常執行,遊戲部分檔案大小變化從 7,262KB 下降到 6,306 KB,最終發布時並沒有套用這個做法…。

提示:Strip engine code 是一種在 IL2CPP 專案中裁減程式碼的機制,降低執行檔的檔案大小,概念是建置遊戲時,移除沒有用到的引擎類別以及實作程式碼,例如 2D 遊戲非物理的專案編譯成執行檔時,可以不用編譯 Physical 的程式碼到執行檔中,更多細節請參考文章末的 Reference。

關於專案環境以及自建建置流程 (Custom build pipeline)

自行建置專案的 WebGL platform build pipeline,遊戲專案包含 A, B, C, D, E 五個 Unity scenes,其建立流程將 B, C, D, E 四個場景以及所依賴到的資源,通通包成進同一包的 Assetbundle。A 場景作為主場景 (as Loading frame) 發布,在遊戲初始化過程中動態載入該 Assetbundle,再載入那四個遊戲場景。

Build project

建立建置流程,首先我們先打包 Assetbundle:

var bundleOutputPath = @"C:\example-output-path\";
var assetBundleBuilds = new AssetBundleBuild[] {
   new AssetBundleBuild()
   {
      assetBundleName= "main",
      assetNames= new string[] { "scenes/B.unity", "scenes/C.unity", "scenes/D.unity", "scenes/E.unity" },
   }
};

BuildPipeline.BuildAssetBundles(
   bundleOutputPath,
   assetBundleBuilds,
   BuildAssetBundleOptions.StrictMode | BuildAssetBundleOptions.ChunkBasedCompression | BuildAssetBundleOptions.DeterministicAssetBundle,
   BuildTarget.WebGL);

接著建置 SceneA,建置成 WebGL 主框架檔案:

var webglOutputPath = @"C:\example-output-path\";
var buildPlayerOptions = new BuildPlayerOptions()
{
   target = BuildTarget.WebGL,
   scenes = new string[] { "scenes/A.unity" },
   options = BuildOptions.None,
   locationPathName = webglOutputPath,

   // 因為我們 Assetbundle 名稱為 main
   assetBundleManifestPath = Path.GetFullPath(bundleOutputPath + Path.DirectorySeparatorChar + "main.manifest"), 
};

BuildPipeline.BuildPlayer(buildPlayerOptions);

使用 IL2CPP backend,開啟 Strip engine code 可使用此建置流程完成建置,並輸出數多個檔案。

發生錯誤 uld not produce class with ID 0

將建置檔案發佈到測試網站,開啟 WebGL 遊戲,執行到加載 Assetbundle 頁面卡死,從執行環境 Chrome Developer Console 中,可以見到以下執行錯誤:

uld not produce class with ID 0.
This could be caused by a class being stripped from the build even though it is needed. Try disabling 'Strip Engine Code' in Player Settings.

如果將 Strip engine code 關閉後,再重新發布是可以正常執行,後續從與 Kelvin Lo 技術交流以及 Unity 官方文件中了解到,該錯誤是 Strip engine code 把某些 assetbundle 中使用到的引擎類別程式碼給裁減了,所以無法正常執行。

如果能找到是那些使用到的引擎類別被裁減,且能夠強制設定該些類別不被裁減,那就能解決這個問題,但如何從該錯誤 uld not produce class with ID 0 找到是那些功能要保留,以及要如何設定使得 Unity 不裁減該些功能,變成一個重要的關鍵。

查詢裁減哪些引擎程式碼 (classes and modules)

從文章末的參考文章中,Unity 是沒有簡便的方法查詢,最後 IL2CPP StripEngineCode 到底裁減哪些引擎類別的程式碼,Unity Editor log 也不會有紀錄,只能從建置成功後的暫存檔案查看 Temp/StagingArea/Data/il2cppOutput/UnityClassRegistration.cpp,以下是該檔案其中一小段例子:

class EditorExtension; template <> void RegisterClass<EditorExtension>();
namespace Unity { class Component; } template <> void RegisterClass<Unity::Component>();
class Behaviour; template <> void RegisterClass<Behaviour>();
class Animation;
class AudioBehaviour; template <> void RegisterClass<AudioBehaviour>();
class AudioListener; template <> void RegisterClass<AudioListener>();
class AudioSource; template <> void RegisterClass<AudioSource>();
class AudioFilter;

從該程式碼中,若有宣告 RegisterClass<T> 函數表示該組件 T 程式碼有編譯沒有被裁減。例如上述例子中 Animation 以及 AudioFilter 這兩個類別都沒有宣告,表示該類別程式碼被裁減,沒有編譯其實作程式碼到執行檔中。

這個程式碼文件在發布 WebGL StripEngineCode 檢查特別有用,後續我們先用一般 Unity Editor 提供的建置方法,不打包 Assetbundle,直接建置遊戲專案,將 UnityClassRegistration.cpp 保留起來,因為該程式碼紀錄我們整個專案有用到哪些引擎類別,之後用 Notepad++ 來比對後續設定發布是否有裁減到不該裁減的類別。

設定 link.xml 保留指定引擎類別程式碼不被裁減

從文章末的參考文章中,若想要保留指定類別不被裁減,在 Assets 資料夾下加入 link.xml,設定哪些 Engine classes 不被裁減,例如以下例子:

<linker>
   <assembly fullname="UnityEngine">
      <type fullname="UnityEngine.Collider" preserve="all"/>
   </assembly>
</linker>

因此只要找到我們用到那些引擎類別,並且將資料寫入到此文件後再建置專案,理論上我們的 WebGL 專案應該就可以正常執行,不會出現 uld not produce class with ID 0 的錯誤。

到底遊戲專案有用到哪些引擎類別 (Engine Classes)

一開始 Kelvin 的 Unity 技術團隊是提供資訊以及程式碼告訴我們,從建置成功的 Assetbundle manifest 中提取用到那些引擎類別,manifest 文件中的 ClassTypes: [] 陣列資料,會記錄該 Assetbundle 引用到那些引擎類別列表。

但是根據實務上的觀察,我們包成 Stream scene assetbundle (裏頭包 Scenes, *.unity3d) 時,ClassTypes: [] 通常是空陣列,所以執行錯誤裡頭的 ID 資料,uld not produce class with ID 0,永遠都是 0,而不是有意義的 YAML Class ID。(詳見 YAML ID 文件

因此我們只好自行硬幹查詢我們到底引用哪些引擎類別(查詢執行階段用到的組件),在 Unity PlayMode 中,執行此程式碼列出所有使用到的引擎類別:

using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEditor;

public static class AssetsMenuItemExtend
{

   [MenuItem("Assets/List Used Components")]
   static void ListUsedComponents()
   {
      var usedComps = new Dictionary<System.Type, int>();
      for (var ii = 0; ii < SceneManager.sceneCount; ii++)
      {
         var scene = SceneManager.GetSceneAt(ii);
         foreach (var go in scene.GetRootGameObjects())
         {
            foreach (var comp in go.GetComponentsInChildren<UnityEngine.Component>())
            {
               var compType = comp.GetType();
               if (compType.FullName.IndexOf("UnityEngine") != 0)
               {
                  continue;
               }

               if (!usedComps.ContainsKey(compType))
               {
                  usedComps.Add(compType, 1);
               }
               else
               {
                  usedComps[compType] += 1;
               }
            }
         }
      }

      var sb = new StringBuilder();
      foreach (var key in usedComps.Keys.OrderBy(v => v.FullName))
      {
         sb.AppendFormat("{0}\r\n", key.FullName);
      }

      Debug.Log(sb.ToString());
   }
}

將這程式碼執行的結果類別,整理後全部加入到 Assets/link.xml,也特別將 UnityEngine.GameObject 這最基本的類別加入,例如以下:

<linker>
   <assembly fullname="UnityEngine">
      <type fullname="UnityEngine.GameObject" preserve="all"/>
      <type fullname="UnityEngine.AudioListener" preserve="all"/>
      <type fullname="UnityEngine.EventSystems.EventSystem" preserve="all"/>
      <type fullname="UnityEngine.Transform" preserve="all"/>
      <type fullname="UnityEngine.UI.Button" preserve="all"/>
      <type fullname="UnityEngine.UI.Text" preserve="all"/>
      ... 以下省略 ...
   </assembly>
</linker>

詭異的 Animations

但以為這樣就可以搞定,使用自建建置流程重新建置,但執行還是會有相同錯誤。從該建置所產生的 UnityClassRegistration.cpp 與之前保留的 UnityClassRegistration.cpp 進行比對:

Compare UnityClassRegistration.cpp

了解是上節程式碼所抓取到的僅僅是使用到的引擎組件類別,對於使用到的 Assets 類別卻都沒有加入,例如 AnimationClip,特別是是動畫類別被裁減,因此在 linker.xml 中再加入以下類別:

<assembly fullname="UnityEngine">
   <type fullname="UnityEngine.AnimationClip" preserve="all"/>
   <type fullname="UnityEngine.Motion" preserve="all"/>
</assembly>

但最有問題的是 AnimatorController,從 Unity Docs 知道該類別屬於 UnityEditor,若是在 linker.xml 加入設定,將無法正常建置專案:

<assembly fullname="UnityEditor">
   <type fullname="UnityEditor.AnimatorController" preserve="all"/>
</assembly>

最後使用俗手,先在 Project 中建立空的 AnimatorController asset,然後在我們主場景中 SceneA 加入 Dummy 組件,並設定參考到上述建立的 AnimatorController asset,使得 UnityEngine 在建置專案時將 AnimatorController 加入參考:

class Dummy : MonoBehaviour{
   [SerializeField]
   RuntimeAnimatorController letItworkController = null;
}

如此一來就可以在 StripEngineCode 下讓專案發布並且正常遊戲。

Reference

沒有留言: