在Unity3D游戲開發(fā)過程中,因?yàn)槭艿接螒蛉萘?、平臺(tái)性能和熱更新等諸多因素的限制,我們可能無法將所有的游戲場(chǎng)景打包到項(xiàng)目中然后相對(duì)”靜態(tài)”地加載,那么這個(gè)時(shí)候就需要我們使用動(dòng)態(tài)加載的方式來將游戲場(chǎng)景加載到場(chǎng)景中。博主在研究了Unity3D動(dòng)態(tài)加載的相關(guān)資料后發(fā)現(xiàn),目前Unity3D中實(shí)現(xiàn)動(dòng)態(tài)加載場(chǎng)景的方式主要有以下兩種方式:
* 使用BuildStreamedSceneAssetBundle()方法將場(chǎng)景打包為AssetBundle:這種方法將生成一個(gè)流式的.unity3d文件,從而實(shí)現(xiàn)按需下載和加載,因此這種方式特別適合Web環(huán)境下游戲場(chǎng)景的加載,因?yàn)樵赪eb環(huán)境下我們可以希望的是玩家可以在玩游戲的同時(shí)加載游戲。可是因?yàn)檫@種打包方式僅僅是保證了場(chǎng)景中的GameObject與本地資源的引用關(guān)系而非是將本地資源打包,因此從減少游戲容量的角度來說并不是十分實(shí)用,而且當(dāng)我們使用WWW下載完AssetBundle后,需要使用Application.Load()方法來加載場(chǎng)景,我們知道在Unity3D中加載一個(gè)關(guān)卡(場(chǎng)景)是需要在BuildSetting中注冊(cè)關(guān)卡的,因此在使用這種方式動(dòng)態(tài)加載的時(shí)候請(qǐng)注意到這一點(diǎn)。
* 將場(chǎng)景內(nèi)的所有物體打包為AssetBundle配合相關(guān)配置文件動(dòng)態(tài)生成場(chǎng)景:這種方法的思路是使用一個(gè)配置文件來記錄下當(dāng)前場(chǎng)景中所有物體的位置、旋轉(zhuǎn)和縮放信息,然后再根據(jù)配置文件使用Instantiate方法逐個(gè)生成即可。這種思路是考慮到需要在一個(gè)場(chǎng)景中動(dòng)態(tài)替換GameObject或者是動(dòng)態(tài)生成GameObject的情形,使用這種方法首先要滿足一個(gè)條件,即:場(chǎng)景內(nèi)所有的物體都是預(yù)制件(Prefab)。這是由Unity3D的機(jī)制決定的,因?yàn)镻refab是一個(gè)模板,當(dāng)你需要?jiǎng)討B(tài)生成一個(gè)物體的時(shí)候就需要為其提供一個(gè)模板(Prefab)。
如果你對(duì)這兩種方式?jīng)]有什么疑問的話,那么我覺得我們可以正式開始今天的內(nèi)容了。既然今天的題目已然告訴大家是使用AssetBundle和Xml文件實(shí)現(xiàn)場(chǎng)景的動(dòng)態(tài)加載,我相信大家已經(jīng)明白我要使用那種方式了。好了,下面我們正式開始吧!
準(zhǔn)備工作
在實(shí)現(xiàn)場(chǎng)景的動(dòng)態(tài)加載前,我們首先要在本地準(zhǔn)備好一個(gè)游戲場(chǎng)景,然后做兩件事情:
* 將場(chǎng)景內(nèi)的所有GameObject打包為AssetBundle
* 將場(chǎng)景內(nèi)所有的GameObject的信息導(dǎo)出為Xml文件
做這兩件事情的時(shí)候,相當(dāng)于我們是在準(zhǔn)備食材和菜譜,有了食材和菜譜我們就可以烹制出美味佳肴了。可是在做著兩件事情前,我們還有一件更為重要的事情要做,那就是我們需要將場(chǎng)景中使用到的GameObject制作成預(yù)制體(Prefab)。因?yàn)樵诓┲鞯挠∠笾?,Unity3D打包的最小粒度應(yīng)該是Prefab,所以為了保險(xiǎn)起見,我還是建議大家將場(chǎng)景中使用到的GameObject制作成預(yù)制體(Prefab)。那么問題來了,當(dāng)我們將這些Prefab打包成AssetBundle后是否還需要本地的Prefab文件?這里博主一直迷惑,因?yàn)槔碚撋袭?dāng)我們將這些Prefab打包成AssetBundle后,我們實(shí)例化一個(gè)物體的時(shí)候?qū)嶋H上是在使用AssetBundle的Load方法來獲取該物體的一個(gè)模板,這個(gè)模板應(yīng)該是存儲(chǔ)在AssetBundle中的?。∫?yàn)槲业墓P記本使用的是免費(fèi)版的Unity3D無法對(duì)此進(jìn)行測(cè)試,所以如果想知道這個(gè)問題結(jié)果的朋友可以等我下周到公司以后測(cè)試了再做討論(我不會(huì)告訴你公司無恥地使用了破解版),當(dāng)然如果有知道這個(gè)問題的答案的朋友歡迎給我留言啊,哈哈!這里就是想告訴大家要準(zhǔn)備好場(chǎng)景中物體的預(yù)設(shè)體(Prefab),重要的事情說三遍!!!
將場(chǎng)景內(nèi)物體打包為AssetBundle
Unity3D打包的相關(guān)內(nèi)容這里就不展開說了,因?yàn)樵诠俜紸PI文檔中都能找到詳細(xì)的說明,雖然說Unity5.0中AssetBundle打包
的方式發(fā)生了變化,不過考慮到大家都還在使用4.X的版本,所以等以后我用上了Unity5.0再說吧,哈哈!好了,下面直接給出代碼:
01.??? [MenuItem('Export/ExportTotal----對(duì)物體整體打包')]
02.staticvoidExportAll()
03.{
04.//獲取保存路徑
05.string savePath=EditorUtility.SaveFilePanel('輸出為AssetBundle','','New Resource','unity3d');
06.if(string.IsNullOrEmpty(savePath))return;
07.//獲取選擇的物體
08.Object[] objs=Selection.GetFiltered(typeof(Object),SelectionMode.DeepAssets);
09.if(objs.Length<0)return;
10.//打包
11.BuildPipeline.BuildAssetBundle(null,objs,savePath,BuildAssetBundleOptions.CollectDependencies|BuildAssetBundleOptions.CompleteAssets);
12.AssetDatabase.Refresh();
13.}
將場(chǎng)景內(nèi)物體信息導(dǎo)出為Xml文件
導(dǎo)出場(chǎng)景內(nèi)物體信息需要遍歷場(chǎng)景中的每個(gè)游戲物體,因?yàn)槲覀冊(cè)谥谱鲌?chǎng)景的時(shí)
候通常會(huì)用一個(gè)空的GameObject作為父物體來組織場(chǎng)景中的各種物體,因此我們?cè)趯?dǎo)出Xml文件的時(shí)候僅僅考慮導(dǎo)出這些父物體,因?yàn)槿绻紤]子物體
的話,可能會(huì)涉及到遞歸,整個(gè)問題將變得特別復(fù)雜。為了簡(jiǎn)化問題,我們這里僅僅考慮場(chǎng)景中的父物體。好了,開始寫代碼:
01.??? [MenuItem('Export/ExportScene----將當(dāng)前場(chǎng)景導(dǎo)出為Xml')]
02.staticvoidExportGameObjects()
03.{
04.//獲取當(dāng)前場(chǎng)景完整路徑
05.string scenePath=EditorApplication.currentScene;
06.//獲取當(dāng)前場(chǎng)景名稱
07.string sceneName=scenePath.Substring(scenePath.LastIndexOf('/')+1,scenePath.Length-scenePath.LastIndexOf('/')-1);
08.sceneName=sceneName.Substring(0,sceneName.LastIndexOf('.'));
09.//獲取保存路徑
10.string savePath=EditorUtility.SaveFilePanel('輸出場(chǎng)景內(nèi)物體','',sceneName,'xml');
11.//創(chuàng)建Xml文件
12.XmlDocument xmlDoc=newXmlDocument();
13.//創(chuàng)建根節(jié)點(diǎn)
14.XmlElement scene=xmlDoc.CreateElement('Scene');
15.scene.SetAttribute('Name',sceneName);
16.scene.SetAttribute('Asset',scenePath);
17.xmlDoc.AppendChild(scene);
18.//遍歷場(chǎng)景中的所有物體
19.foreach(GameObject go in Object.FindObjectsOfType(typeof(GameObject)))
20.{
21.//僅導(dǎo)出場(chǎng)景中的父物體
22.if(go.transform.parent==null)
23.{
24.//創(chuàng)建每個(gè)物體
25.XmlElement gameObject=xmlDoc.CreateElement('GameObject');
26.gameObject.SetAttribute('Name',go.name);
27.gameObject.SetAttribute('Asset','Prefabs/'+ go.name +'.prefab');
28.//創(chuàng)建Transform
29.XmlElement transform=xmlDoc.CreateElement('Transform');
30.transform.SetAttribute('x',go.transform.position.x.ToString());
31.transform.SetAttribute('y',go.transform.position.y.ToString());
32.transform.SetAttribute('z',go.transform.position.z.ToString());
33.gameObject.AppendChild(transform);
34.//創(chuàng)建Rotation
35.XmlElement rotation=xmlDoc.CreateElement('Rotation');
36.rotation.SetAttribute('x',go.transform.eulerAngles.x.ToString());
37.rotation.SetAttribute('y',go.transform.eulerAngles.y.ToString());
38.rotation.SetAttribute('z',go.transform.eulerAngles.z.ToString());
39.gameObject.AppendChild(rotation);
40.//創(chuàng)建Scale
41.XmlElement scale=xmlDoc.CreateElement('Scale');
42.scale.SetAttribute('x',go.transform.localScale.x.ToString());
43.scale.SetAttribute('y',go.transform.localScale.y.ToString());
44.scale.SetAttribute('z',go.transform.localScale.z.ToString());
45.gameObject.AppendChild(scale);
46.//添加物體到根節(jié)點(diǎn)
47.scene.AppendChild(gameObject);
48.}
49.}
50.
51.xmlDoc.Save(savePath);
52.}
53.
好了,在這段代碼中我們以Scene作為根節(jié)點(diǎn),然后以每個(gè)GameObject作為Scene的子節(jié)點(diǎn),重點(diǎn)在Xml文件中記錄了每個(gè)GameObject的名稱、Prefab、坐標(biāo)、旋轉(zhuǎn)和縮放等信息。下面是一個(gè)導(dǎo)出場(chǎng)景的Xml文件的部分內(nèi)容:
01.
02.
03.
04.
05.
06.
07.
08.
09.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
在這里我們假設(shè)所有的Prefab是放置在Resources/Prefabs目錄中的,那么此時(shí)我們便有了兩種動(dòng)態(tài)加載場(chǎng)景的方式
* 通過每個(gè)GameObject的Asset屬性,配合Resources.Load()方法實(shí)現(xiàn)動(dòng)態(tài)加載
* 通過每個(gè)GameObject的Name屬性,配合AssetBundle的Load()方法實(shí)現(xiàn)動(dòng)態(tài)加載
這兩種方法大同小異,區(qū)別僅僅在于是否需要從服務(wù)器下載相關(guān)資源。因此本文的主題是使用AssetBundle和Xml實(shí)現(xiàn)場(chǎng)景的動(dòng)態(tài)加載,因此,接下來我們主要以第二種方式為主,第一種方式請(qǐng)大家自行實(shí)現(xiàn)吧!
動(dòng)態(tài)加載物體到場(chǎng)景中
首先我們來定義一個(gè)根據(jù)配置文件動(dòng)態(tài)加載AssetBundle中場(chǎng)景的方法LoadDynamicScene
01.///
02./// 根據(jù)配置文件動(dòng)態(tài)加載AssetBundle中的場(chǎng)景
03.///
04./// 從服務(wù)器上下載的AssetBundle文件
05./// AssetBundle文件對(duì)應(yīng)的場(chǎng)景配置文件
06.publicstaticvoidLoadDynamicScene(AssetBundle bundle,string xmlFile)
07.{
08.//加載本地配置文件
09.XmlDocument xmlDoc=newXmlDocument();
10.xmlDoc.LoadXml(((TextAsset)Resources.Load(xmlFile)).text);
11.//讀取根節(jié)點(diǎn)
12.XmlElement root=xmlDoc.DocumentElement;
13.if(root.Name=='Scene')
14.{
15.XmlNodeList nodes=root.SelectNodes('/Scene/GameObject');
16.//定義物體位置、旋轉(zhuǎn)和縮放
17.Vector3 position=Vector3.zero;
18.Vector3 rotation=Vector3.zero;
19.Vector3 scale=Vector3.zero;
20.//遍歷每一個(gè)物體
21.foreach(XmlElement xe1 in nodes)
22.{
23.//遍歷每一個(gè)物體的屬性節(jié)點(diǎn)
24.foreach(XmlElement xe2 in xe1.ChildNodes)
25.{
26.//根據(jù)節(jié)點(diǎn)名稱為相應(yīng)的變量賦值
27.if(xe2.Name=='Transform')
28.{
29.position=newVector3(float.Parse(xe2.GetAttribute('x')),float.Parse(xe2.GetAttribute('y')),float.Parse(xe2.GetAttribute('z')));
30.}elseif(xe2.Name=='Rotation')
31.{
32.rotation=newVector3(float.Parse(xe2.GetAttribute('x')),float.Parse(xe2.GetAttribute('y')),float.Parse(xe2.GetAttribute('z')));
33.}else{
34.scale=newVector3(float.Parse(xe2.GetAttribute('x')),float.Parse(xe2.GetAttribute('y')),float.Parse(xe2.GetAttribute('z')));
35.}
36.}
37.//生成物體
38.GameObject go=(GameObject)GameObject.Instantiate(bundle.Load(xe1.GetAttribute('Name')),position,Quaternion.Euler(rotation));
39.go.transform.localScale=scale;
40.}
41.}
42.}
因?yàn)樵摲椒ㄖ械腁ssetBundle是需要從服務(wù)器下載下來的,因此我們需要使用協(xié)程來下載AssetBundle:
01.??? IEnumerator Download()
02.{
03.WWW _www =newWWW ('http://localhost/DoneStealth.unity3d');
04.yieldreturn_www;
05.//檢查是否發(fā)生錯(cuò)誤
06.if(string.IsNullOrEmpty (_www.error))
07.{
08.//檢查AssetBundle是否為空
09.if(_www.assetBundle!=null)
10.{
11.LoadDynamicScene(_www.assetBundle,'DoneStealth.xml');
12.}
13.}
14.}
好了,現(xiàn)在運(yùn)行程序,可以發(fā)現(xiàn)場(chǎng)景將被動(dòng)態(tài)地加載到當(dāng)前場(chǎng)景中:),哈哈

小結(jié)
使
用這種方式來加載場(chǎng)景主要是為了提高游戲的性能,如果存在大量重復(fù)性的場(chǎng)景的時(shí)候,可以使用這種方式來減小游戲的體積,可是這種方式本質(zhì)上是一種用時(shí)間換
效率的方式,因?yàn)樵谑褂眠@種方法前,我們首先要做好游戲場(chǎng)景,然后再導(dǎo)出相關(guān)的配置文件和AssetBundle,從根本上來講,工作量其實(shí)沒有減少。
當(dāng)場(chǎng)景導(dǎo)出的Xml文件中的內(nèi)容較多時(shí),建議使用內(nèi)存池來管理物體的生成和銷毀,因?yàn)轭l繁的生成和銷毀是會(huì)帶來較大的內(nèi)存消耗的。說到這里的時(shí)候,我不得
不吐槽下公司最近的項(xiàng)目,在將近300個(gè)場(chǎng)景中只有30個(gè)場(chǎng)景是最終發(fā)布游戲時(shí)需要打包的場(chǎng)景,然后剩余場(chǎng)景將被用來動(dòng)態(tài)地加載到場(chǎng)景中,因?yàn)轭I(lǐng)導(dǎo)希望可
以實(shí)現(xiàn)動(dòng)態(tài)改變場(chǎng)景的目的,更為郁悶的是整個(gè)場(chǎng)景要高度DIY,模型要能夠隨用戶拖拽移動(dòng)、旋轉(zhuǎn),模型和材質(zhì)要能夠讓用戶自由替換。從整體上來講,頻繁地
銷毀和生成物體會(huì)耗費(fèi)大量資源,因此如果遇到這種情況建議還是使用內(nèi)存池進(jìn)行管理吧!