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

Unity AssetBundle 快取機制與載入範例

Edit icon 1 則留言
Unity

前陣子看到有人詢問關於 AssetBundle 載入以及其快取機制 (caching) 的處理問題,因此整理這篇筆記,供之後給團隊解說與教學之用。

使用 UnityWebRequest 載入 AssetBundle

Assetbundle 載入方式目前已知有三種,前兩種通常是有特殊需求才會使用到,例如自定義 Assetbundle 加解密的保護機制,否則較常使用會是第三種方式:

  • 從檔案,AssetBundle.LoadFromFile
  • 從記憶體,AssetBundle.LoadFromMemory
  • 從網路位置,UnityWebRequest.GetAssetBundle

記錄最常使用的 UnityWebRequest,配合 Coroutine,一個簡單載入 AssetBundle 的範例;

// using System.Collections;
// using UnityEngine;
// using UnityEngine.Networking;

IEnumerator LoadAssetBundle(string url)
{
   var request = UnityWebRequest.GetAssetBundle(url);
   yield return request.Send();
   if (request.isError)
   {
      // TODO: Handle error, request.error
      yield break;
   }
   var assetBundle = DownloadHandlerAssetBundle.GetContent(request);
}

如果載入完成後,發現已經沒有必要取得該 AssetBundle 資源,應該要強制釋放其資源:

request.Dispose()

使用 AssetBundle 的載入快取 (caching)

UnityWebRequest 本身有實作快取,當用戶端載入 AssetBundle 後,Unity 會將該 AssetBundle 另存一份到檔案系統裡頭,當下一次要求載入其 AssetBundle 時,Unity 若發現檔案系統有該 AssetBundle,便直接從檔案中載入,而不用再透過網路下載載入。

使用快取機制很簡單,先確認快取已經準備好:

while (!Caching.ready)
{
   yield return null;
}

要求載入時,多加入 Hash 參數即可,若要驗證 AssetBundle 檔案可多帶入 CRC:

var request = UnityWebRequest.GetAssetBundle(url, hash, crc);

Hash 以及 CRC 參數,可以在建置後的 manifest 找得到,或是使用 Script 去計算:

// using UnityEditor;

Hash128 hash;
uint crc;

BuildPipeline.GetHashForAssetBundle(assetBundlePath, out hash);
BuildPipeline.GetCRCForAssetBundle(assetBundlePath, out crc);

至於快取放在哪裡,經過實測發現快取會存在這個位置:

Application.persistentDataPath + "/UnityCache/Shared/"

若要檢查之前是否已經有快取,可以以下方式檢查:

Caching.IsVersionCached(url, hash)

取得載入 AssetBundle 實際檔案大小

一般載入進度 0%-100% 可以從 AsyncOperator 拿得到:

var request = UnityWebRequest.GetAssetBundle(url);
var op = request.Send();
while (!op.isDone)
{
  var progress = op.progress;
  // TODO: Display progress [0-1]
}

但存在一個問題是,該載入進度已經正規化 (normalized) 成 0-1 的數值,當多個 AssetBundles 載入時,其總進度會因為缺少 AssetBundle 的檔案大小數值,計算上會有些問題 (假設每個 AssetBundles 檔案大小都不同)。

較好的方式會是先取得 AssetBundle 的檔案大小,然後再開始進行加載,但問題是如何取得 AssetBundle 的檔案大小?這時候就要靠 HTTP 通訊協定中的 HEAD 來取得,若不清楚該通訊協定,可以參考這篇筆記 HTTP 淺談

long fileSize = 0;
using (var headRequest = UnityWebRequest.Head(url))
{
  yield return headRequest.Send();
  if (headRequest.responseCode != 200)
  {
  // TODO: Error response
  }
  else
  {
  var contentLength = headRequest.GetResponseHeader("CONTENT-LENGTH");
  long.TryParse(contentLength, out fileSize);
  }
}

實作載入多個 AssetBundles 範例

以下提供一個較完整使用 Coroutine 的載入範例,首先先定義 AssetBundleLoader 以及主要的回傳資料結構,用於傳遞進度條以及載入的 AssetBundles 資料:

using System.Collections;
using UnityEngine;

public static partial class AssetBundleLoader
{
   public class AsyncResult : IEnumerator
   {
      private bool done = false;

      public AssetBundle[] AssetBundles
      {
         get;
         internal set;
      }

      public float Progress
      {
         get;
         internal set;
      }

      public bool Done
      {
         get
         {
            return done;
         }

         internal set
         {
            if (!done)
            {
               done = true;
               Progress = 1.0f;
            }
         }
      }

      public bool IsError
      {
         get
         {
            return this.Error != null;
         }
      }


      public Exception Error
      {
         get;
         internal set;
      }

      object IEnumerator.Current
      {
         get
         {
            throw new NotImplementedException();
         }
      }

      bool IEnumerator.MoveNext()
      {
         return !this.Done;
      }

      void IEnumerator.Reset()
      {
         throw new NotImplementedException();
      }
   }
}

可以注意到的是 AssetBundleLoader 是 static class,並沒有繼承 MonoBehaviour,由於需要 Coroutine 的控制手段,因此透過以下方式,先建立一個 Worker,之後使用 Work.StartCoroutine 來開始新的 Coroutine 工作:

using UnityEngine;

