Unity3d游戲開發(fā)之使用AssetBundle和Xml實(shí)現(xiàn)場(chǎng)景的動(dòng)態(tài)加載

在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再說吧,哈哈!好了,下面直接給出代碼:

view sourceprint?

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)景中的父物體。好了,開始寫代碼:

view sourceprint?

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)容:

view sourceprint?

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

view sourceprint?

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:

view sourceprint?

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)行管理吧!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容