Unity AssetBundle 快取機制與載入範例
前陣子看到有人詢問關於 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)
{
...
}
系列文章
- Unity AsserBundle 快取機制與載入範例
- 在 Unity 使用 AssetBundles 實作簡易的遊戲資源打包以及更新機制
- 在 Unity 使用 AssetBundles 實作進階的遊戲資源打包以及更新機制
- Unity AssetBundle Variants 機制研究筆記
- Unity AssetBundle 資料列表載入以及打包架構思考,使用 Pokémon 作為範例
沒有留言: