用Unity3D製作基於web的網路遊戲,不可避免的會用到一個技術-資源動態載入。比如想載入一個大情境的資源,不應該在遊戲的開始讓使用者長時間等待全部資源的載入完畢。應該優先載入使用者附近的情境資源,在遊戲的過程中,不影響操作的情況下,後台載入剩餘的資源,直到所有載入完畢。
本文包含一些程式碼片段講述實現這個技術的一種方法。本方法不一定是最好的,希望能拋磚引玉。代碼是C#寫的,用到了Json,還有C#的事件機制。
在講述代碼之前,先想象這樣一個網路遊戲的開發流程。首先美工製作情境資源的3D建模,遊戲設計人員把3D建模導進Unity3D,托托拽拽編輯情境,完成後把每個gameobject匯出成XXX.unity3d格式的資源檔(參看BuildPipeline),並且把整個情境的資訊產生一個設定檔,xml或者Json格式(本文使用Json)。最後還要把資源檔和情境設定檔上傳到伺服器,最好使用CMS管理。用戶端運行遊戲時,先讀取伺服器的情境設定檔,再根據玩家的位置從伺服器下載相應的資源檔並載入,然後開始遊戲,注意這裡並不是下載所有的情境資源。在遊戲的過程中,後台繼續載入資源直到所有載入完畢。
一個簡單的情境設定檔的例子:
MyDemoSence.txt
Json代碼:
{
"AssetList" : [{
"Name" : "Chair 1",
"Source" : "Prefabs/Chair001.unity3d",
"Position" : [2,0,-5],
"Rotation" : [0.0,60.0,0.0]
},
{
"Name" : "Chair 2",
"Source" : "Prefabs/Chair001.unity3d",
"Position" : [1,0,-5],
"Rotation" : [0.0,0.0,0.0]
},
{
"Name" : "Vanity",
"Source" : "Prefabs/vanity001.unity3d",
"Position" : [0,0,-4],
"Rotation" : [0.0,0.0,0.0]
},
{
"Name" : "Writing Table",
"Source" : "Prefabs/writingTable001.unity3d",
"Position" : [0,0,-7],
"Rotation" : [0.0,0.0,0.0],
"AssetList" : [{
"Name" : "Lamp",
"Source" : "Prefabs/lamp001.unity3d",
"Position" : [-0.5,0.7,-7],
"Rotation" : [0.0,0.0,0.0]
}]
}]
}
AssetList:情境中資源的列表,每一個資源都對應一個unity3D的gameobject
Name:gameobject的名字,一個情境中不應該重名
Source:資源的實體路徑及檔案名稱
Position:gameobject的座標
Rotation:gameobject的旋轉角度
你會注意到Writing Table裡麵包含了Lamp,這兩個對象是父子的關係。設定檔應該是由程式產生的,手工也可以修改。另外在遊戲上線後,用戶端接收到的設定檔應該是加密並壓縮過的。
主程式:
C#代碼:
。。。
public class MainMonoBehavior : MonoBehaviour {
public delegate void MainEventHandler(GameObject dispatcher);
public event MainEventHandler StartEvent;
public event MainEventHandler UpdateEvent;
public void Start() {
ResourceManager.getInstance().LoadSence("Scenes/MyDemoSence.txt");
if(StartEvent != null){
StartEvent(this.gameObject);
}
}
public void Update() {
if (UpdateEvent != null) {
UpdateEvent(this.gameObject);
}
}
}
。。。
}
這裡面用到了C#的事件機制。
在start方法裡調用ResourceManager,先載入設定檔。每一次調用update方法,MainMonoBehavior會把update事件分發給ResourceManager,因為ResourceManager註冊了MainMonoBehavior的update事件。
ResourceManager.cs
C#代碼:
。。。
private MainMonoBehavior mainMonoBehavior;
private string mResourcePath;
private Scene mScene;
private Asset mSceneAsset;
private ResourceManager() {
mainMonoBehavior = GameObject.Find("Main Camera").GetComponent();
mResourcePath = PathUtil.getResourcePath();
}
public void LoadSence(string fileName) {
mSceneAsset = new Asset();
mSceneAsset.Type = Asset.TYPE_JSON;
mSceneAsset.Source = fileName;
mainMonoBehavior.UpdateEvent += OnUpdate;
}
。。。
在LoadSence方法裡先建立一個Asset的對象,這個對象是對應於設定檔的,設定type是Json,source是傳進來的“Scenes/MyDemoSence.txt”。然後註冊MainMonoBehavior的update事件。
C#代碼:
public void OnUpdate(GameObject dispatcher) {
if (mSceneAsset != null) {
LoadAsset(mSceneAsset);
if (!mSceneAsset.isLoadFinished) {
return;
}
//clear mScene and mSceneAsset for next LoadSence call
mScene = null;
mSceneAsset = null;
}
mainMonoBehavior.UpdateEvent -= OnUpdate;
}
OnUpdate方法裡調用LoadAsset載入設定檔對象及所有資來源物件。每一幀都要判斷是否載入結束,如果結束清空mScene和mSceneAsset對象為下一次載入做準備,並且取消update事件的註冊。
最核心的LoadAsset方法:
C#代碼:
private Asset LoadAsset(Asset asset) {
string fullFileName = mResourcePath + "/" + asset.Source;
//if www resource is new, set into www cache
if (!wwwCacheMap.ContainsKey(fullFileName)) {
if (asset.www == null) {
asset.www = new WWW(fullFileName);
return null;
}
if (!asset.www.isDone) {
return null;
}
wwwCacheMap.Add(fullFileName, asset.www);
}
。。。
傳進來的是要載入的資來源物件,先得到它的物理地址,mResourcePath是個全域變數儲存資原始伺服器的網址,得到fullFileName類似http://www.mydemogame.com/asset/Prefabs/xxx.unity3d。然後通過wwwCacheMap判斷資源是否已經載入完畢,如果載入完畢把載入好的www對象放到Map裡緩衝起來。看看前面Json設定檔,Chair 1和Chair 2用到了同一個資源Chair001.unity3d,載入Chair 2的時候就不需要下載了。如果當前幀沒有載入完畢,返回null等到下一幀再做判斷。這就是WWW類的特點,剛開始用WWW下載資源的時候是不能馬上使用的,要等待諾幹幀下載完成以後才可以使用。可以用yield返回www,這樣代碼簡單,但是C#要求調用yield的方法返回IEnumerator類型,這樣限制太多不靈活。
繼續LoadAsset方法:
C#代碼:
。。。
if (asset.Type == Asset.TYPE_JSON) { //Json
if (mScene == null) {
string jsonTxt = mSceneAsset.www.text;
mScene = JsonMapper.ToObject(jsonTxt);
}
//load scene
foreach (Asset sceneAsset in mScene.AssetList) {
if (sceneAsset.isLoadFinished) {
continue;
} else {
LoadAsset(sceneAsset);
if (!sceneAsset.isLoadFinished) {
return null;
}
}
}
}
。。。
代碼能夠運行到這裡,說明資源都已經下載完畢了。現在開始載入處理資源了。第一次肯定是先載入設定檔,因為是Json格式,用JsonMapper類把它轉換成C#對象,我用的是LitJson開源類庫。然後迴圈遞迴處理情境中的每一個資源。如果沒有完成,返回null,等待下一幀處理。
繼續LoadAsset方法:
C#代碼:
。。。
else if (asset.Type == Asset.TYPE_GAMEOBJECT) { //Gameobject
if (asset.gameObject == null) {
wwwCacheMap[fullFileName].assetBundle.LoadAll();
GameObject go = (GameObject)GameObject.Instantiate(wwwCacheMap[fullFileName].assetBundle.mainAsset);
UpdateGameObject(go, asset);
asset.gameObject = go;
}
if (asset.AssetList != null) {
foreach (Asset assetChild in asset.AssetList) {
if (assetChild.isLoadFinished) {
continue;
} else {
Asset assetRet = LoadAsset(assetChild);
if (assetRet != null) {
assetRet.gameObject.transform.parent = asset.gameObject.transform;
} else {
return null;
}
}
}
}
}
asset.isLoadFinished = true;
return asset;
}
終於開始處理真正的資源了,從緩衝中找到www對象,調用Instantiate方法執行個體化成Unity3D的gameobject。UpdateGameObject方法設定gameobject各個屬性,如位置和旋轉角度。然後又是一個迴圈遞迴為了載入子物件,處理gameobject的父子關係。注意如果LoadAsset返回null,說明www沒有下載完畢,等到下一幀處理。最後設定載入完成標誌返回asset對象。
UpdateGameObject方法:
C#代碼:
private void UpdateGameObject(GameObject go, Asset asset) {
//name
go.name = asset.Name;
//position
Vector3 vector3 = new Vector3((float)asset.Position[0], (float)asset.Position[1], (float)asset.Position[2]);
go.transform.position = vector3;
//rotation
vector3 = new Vector3((float)asset.Rotation[0], (float)asset.Rotation[1], (float)asset.Rotation[2]);
go.transform.eulerAngles = vector3;
}
這裡只設定了gameobject的3個屬性,眼力好的同學一定會發現這些對象都是“死的”,因為少了指令碼屬性,它們不會和玩家互動。設定指令碼屬性要複雜的多,編譯好的指令碼隨著主程式下載到本地,它們也應該通過設定檔載入,再通過C#的反射建立指令碼對象,賦給相應的gameobject。
最後是Scene和asset代碼:
C#代碼:
public class Scene {
public List AssetList {
get;
set;
}
}
public class Asset {
public const byte TYPE_JSON = 1;
public const byte TYPE_GAMEOBJECT = 2;
public Asset() {
//default type is gameobject for json load
Type = TYPE_GAMEOBJECT;
}
public byte Type {
get;
set;
}
public string Name {
get;
set;
}
public string Source {
get;
set;
}
public double[] Bounds {
get;
set;
}
public double[] Position {
get;
set;
}
public double[] Rotation {
get;
set;
}
public List AssetList {
get;
set;
}
public bool isLoadFinished {
get;
set;
}
public WWW www {
get;
set;
}
public GameObject gameObject {
get;
set;
}
}
代碼就講完了,在我實際測試中,會看到gameobject一個個載入並顯示在螢幕中,並不會影響到遊戲操作。代碼還需要進一步完善適合更多的資源類型,如動畫資源,文本,字型,圖片和聲音資源。
動態載入資源除了網路遊戲必需,對於大公司的遊戲開發也是必須的。它可以讓遊戲策劃(負責情境設計),美工和程式3個角色獨立出來,極大提高開發效率。試想如果策劃改變了什麼NPC的位置,美工改變了某個動畫,或者改變了某個程式,大家都要重新倒入一遍資源是多麼低效和麻煩的一件事。