public static partial class AssetBundleLoader
{
   static LoaderWorker worker = null;
   static LoaderWorker Worker
   {
      get
      {
         if (worker == null)
         {
            var go = new GameObject("AssetBundleLoader");
            worker = go.AddComponent<LoaderWorker>();
            GameObject.DontDestroyOnLoad(go);
            go.hideFlags = HideFlags.DontSave | HideFlags.HideInHierarchy;
         }

         return worker;
      }
   }
}

定義主要的載入函數宣告 LoadAssetBundles:

using System;
using System.Collections;
using UnityEngine;

public static partial class AssetBundleLoader
{

   public static AsyncResult LoadAssetBundles(string[] urls)
   {
      var result = new AsyncResult();
      Worker.StartCoroutine(LoadAssetBundleCoroutine(urls, result));
      return result;
   }

   static IEnumerator LoadAssetBundleCoroutine(string[] urls, AsyncResult result)
   {
      throw new NotImplementedException();
   }
}

因此對於主流程載入多個 AssetBundles 變成較為簡單:

IEnumerator LoadAssetBundles(string[] urls)
{
   var r = AssetBundleLoader.LoadAssetBundles(urls);
   yield return r;
   if (r.IsError)
   {
      // TODO: Error handle, r.Error
      yield break;
   }
   // TODO: Handle assetbundles, r.AssetBundles
}

若要處理進度調顯示,也可以調整成以下方式:

IEnumerator LoadAssetBundles(string[] urls)
{
   var r = AssetBundleLoader.LoadAssetBundles(urls);
   while (true)
   {
      // TODO: Progress display, r.Progress
      if (r.Done) break;
      yield return null;
   }

   if (r.IsError)
   {
      // TODO: Error handle, r.Error
      yield break;
   }
   // TODO: Handle assetbundles, r.AssetBundles
}

回到 AssetBundleLoader 的主要 Coroutine 實作,原則上分成兩個步驟,第一個步驟送出 HEAD 取得 AssetBundles 檔案大小,順便檢查是否遠端伺服器有正常回應,若回傳非 HTTP 200,那麼表示一定有問題則直接回傳錯誤 (例如收到 HTTP 404 找不到該資源)。而第二步驟則是實際載入 AssetBundles:

static IEnumerator LoadAssetBundleCoroutine(string[] urls, AsyncResult result)
{
   var filesizes = new long[urls.Length];
   var requests = new UnityWebRequest[urls.Length];
   var operations = new AsyncOperation[urls.Length];

   // Phase1: Get assetbundles file-size
   for (var i = 0; i < urls.Length; i++)
   {
      requests[i] = UnityWebRequest.Head(urls[i]);
      operations[i] = requests[i].Send();
   }

   while (true)
   {
      var done = true;
      for (var i = 0; i < operations.Length && done; i++)
      {
         done = done && operations[i].isDone;
      }

      if (!done)
      {
         yield return null;
      }
      else
      {
         break;
      }
   }

   // Check error
   for (var i = 0; i < requests.Length; i++)
   {
      var request = requests[i];
      if (request.isError)
      {
         result.Done = true;
         result.Error = new Exception(request.error);
         yield break;
      }
      else if (request.responseCode != 200)
      {
         result.Done = true;
         result.Error = new Exception(request.url + ", HTTP " + request.responseCode);
         yield break;
      }
      else
      {
         var header = request.GetResponseHeader("CONTENT-LENGTH");
         long.TryParse(header, out filesizes[i]);
      }
   }

   // Phase2: Load assetbundles
   var totalSize = 0L;
   for (var i = 0; i < urls.Length; i++)
   {
      requests[i] = UnityWebRequest.GetAssetBundle(urls[i]);
      operations[i] = requests[i].Send();
      totalSize += filesizes[i];
   }

   while (true)
   {
      var done = true;
      var loadedSize = 0L;
      for (var i = 0; i < operations.Length; i++)
      {
         var operation = operations[i];
         done = done && operation.isDone;
         loadedSize += (long)(filesizes[i] * (double)operation.progress);
      }

      result.Progress = (float)((double)loadedSize / totalSize);
      if (!done)
      {
         yield return null;
      }
      else
      {
         break;
      }
   }

   // Get assetBundles
   var assetBundles = new AssetBundle[urls.Length];
   for (var i = 0; i < requests.Length; i++)
   {
      var request = requests[i];
      if (request.isError)
      {
         result.Done = true;
         result.Error = new Exception(request.error);
         yield break;
      }

      assetBundles[i] = DownloadHandlerAssetBundle.GetContent(request);
   }

   result.Done = true;
   result.AssetBundles = assetBundles;
}

也許會問這個範例沒有處理快取?如果要啟用快取,那麼傳入的參數可就不能只有字串 Url,而是要調整成以下方式,那會更加複雜一些:

public struct AssetBundleLoadRequest
{
   public string Url;
   public UnityEngine.Hash128 Hash;
   public uint Crc;
}

public static AsyncResult LoadAssetBundles(AssetBundleLoadRequest requests)
{
   ...
}

系列文章

Reference

1 則留言:

  1. Live Casino (H2H) - Jordan's Arabia23 Retro
    The casino features a collection of where to get air jordan 18 retro racer blue casino air jordan 18 retro yellow suede great site slots, poker, roulette and live dealer games. All from your desktop. Download the H2H client jordan 18 white royal blue to my site for air jordan 18 retro racer blue online free. real air jordan 18 retro varsity red

    回覆刪除