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

Unity Prefab 之使用方式筆記

Edit icon 沒有留言
Unity

Prefab (預製物件) 是在 Unity 中經常被使用的機制之一,能將 GameObject 以及其 Components 的屬性狀態儲存為單一個 Asset。當 Prefab 屬性調整時,Unity 能夠自動更新放置在場景 (Scenes) 中的實體物件 (Prefab instances)。

它就像是在使用樣板 (Template),例如在射擊遊戲中將大量使用相同的子彈物件,將子彈物件建立成 Prefab,並藉由該 Prefab 在場景大量建立其實體,調整參數只需要調整單一 Prefab 即可,方便設計者快速調整遊戲參數。

以下整理常用的 Prefab 建立實體手段 (Instantiate from prefab),讓未來使用 Prefab 時有所參考。

Instantiating in Editor

最常用的使用方式,在編輯階段,直接在 Project view 點選該 Prefab,然後拖曳到 Hierarchy view 中,並完成其實體的建立。

將 Prefab 拖曳到 Hierarchy 便可建立其實體物件

將 Prefab 拖曳到 Hierarchy 便可建立其實體物件

這個機制常在大型專案中使用,由於場景 (Scenes) 可能會包含多個大功能的系統物件,若通通放在同一個場景又同時多個人編輯,在進行版本控管上很容易造成版本衝突 (Conflict),而版本衝突並不是那麼好處理與解決,必須得先讀懂 Unity 檔案文本格式才能手動去合併修改,需要花上一段時間去學習。

因此若能多其系統物件,依照其功能拆成數多個 Prefabs,每個人僅上傳編輯後的系統功能 Prefab,這樣可以降低場景發生版本衝突的機會。(當然有些版本控制軟體可以設定檔案 Lock,但使用上不是那麼容易以及彈性。)

這個方式的最大限制,不能透過程式控制要建立多少數量的實體物件。

Resources

在許多的書籍中所介紹的方式,將 Prefab 放置在 Resources 資料夾內,透過以下程式碼載入並建立實體:

using UnityEngine;

public class BulletManager : MonoBehaviour
{
   void Start()
   {
      var prefab = Resources.Load<GameObject>("BulletA");
      var instance = GameObject.Instantiate<GameObject>(prefab, null);
      // TODO:
   }
}

Resources 從 Unity 1.x 存在已久的機制,但擁有一些無法處理的缺陷,例如耗用的記憶體會明顯增加,遊戲啟動速度變慢等等,在官方的技術論壇交流中,都已經不再建議使用這方式進行載入。

Reference and Instantiating in Runtime

相較於使用 Resources.Load,自己更傾向使用這樣的方式,宣告 SerializeField,在編輯環境設定其參照 (Reference) 到 Prefab 物件:

using UnityEngine;

public class BulletManager : MonoBehaviour
{
   [SerializeField]
   GameObject bulletPrefab = null;

   void Start()
   {
      var prefab = this.bulletPrefab;
      var instance = GameObject.Instantiate<GameObject>(prefab, null);
      // TODO:
   }
}

這可以讓開發者自行決定要使用哪一個預製物件來使用,在 Unity 編輯器操作可說是相當直覺,而且這樣的做法也可以迴避使用到 Resources 機制。

將 Prefab 拖曳到 Inspector,建立物件參照

將 Prefab 拖曳到 Inspector,建立物件參照

AssetBundles

AssetBundles 是 Unity 用來取代 Resources 所建立的機制,若要從外部載入 Prefab 使用,這是唯一的方式。得先將 Prefab 打包成 AssetBundle ,之後在執行階段載入該 AssetBundle,然後讀取其內容取得該 Prefab 來使用。

目前 AssetBundle 載入來源目前支援以下幾種:

使用該機制的建置步驟:

  • 將 Prefab 包進 AssetBundle 中 (簡單的建置範例)
    // using UnityEngine;
    // using UnityEditor;
    
    var build = new AssetBundleBuild()
    {
       assetBundleName = "bullets.assetbundle",
       assetNames = new string[]
       {
          "Assets/Bullets/BulletA.prefab",
          "Assets/Bullets/BulletB.prefab",
          "Assets/Bullets/BulletC.prefab",
       },
    };
    
    var builds = new AssetBundleBuild[] { build };
    var target = BuildTarget.Android;
    var options = BuildAssetBundleOptions.None;
    
    BuildPipeline.BuildAssetBundles(output, builds, options, target);
    
  • 使用 UnityWebRequest 從遠端伺服器加載該 Assetbundle
    using System.Collections;
    using UnityEngine.Networking;
    using UnityEngine;
    
    public class UnityWebRequestLoadExample : MonoBehaviour
    {
       void Start()
       {
          this.StartCoroutine(this.LoadPrefab());
       }
    
       IEnumerator LoadPrefab()
       {
          var url = "https://example.com/bullets.assetbundle";
          var request = UnityWebRequest.GetAssetBundle(url);
          yield return request.Send();
          var assetBundle = DownloadHandlerAssetBundle.GetContent(request);
          var prefab = assetBundle.LoadAsset<GameObject>("BulletB");
          // TODO:
       }
    }
    
  • 套用先前的程式碼,從 Prefab 中建立實體物件
    var instance = GameObject.Instantiate<GameObject>(prefab, null);
    // TODO:
    

相較於前面幾種方式,這方式稍微複雜麻煩,且使用該機制時,最好能建立完整的 AssetBundles 打包流程,減少未來發布所需的更新負擔。

不過依據自己使用 AssetBundles 的經驗,認為 AssetBundles 以場景 (Scenes) 為單位載入會比較方便,將 Prefabs 根據設計需求,放置在一個到多個場景,將場景打包成一個或是多個 AssetBundles。遊戲執行時,先加載 AssetBundles 再加載場景,之後透過 RootGameObjects 或是設計模式來取得依附在 Prefab 上的參數資料,好處是管理資源只需要透過 SceneManager Load/ Unload 即可,但那又是另一段故事了。

AssetDatabase (Editor Only)

使用到 Editor API - AssetDatabase,無法在遊戲建置後環境使用,通常是要自製 Unity 編輯器才會使用到:

using UnityEngine;
using UnityEditor;

public class BulletManager : MonoBehaviour
{
   void Start()
   {
      var path = "Assets/Bullets/BulletA.prefab";
      var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
      var instance = GameObject.Instantiate<GameObject>(prefab, null);
   }
}

沒有留言: