2012年2月3日 星期五

Unity 3D : 製作與載入 AssetBundle


通常我們在遊戲程式執行過程,並不希望一次將全部的資源都載入,而比較希望實際上有使用到的才載入,以免佔用多餘的記憶體,所以我們可能會儘量規劃好不同功能的場景,在需要時才載入場景並釋放掉前個場景中不需要的資源,或是將資源放在 Resource 資料夾中,在真正需要時才利用 Resources.Load() 把資源載入;這些都是不錯的管理方法,但是當我們遊戲中的資源相當多時,輸出的程式還是會相當龐大,而且如果是時常會更新內容資源的遊戲,也會因為龐大的資源而造成編譯輸出時要花相當多的時間;特別是手機或網頁遊戲,幾乎輸出最後的結果只是一個檔案而已,這個檔案如果很龐大,將可能造成下載或啟動遊戲的不方便;這時候我們可能就要考慮製作 AssetBundle。


製作與遊戲主程式分離的資源包可以得到某程度上的好處,我們可以讓遊戲主程式只管理遊戲邏輯的部份,當有需要某些資源則從外部的 AssetBundle 載入資源,這樣一來,如果有任何遊戲更新只是添加或更換資源,並沒有變更到遊戲程式邏輯或內容部署,我們只需要重新打包資源的部份就能完成更新,而不再需要整個遊戲重新編譯輸出。

例如我們有已上架的 iPhone 遊戲,因為特定節日想把遊戲內的圖片換成另一種氣氛,通常就必須要重新輸出,然後送審,等節日過後又想把圖片換回原本的,要再次輸出遊戲,再送審,一方面送審費時費工,另一方面是從送審到架上遊戲更新完成的時間,我們並不能準確掌控;如果事先將資源分離打包成 AssetBundle 儲存在我們自己管理的伺服器中,在這種需要換圖的時刻,只需要打包自行在伺服器中將 AssetBundle 更新即可,可以省掉不斷送審的麻煩,時間上也更能掌控,另外就是遊戲主程式中沒有龐大的資源資料,主程式也就會小一點,那麼玩家要下載我們的遊戲也會更快速些。

還有就是,我們輸出為網頁遊戲時一定會發現到,輸出結果只是一個 html 檔及一個 unity3d 檔,應該有寫過網頁的人都知道,網頁的運作是瀏覽器將網頁中的文件、圖片、音樂、影像... 等等的資料下載到客戶端暫存之後再執行呈現出結果,那麼如果我們遊戲內容的龐大資源都與主程式編在一起的話,那麼輸出後的 unity3d 檔應該也會不小,此時我們可能要思考一下,我們希望玩家花多久時間完成啟動頁面再進行遊戲呢?也許 Unity 的 Web Player 有辦法解決這個問題使玩家不會花太多時間啟動頁面(這部份我就比較不確定了^_^),但是正因為網頁的運作方式是如此,所以當我們在伺服器端更新時,玩家只要沒有重新載入頁面,玩到的遊戲內容始終是舊版的,此時又要考慮到,如果沒有更新遊戲邏輯或內容規則的情況下,是否有必要強制玩家中斷遊戲重新載入頁面;如果將資源都打包成 AssetBundle 分離出來,那麼 unity3d 檔這個主程式的部份將會變得更小,而當有任何資源更新時只需要在伺服器端將 AssetBundle 的檔案換掉,玩家不需要重新載入頁面一樣能體驗到更新後的內容。

以上寫了一大堆,主要就是要說明 AssetBundle 是個很重要又好用的東西,畢竟在沒變更程式或遊戲部署的情況下,沒必要去重新 Build 遊戲主程式,以下就來說明如何製作 AssetBundle ...



首先要使用【自訂 Unity 工具列選單處理專案內容】的方法自定義選單命令,使我們能夠操作何時開始製作 AssetBundle 及如何做,接下來我們需要能夠指定哪些資源將打包成 AssetBundle,所以需要使用到 Editor class 的 Selection.GetFiltered(),還有就是要過濾哪些型別的資源才是我們要的,選擇錯誤則略過,不要處理,最後再使用 BuildPipeline.BuildAssetBundle() 建立 AssetBundle 即可,以下範例會在 Unity 專案資料夾(不是 Asset 資料夾)中建立 _AssetBunldes 資料夾用來存放打包好的 AssetBundle,打包的目標為 GameObject、Texture2D、Material 三種型別的資源,如果儲存檔案路徑檔名相同則會先刪除舊檔:

[MenuItem("Custom Editor/Create AssetBunldes")]
static void ExecCreateAssetBunldes(){

// AssetBundle 的資料夾名稱及副檔名
string targetDir = "_AssetBunldes";
string extensionName = ".assetBunldes";

//取得在 Project 視窗中選擇的資源(包含資料夾的子目錄中的資源)
Object[] SelectedAsset = Selection.GetFiltered(typeof (Object), SelectionMode.DeepAssets);

//建立存放 AssetBundle 的資料夾
if(!Directory.Exists(targetDir)) Directory.CreateDirectory(targetDir);

foreach(Object obj in SelectedAsset){

//資源檔案路徑
string sourcePath = AssetDatabase.GetAssetPath(obj);

// AssetBundle 儲存檔案路徑
string targetPath = targetDir + Path.DirectorySeparatorChar + obj.name + extensionName;

if(File.Exists(targetPath)) File.Delete(targetPath);

if(!(obj is GameObject) && !(obj is Texture2D) && !(obj is Material)) continue;

//建立 AssetBundle
if(BuildPipeline.BuildAssetBundle(obj, null, targetPath, BuildAssetBundleOptions.CollectDependencies)){

Debug.Log(obj.name + " 建立完成");

}else{

Debug.Log(obj.name + " 建立失敗");
}
}
}

以上是直接將選擇的資源打包成 AssetBundle,另外,你也可以在打包前依需求將型別為 GameObject 的資源 Instantiate 到場景中添加需要的 Component 並回到 Project 中建立 Prefab,最後再以這個 Prefab 打包成 AssetBundle,一切就看個人如何運用了。

製作好的 AssetBundle 最終目的還是要在遊戲中載入,以下用簡短的範例在遊戲畫面上顯示一個按鈕,當按下按鈕之後載入型別為 GameObject 的 AssetBundle 並利用 Instantiate() 使其實例化而在場景中產生 GameObject:

void OnGUI(){

if(GUI.Button(new Rect(5,35,100,25) , "Load GameObject")){

StartCoroutine(LoadGameObject());
}
}

private IEnumerator LoadGameObject(){

// AssetBundle 檔案路徑
string path = string.Format("file://{0}/../_AssetBunldes/{1}.assetBunldes" , Application.dataPath , "TestGameObject");

//  載入 AssetBundle
WWW bundle = new WWW(path);

//等待載入完成
yield return bundle;

//實例化 GameObject 並等待實作完成
yield return Instantiate(bundle.assetBundle.mainAsset);

//卸載 AssetBundle
bundle.assetBundle.Unload(false);
}

範例中的檔案路徑(path)字串是以此程式碼在 Unity 編輯器中執行為例,實作時應該以實際執行平台及個人喜好指定 AssetBundle 存放路徑,詳情可參考官方文件 Application.dataPath 說明;因為載入的資源必須要等待載入完成以確保遊戲運作正常,在 C# 使用 yield 除了回傳型別為 IEnumerator 之外,調用時也必須使用 StartCoroutine() 調用,如果使用 Javascript 則可直接呼叫該 function;如果 AssetBundle 已載入過,下次再請求載入將會出現 【The asset bundle '檔名' can't be loaded because another asset bundle with the same files are already loaded】的錯誤訊息,即使使用區域變數(範例中的 bundle)仍然會得到這個錯誤訊息,所以必須在每次載入使用完後將 AssetBundle 卸載;另外,要特別注意的是載入 AssetBundle 到卸載期間可能消耗些微時間,如果連續調用載入同一個 AssetBundle,造成在卸載前重複載入也可能出現前面的錯誤訊息,所以,最好是能定義個陣列或其它變數來記錄載入中的 AssetBundle 有哪些,以此變數內容在載入 AssetBundle 前檢查。