2012年2月6日 星期一

Unity 3D : 腳本化物件 ScriptableObject 設置資料成為 AssetBundle


不管是製作遊戲或應用軟體,多少都會有一些系統設定值,有些人可能會使用一個文件檔,依照定義好的格式,要求製作團隊全部的人都依照這個格式去設定這些值,可是因為文件檔的編寫太過方便和自由,如果解讀這個文件檔的程式漏掉某些檢查的地方,有可能因為人員的疏忽,弄錯了空白、換行、引號、逗號....等而造成解讀結果錯誤,而且要針對各種設定資料的文檔編寫解讀程式並轉換成可用的物件成員,有時候還蠻費工的,而且也不方便管理;另外,在【Unity 3D : 製作與載入 AssetBundle】提到將 Project view 中的資源打包成 AssetBundle,並於遊戲執行時視需要再從外部載入,那麼對於自定義的資料資源,又該如何運用在 AssetBundle 呢?此時就要使用到 ScriptableObject。


試想,如果我們把需要設定的資料值都寫成 script 的 public 欄位,然後再拉到場景中的物件做設定,那麼我們可能要每個場景建立時都要做一個這樣的設定物件,當然,此時可以為這個物件建立個 Prefab,那麼每次建場景時直接拉進來就能設定了,但是,如果有一天發現其中一個共用的設定值需要變更,就必須把場景一個打開來修改;此時可能會想不如做個遊戲啟始設定場景,只要轉換場景時,這個場景的物件不要銷毀就好了,但常常一個場景中並不用使用到整個遊戲全部的設定值,那真的有必要這樣做嗎?而且把這些設定值放在場景中的物件來使用,可攜性不佳;此時,如果能在 Project view 裡的資源檔案設定好,於遊戲進行中再讀入可能會方便些,但可能又會有上一段所提到的文件格式問題;有鑒於以上顧慮到的部份,我們可以使用 ScriptableObject 在 Project view 建立專用的 Asset 來做設定,並將它存放在 Resources 資料夾中,於是,遊戲中有需要用到時,才使用 Resources.Load() 取到設定值,編輯場景內容時也不用再加入設定值專用的物件,有任何設定值需要修改的話,只要打開 Project view 內的 Resources 資料夾統一設定;另外,由於 ScriptableObject 的欄位名稱及內容值的型態都已規範好,所以也大大降低人員在設置上輸入錯誤的問題。

首先,先撰寫一個繼承自 ScriptableObject 的 class ...

public class DataHolder : ScriptableObject {

public string[] strings;
public int[] integers;
public float[] floats;
public bool[] booleans;
public byte[] bytes;
}

接下來使用【自訂 Unity 工具列選單處理專案內容】一文中提到的方式製作選單命令,使這個命令執行時在 Resources 資料夾中建立 DataHolder Asset ...

[MenuItem("Custom Editor/Create Data Asset")]
static void CreateDataAsset(){

//資料 Asset 路徑
string holderAssetPath = "Assets/Resources/";

if(!Directory.Exists(holderAssetPath)) Directory.CreateDirectory(holderAssetPath);

//建立實體
DataHolder holder = ScriptableObject.CreateInstance<DataHolder> ();

//使用 holder 建立名為 dataHolder.asset 的資源
AssetDatabase.CreateAsset(holder, holderAssetPath + "dataHolder.asset");
}

當點擊選單 Custom Editor/Create Data Asset 之後,會發現在 Project view 會多出 Resources 資料夾,且裡面會多一個名為 dataHolder 的資源,點擊它的話可以在 Inspector view 查看到它的內容欄位就如同我們在前面撰寫的 DataHolder class 中的欄位一樣,此時我們就可以開始使用它來設定欄位值,如果我們需要多個同類型的設定,只要在點選 Resources 資料夾內的 dataHolder 時按鍵盤的 Ctrl+D,就能複製多個來使用。


當遊戲執行中,只需要用一句即可取得這個資源...

DataHolder holder = (DataHolder)Resources.Load("dataHolder" , typeof(DataHolder));

到這裡應該就能明白,holder.strings 代表的就是 Inspector view 中 strings 欄位的內容,以此類推,這些設定值是不是就變得很容易取得。



接下來,如果要將設定值做成外部資源來讀入,只要依照【Unity 3D : 製作與載入 AssetBundle】一文提到的方式製作及載入即可...

[MenuItem("Custom Editor/Create Data AssetBundle")]
static void CreateDataAssetBundle(){

// AssetBundle 的資料夾路徑及副檔名
string targetDir = "_DataAssetBundles" + Path.DirectorySeparatorChar;
string extensionName = ".assetData";

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

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

foreach(Object obj in SelectedAsset){

//只處理型別為 DataHolder 的資源
if(!(obj is DataHolder)) continue;

string targetFile = targetDir + obj.name + extensionName;

//建立 AssetBundle
if(BuildPipeline.BuildAssetBundle(obj, null, targetFile, BuildAssetBundleOptions.CollectDependencies)) Debug.Log(obj.name + " 建立完成");
else Debug.Log(obj.name + " 建立失敗");
}
}

當選擇我們在前面所建立的 Resources 資料夾內的 dataHolder 之後再點擊選單 Custom Editor/Create Data AssetBundle,即會在 Unity 專案目錄下建立 _DataAssetBundles 資料夾並將此資源儲存為檔名是 dataHolder.assetData 的檔案。

之後於遊戲中要載入,則調用以下方法...

private IEnumerator LoadDataHolder(){

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

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

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

//取出 dataHolder 資源的內容
DataHolder holder = (DataHolder)bundle.assetBundle.mainAsset;

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

此處的 holder 區域變數內容則與前面使用 Resources.Load() 取得的內容相同,當然,實作時可能不是使用區域變數,這就看個人如何運用了。

以上,如果是使用製作為 AssetBundle 的方式來使用的話,那麼在 CreateDataAsset() 那邊的【資料 Asset 路徑】就不需要是在 Resources 資料夾,以免都打包成 AssetBundle 要外部使用了,結果遊戲在輸出時還全都編進去了,反而變成多餘的浪費。

也許有人會問,究竟有什麼地方使用到這些做法了,簡單的例子... 與伺服器連接的設定值,總不能改個 IP 或 port 就要重新編譯輸出整個遊戲吧!另外,在【使用 Unity 製作紙娃娃換裝的方法】一文中可以發現,在換裝後重建骨架列表時,用到的只是來源模型的骨架列表內容物件的名稱而已,像這種狀況,我們就不需要把模型及其骨架資料全都包成 AssetBundle 來載入,而可以將模型與骨架分開打包,因為骨架的部份只會用到名稱,所以只要另建字串陣列儲存即可,這樣也可有效節省資源容量,載入時間也會快一些,詳細運用可以查看官方的 Character Customization 範例;還有很多可運用的地方,就看個人如何發揮囉!!