Unity3d游戏开发之使用AssetBundle和Xml实现场景的动态加载

在Unity3D游戏开发过程中,因为受到游戏容量、平台性能和热更新等诸多因素的限制,我们可能无法将所有的游戏场景打包到项目中然后相对”静态”地加载,那么这个时候就需要我们使用动态加载的方式来将游戏场景加载到场景中。博主在研究了Unity3D动态加载的相关资料后发现,目前Unity3D中实现动态加载场景的方式主要有以下两种方式:

* 使用BuildStreamedSceneAssetBundle()方法将场景打包为AssetBundle:这种方法将生成一个流式的.unity3d文件,从而实现按需下载和加载,因此这种方式特别适合Web环境下游戏场景的加载,因为在Web环境下我们可以希望的是玩家可以在玩游戏的同时加载游戏。可是因为这种打包方式仅仅是保证了场景中的GameObject与本地资源的引用关系而非是将本地资源打包,因此从减少游戏容量的角度来说并不是十分实用,而且当我们使用WWW下载完AssetBundle后,需要使用Application.Load()方法来加载场景,我们知道在Unity3D中加载一个关卡(场景)是需要在BuildSetting中注册关卡的,因此在使用这种方式动态加载的时候请注意到这一点。

* 将场景内的所有物体打包为AssetBundle配合相关配置文件动态生成场景:这种方法的思路是使用一个配置文件来记录下当前场景中所有物体的位置、旋转和缩放信息,然后再根据配置文件使用Instantiate方法逐个生成即可。这种思路是考虑到需要在一个场景中动态替换GameObject或者是动态生成GameObject的情形,使用这种方法首先要满足一个条件,即:场景内所有的物体都是预制件(Prefab)。这是由Unity3D的机制决定的,因为Prefab是一个模板,当你需要动态生成一个物体的时候就需要为其提供一个模板(Prefab)。

如果你对这两种方式没有什么疑问的话,那么我觉得我们可以正式开始今天的内容了。既然今天的题目已然告诉大家是使用AssetBundle和Xml文件实现场景的动态加载,我相信大家已经明白我要使用那种方式了。好了,下面我们正式开始吧!

准备工作

在实现场景的动态加载前,我们首先要在本地准备好一个游戏场景,然后做两件事情:

* 将场景内的所有GameObject打包为AssetBundle

* 将场景内所有的GameObject的信息导出为Xml文件

做这两件事情的时候,相当于我们是在准备食材和菜谱,有了食材和菜谱我们就可以烹制出美味佳肴了。可是在做着两件事情前,我们还有一件更为重要的事情要做,那就是我们需要将场景中使用到的GameObject制作成预制体(Prefab)。因为在博主的印象中,Unity3D打包的最小粒度应该是Prefab,所以为了保险起见,我还是建议大家将场景中使用到的GameObject制作成预制体(Prefab)。那么问题来了,当我们将这些Prefab打包成AssetBundle后是否还需要本地的Prefab文件?这里博主一直迷惑,因为理论上当我们将这些Prefab打包成AssetBundle后,我们实例化一个物体的时候实际上是在使用AssetBundle的Load方法来获取该物体的一个模板,这个模板应该是存储在AssetBundle中的啊!因为我的笔记本使用的是免费版的Unity3D无法对此进行测试,所以如果想知道这个问题结果的朋友可以等我下周到公司以后测试了再做讨论(我不会告诉你公司无耻地使用了破解版),当然如果有知道这个问题的答案的朋友欢迎给我留言啊,哈哈!这里就是想告诉大家要准备好场景中物体的预设体(Prefab),重要的事情说三遍!!!

将场景内物体打包为AssetBundle

Unity3D打包的相关内容这里就不展开说了,因为在官方API文档中都能找到详细的说明,虽然说Unity5.0中AssetBundle打包

的方式发生了变化,不过考虑到大家都还在使用4.X的版本,所以等以后我用上了Unity5.0再说吧,哈哈!好了,下面直接给出代码:

view sourceprint?

01.    [MenuItem('Export/ExportTotal----对物体整体打包')]

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.}

将场景内物体信息导出为Xml文件

导出场景内物体信息需要遍历场景中的每个游戏物体,因为我们在制作场景的时

候通常会用一个空的GameObject作为父物体来组织场景中的各种物体,因此我们在导出Xml文件的时候仅仅考虑导出这些父物体,因为如果考虑子物体

的话,可能会涉及到递归,整个问题将变得特别复杂。为了简化问题,我们这里仅仅考虑场景中的父物体。好了,开始写代码:

view sourceprint?

01.    [MenuItem('Export/ExportScene----将当前场景导出为Xml')]

02.staticvoidExportGameObjects()

03.{

04.//获取当前场景完整路径

05.string scenePath=EditorApplication.currentScene;

06.//获取当前场景名称

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('输出场景内物体','',sceneName,'xml');

11.//创建Xml文件

12.XmlDocument xmlDoc=newXmlDocument();

13.//创建根节点

14.XmlElement scene=xmlDoc.CreateElement('Scene');

15.scene.SetAttribute('Name',sceneName);

16.scene.SetAttribute('Asset',scenePath);

17.xmlDoc.AppendChild(scene);

18.//遍历场景中的所有物体

19.foreach(GameObject go in Object.FindObjectsOfType(typeof(GameObject)))

20.{

21.//仅导出场景中的父物体

22.if(go.transform.parent==null)

23.{

24.//创建每个物体

25.XmlElement gameObject=xmlDoc.CreateElement('GameObject');

26.gameObject.SetAttribute('Name',go.name);

27.gameObject.SetAttribute('Asset','Prefabs/'+ go.name +'.prefab');

28.//创建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.//创建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.//创建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.//添加物体到根节点

47.scene.AppendChild(gameObject);

48.}

49.}

50.

51.xmlDoc.Save(savePath);

52.}

53.

好了,在这段代码中我们以Scene作为根节点,然后以每个GameObject作为Scene的子节点,重点在Xml文件中记录了每个GameObject的名称、Prefab、坐标、旋转和缩放等信息。下面是一个导出场景的Xml文件的部分内容:

view sourceprint?

01.

02.

03.

04.

05.

06.

07.

08.

09.

10.

11.

12.

13.

14.

15.

16.

17.

18.

19.

20.

21.

在这里我们假设所有的Prefab是放置在Resources/Prefabs目录中的,那么此时我们便有了两种动态加载场景的方式

* 通过每个GameObject的Asset属性,配合Resources.Load()方法实现动态加载

* 通过每个GameObject的Name属性,配合AssetBundle的Load()方法实现动态加载

这两种方法大同小异,区别仅仅在于是否需要从服务器下载相关资源。因此本文的主题是使用AssetBundle和Xml实现场景的动态加载,因此,接下来我们主要以第二种方式为主,第一种方式请大家自行实现吧!

动态加载物体到场景中

首先我们来定义一个根据配置文件动态加载AssetBundle中场景的方法LoadDynamicScene

view sourceprint?

01.///

02./// 根据配置文件动态加载AssetBundle中的场景

03.///

04./// 从服务器上下载的AssetBundle文件

05./// AssetBundle文件对应的场景配置文件

06.publicstaticvoidLoadDynamicScene(AssetBundle bundle,string xmlFile)

07.{

08.//加载本地配置文件

09.XmlDocument xmlDoc=newXmlDocument();

10.xmlDoc.LoadXml(((TextAsset)Resources.Load(xmlFile)).text);

11.//读取根节点

12.XmlElement root=xmlDoc.DocumentElement;

13.if(root.Name=='Scene')

14.{

15.XmlNodeList nodes=root.SelectNodes('/Scene/GameObject');

16.//定义物体位置、旋转和缩放

17.Vector3 position=Vector3.zero;

18.Vector3 rotation=Vector3.zero;

19.Vector3 scale=Vector3.zero;

20.//遍历每一个物体

21.foreach(XmlElement xe1 in nodes)

22.{

23.//遍历每一个物体的属性节点

24.foreach(XmlElement xe2 in xe1.ChildNodes)

25.{

26.//根据节点名称为相应的变量赋值

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.}

因为该方法中的AssetBundle是需要从服务器下载下来的,因此我们需要使用协程来下载AssetBundle:

view sourceprint?

01.    IEnumerator Download()

02.{

03.WWW _www =newWWW ('http://localhost/DoneStealth.unity3d');

04.yieldreturn_www;

05.//检查是否发生错误

06.if(string.IsNullOrEmpty (_www.error))

07.{

08.//检查AssetBundle是否为空

09.if(_www.assetBundle!=null)

10.{

11.LoadDynamicScene(_www.assetBundle,'DoneStealth.xml');

12.}

13.}

14.}

好了,现在运行程序,可以发现场景将被动态地加载到当前场景中:),哈哈

小结

使

用这种方式来加载场景主要是为了提高游戏的性能,如果存在大量重复性的场景的时候,可以使用这种方式来减小游戏的体积,可是这种方式本质上是一种用时间换

效率的方式,因为在使用这种方法前,我们首先要做好游戏场景,然后再导出相关的配置文件和AssetBundle,从根本上来讲,工作量其实没有减少。

当场景导出的Xml文件中的内容较多时,建议使用内存池来管理物体的生成和销毁,因为频繁的生成和销毁是会带来较大的内存消耗的。说到这里的时候,我不得

不吐槽下公司最近的项目,在将近300个场景中只有30个场景是最终发布游戏时需要打包的场景,然后剩余场景将被用来动态地加载到场景中,因为领导希望可

以实现动态改变场景的目的,更为郁闷的是整个场景要高度DIY,模型要能够随用户拖拽移动、旋转,模型和材质要能够让用户自由替换。从整体上来讲,频繁地

销毁和生成物体会耗费大量资源,因此如果遇到这种情况建议还是使用内存池进行管理吧!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容