1、[unity3d]从服务器端获取资源动态加载到场景 分类: 编程语言/ C语言/ 文章 我们的游戏制作完发布出去提供给玩家,为了给玩家带来更好的游戏体验,要做各种的优化以及设计,首先,游戏资源的加载就是一个非常重要的方面(尤其是网页游戏)。由于我们的游戏资源比较大,不能一下全部加载出来,如果是这样,可能会造成玩家长时间的等待。所以我们应该采取动态加载的方式,让玩家在玩游戏的过程中来一点一点从服务器加载游戏资源。要实现这样的效果,首先就必须要制作用于一点点加载的游戏资源。 (注:本文只是谈及这些游戏资源的制作和下载,关于游戏运行中的动态加载不做讨论) (再注:本文涉及到的代码都是以C#
2、语言来编写的) 开发环境: Windows 7 Unity3D 3.5.1f2 本文中将会涉及到以下的内容: 1、 UnityEditor命名空间 2、 Editor模式下窗口制作 3、 导出功能的具体实现 4、 资源的下载 5、 下载后使用 1、 UnityEditor命名空间 这个命名空间下的类是在Unity的编辑模式下使用的,我们可以用它来制作各种小工具来辅助开发,提高开发效率。这里的所有的类都不能在Unity的运行时里使用。只能在编辑器下使用,并且在使用他们的时候还必须要放到项目Project视图下的Editor文件夹中。需要注意一点的就是,我们的项目代码里如果有
3、使用到UnityEditor命名空间时,在项目的最后编译是不能通过的,必须要移除他们。 我们来看一个我们即将使用到的一个Attribute: MenuItem是UnityEditor命名空间下的一个属性标志,它可以定义出一个菜单条目,并添加在Unity编辑器的菜单栏中,语法: [csharp] view plaincopy 1. [MenuItem(“Tools/Export”)] 我们来新建一个工程看一下效果(具体创建步骤这里真的不说了) (注:我的项目中加了很多装饰性的东西,这里就不一一说明怎么实现了哈) 完成之后,先在Project下创建Editor文件夹并创建一个脚本
4、文件,输入以下内容:
[csharp] view plaincopy
1. using UnityEditor;
2. using UnityEngine;
3. using System.Collections;
4.
5. ///
5、 12. { 13. Debug.Log("Menu is selected !!"); 14. } 15. } 当我们点击菜单栏上的对应菜单选项:ToolsàExport时, 菜单项会调用静态的Execute()方法,即可在Console面板中打印出”Menu is selected”。 这里要注意两点: 1、 引入UnityEditor命名空间。 2、 MenuItem要调用的方法需要是static的。 关于UnityEditor的更多详细内容,请参照官方文档,这里不做重点讲解。 2、Editor模式下窗口制作 要制作一个小工具,提供出一个友好界面是很
6、有必要的。UnityEditor下的类可以很方便的完成这一需求。我们通过这些类,可以实现各种不同的控件: 怎么样,还算丰富吧?这些控件的具体实现我不想说,请自行查看API吧。 这里我还是遵循本文的主旨,围绕本文的中心思想(本文我们是要导出资源到服务器,并在游戏中下载这个资源过来使用)实现一个界面。 用例描述: 导出场景中的一个模型,并带着默认材质,如果该模型有多个可替换的贴图,也把这些贴图作为该模型的资源一并导出到一个资源包中。 按照这个需求,我猜想界面应该是这样的: 一个导出模型的口,一个提供可选贴图数量的口,根据用户输入的可选数量,给提供出对应的贴图导出口,最后填写完毕之后
7、有一个按钮用于导出交互。
,不好意思,这哪里是猜想,我其实早就写好了。其实也没骗你了,我在写之前是猜想的!
要实现上面这个窗口,我该怎么做呢?
首先,定义一个继承EditorWindow的类,然后,重写OnGUI方法即可。我们这里在之前的代码基础上做修改添加:
[csharp] view plaincopy
1. using UnityEditor;
2. using UnityEngine;
3.
4. ///
8、c class ExportTools : "color:#ff0000;">EditorWindow
8. {
9. [MenuItem("Tools/Export")]
10. static void Execute ()
11. {
12. // 实例化一个Window窗口 //
13. ExportTools windows = EditorWindow.GetWindow
9、UI() 17. { 18. 19. } 20. } 这里要注意的就是将原来的脚本有继承自MonoBehaviour 修改为继承自EditorWindow。并在Execute ()方法中对当前的Window实例化。这时我们就可以得到一个Window窗口了: 其次,就是向我们生成的窗口中添加不同的控件,这些控件的生成都是在OnGUI()方法中实现的。和MonoBehaviour的OnGUI方法一样,EditorWindow的OnGUI()方法也主要是处理UI的,我们关于UI控件的生成处理都要写在这个方法里。OnGUI()这个方法每帧调用好几次(每个事件一次),所以一些逻辑
10、处理要避免在这里调用。 [csharp] view plaincopy 1. private string savePath; 2. private GameObject exportObject; 3. private int optionalCount = 0; 4. private Texture2D[] optionalTexture = new Texture2D[0]; 5. 6. void OnGUI() 7. { 8. /* 9. * ObjectField: 10. * 是这里的第一个控件,它可以允许用户拖拽将一个Object的对象赋给
11、它。 11. * 如果要限制可接收的对象类型,可以通过第三个参数来限制类型这里表示直接收GameObject类型 12. * 第四个bool型的参数标志能否接受当前scene里的对象,true表示接受 13. * 这个方法返回的是一个Object类型的值,最后要将它转化为需要的类型 14. */ 15. exportObject = EditorGUILayout.ObjectField("Export Object", exportObject, 16. typeof(GameObject), true) 17. as GameObject; 18. // 就
12、相当于提供一个换行,用于格式化控件的 //
19. EditorGUILayout.Space();
20. // IntField:该控件只能输入 int 类型的值//
21. optionalCount = EditorGUILayout.IntField("Optional Count", optionalCount);
22. for(int i=0; i 13、 new Texture2D[optionalCount];
27. }
28.
29. EditorGUILayout.Space();
30. // 这里将 ObjectField 限制只接受Texture2D类型的值 //
31. optionalTexture[i] = EditorGUILayout.ObjectField("Optional Textures " + i, optionalTexture[i],
32. typeof(Texture2D), false)
33. as Texture2D;
34. }
35.
36. Edito 14、rGUILayout.Space();
37. EditorGUILayout.Space();
38.
39. EditorGUILayout.BeginHorizontal();
40. EditorGUILayout.Space();
41. // 导出按钮 //
42. if(GUILayout.Button("EXPORT", GUILayout.Width(100), GUILayout.Height(20)))
43. {
44.
45. }
46.
47. EditorGUILayout.EndHorizontal();
48. }
15、
这里一些必要的东西我都添加都注释理了,就不重复了。
到这里这个窗口就基本算是完成了。
3、导出功能的具体实现
以上只是实现出了这样一个窗口,具体响应功能,以及必要的逻辑实现还都不具备,这里我们将为这个窗口添加具体的功能实现代码。
[csharp] view plaincopy
1. "white-space:pre"> private void ExportAndSave(GameObject go)
2. {
3. //该方法将打开保存对话框,选择导出文件的保存位置//
4. savePath = EditorUtility.Save 16、FilePanel("Save", @"E:\", go.name, "unity3d");
5. Export(go, savePath);
6. }
7.
8. private void Export(GameObject go, string filePath)
9. {
10. // IsPersistent 判断传入的对象是磁盘文件还是场景文件(即是否是Project视图下的文件,是返回true)//
11. if(!EditorUtility.IsPersistent(go))
12. {
13. GameObject tmp = GameObje 17、ct.Instantiate(go) as GameObject;
14. go = GetPrefab(tmp, go.name) as GameObject;
15. }
16. Object[] asset = optionalTexture;
17. if(File.Exists(filePath)) File.Delete(filePath);
18. /*
19. BuildPipeline.BuildAssetBundle():该方法是将提供的对象导出成Unity能识别的二进制文件
20. 第一个参数是提供一个要导出的对象,第二个参数是一个Object 18、[]类型,它可以将数据附加到第一个
21. 参数定义的主数据一起整体导出.但是这两个参数要求必须是磁盘文件的格式,所以上面的if语句判断
22. 是否是磁盘文件类型,如果不是,先将其转化为prefab,在Assets下临时保存一下。这个转化就是要
23. 用到 PrefabUtility 类里的方法。
24. */
25. BuildPipeline.BuildAssetBundle(go, asset, filePath, BuildAssetBundleOptions.CollectDependencies, BuildTarget.StandaloneWindows) 19、
26. // 将暂时生成的prefab文件使用完后删除 //
27. AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(go));
28. }
29.
30. /// 20、"Assets/" + name + ".prefab");
36. result = PrefabUtility.ReplacePrefab(go, result);
37. Object.DestroyImmediate(go);
38. return result;
39. }
这里我又新添加了三个方法来具体实现导出并保存的逻辑:
· private voidExportAndSave(GameObject go):
在这个方法里只要关注一下怎么打开一个保存对话框就可以了
[csharp] view plaincopy
1. //该方法将打开保存对话框,选择 21、导出文件的保存位置。第二和第三个参数表示默认保存位置和默认文件名//
2. savePath =EditorUtility.SaveFilePanel("Save", @"E:\", go.name,"unity3d");
· private void Export(GameObjectgo, string filePath)
这个方法具体实现了导出二进制文件的功能。这里需要说明的是 BuildPipeline.BuildAssetBundle(): 该方法是将提供的对象导出成Unity能识别的二进制文件第一个参数是提供一个要导出的对象,第二个参数是一个Object[]类型,它可以将 22、数据附加到第一个参数定义的主数据一起整体导出.但是这两个参数要求必须是磁盘文件的格式,所以上面的if语句判断是否是磁盘文件类型,如果不是,先将其转化为prefab,在Assets下临时保存一下。这个转化就是要用到 PrefabUtility 类里的方法。具体判断是否是磁盘文件,是通过 if(!EditorUtility.IsPersistent(go))这一句来判断的:如果go不是磁盘文件,是场景对象,则执行该语句里的代码来生成磁盘文件,具体的是下面这个方法来实现的。
· private ObjectGetPrefab(GameObject go, string name)
我们在导出前, 23、如果导出信息设置的不正确,可能会致使导出的文件有问题或者不可用,所以在导出之前对信息有效性的验证也是必要的:
[csharp] view plaincopy
1. "white-space:pre"> /// 24、Texture2D t in optionalTexture)
10. {
11. b2 = b2 || (t == null);
12. }
13.
14. return !(b1 || b2);
15. }
如果用户全部信息都填写完整了,该方法会返回true,导出时可以根据返回值状态来做相应的响应。
[csharp] view plaincopy
1. "white-space:pre"> // 导出按钮 //
2. if(GUILayout.Button("EXPORT", GUILayout.Width(100), GUI 25、Layout.Height(20)))
3. {
4. if(Validate())
5. {
6. ExportAndSave(exportObject);
7. Clear();//成功导出数据后,清除导出信息//
8. }
9. else
10. {
11. //导出信息填写有误时,给出提示//
12. EditorUtility.DisplayDialog("错误提示", "导出信息设置有误,请返回检查!", "确定");
13. }
14. }
这里可以看到我还添加了一个Clear()方法,该方法在用户导出完毕时,将导出工具面板的信 26、息清除掉,以便开始导出其它资源:
[csharp] view plaincopy
1. "white-space:pre"> /// 27、
[csharp] view plaincopy
1. using System.IO;
2. using UnityEditor;
3. using UnityEngine;
4.
5. /// 28、ndow窗口 //
14. EditorWindow.GetWindow 29、25. * ObjectField:
26. * 是这里的第一个控件,它可以允许用户拖拽将一个Object的对象赋给它。
27. * 如果要限制可接收的对象类型,可以通过第三个参数来限制类型这里表示直接收GameObject类型
28. * 第四个bool型的参数标志能否接受当前scene里的对象,true表示接受
29. * 这个方法返回的是一个Object类型的值,最后要将它转化为需要的类型
30. */
31. exportObject = EditorGUILayout.ObjectField("Export Object", exportObject,
3 30、2. typeof(GameObject), true)
33. as GameObject;
34. // 就相当于提供一个换行,用于格式化控件的 //
35. EditorGUILayout.Space();
36. // IntField:该控件只能输入 int 类型的值//
37. optionalCount = EditorGUILayout.IntField("Optional Count", optionalCount);
38. for(int i=0; i 31、ture.Length != optionalCount)
41. {
42. optionalTexture = new Texture2D[optionalCount];
43. }
44.
45. EditorGUILayout.Space();
46. // 这里将 ObjectField 限制只接受Texture2D类型的值 //
47. optionalTexture[i] = EditorGUILayout.ObjectField("Optional Textures " + i, optionalTexture[i],
48. typeof(Tex 32、ture2D), false)
49. as Texture2D;
50. }
51.
52. EditorGUILayout.Space();
53. EditorGUILayout.Space();
54.
55. EditorGUILayout.BeginHorizontal();
56. EditorGUILayout.Space();
57. // 导出按钮 //
58. if(GUILayout.Button("EXPORT", GUILayout.Width(100), GUILayout.Height(20)))
59. {
60. 33、if(Validate())
61. {
62. ExportAndSave(exportObject);
63. Clear();//成功导出数据后,清除导出信息//
64. }
65. else
66. {
67. //导出信息填写有误时,给出提示//
68. EditorUtility.DisplayDialog("错误提示", "导出信息设置有误,请返回检查!", "确定");
69. }
70. }
71.
72. EditorGUILayout.EndHorizontal();
73. }
74.
75. private vo 34、id ExportAndSave(GameObject go)
76. {
77. //该方法将打开保存对话框,选择导出文件的保存位置。第二和第三个参数表示默认保存位置和默认文件名//
78. savePath = EditorUtility.SaveFilePanel("Save", @"E:\", go.name, "unity3d");
79. Export(go, savePath);
80. }
81.
82. private void Export(GameObject go, string filePath)
83. {
84. // IsPer 35、sistent 判断传入的对象是磁盘文件还是场景文件(即是否是Project视图下的文件,是返回true)//
85. if(!EditorUtility.IsPersistent(go))
86. {
87. GameObject tmp = GameObject.Instantiate(go) as GameObject;
88. go = GetPrefab(tmp, go.name) as GameObject;
89. }
90. //Texture2D本身就是磁盘文件了,这里就没必要再转化了//
91. Object[] asset = optional 36、Texture;
92. if(File.Exists(filePath)) File.Delete(filePath);
93. /*
94. BuildPipeline.BuildAssetBundle():该方法是将提供的对象导出成Unity能识别的二进制文件
95. 第一个参数是提供一个要导出的对象,第二个参数是一个Object[]类型,它可以将数据附加到第一个
96. 参数定义的主数据一起整体导出.但是这两个参数要求必须是磁盘文件的格式,所以上面的if语句判断
97. 是否是磁盘文件类型,如果不是,先将其转化为prefab,在Assets下临时保存一下。这个转 37、化就是要
98. 用到 PrefabUtility 类里的方法。
99. */
100. BuildPipeline.BuildAssetBundle(go, asset, filePath, BuildAssetBundleOptions.CollectDependencies, BuildTarget.StandaloneWindows);
101. // 将暂时生成的prefab文件使用完后删除 //
102. AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(go));
103. }
104.
105 38、 /// 39、ediate(go);
113. return result;
114. }
115.
116. /// 40、 b2 = b2 || (t == null);
127. }
128.
129. return !(b1 || b2);
130. }
131.
132. /// 41、过这个小小的导出工具就可以制作出要需要的资源文件了,这些资源文件是存放在服务器上的,接下来我们一起看看关于这些资源文件的下载。
4、获取资源文件
这些文件是可以就从本地磁盘加载进游戏里使用的,但这里为了模拟从远程服务器下载这样一个模式,我还是将刚刚制作好的文件上传到远程主机来给大家展示一下这种的从远端获取的做法(其实从本地磁盘加载几乎是一样的)。
第一步:将文件上传到服务器。
我真的没有服务器,但是我感觉度娘很热情,估计能帮上我们什么忙。
(此处省略好几个字。。。。。。其实就是怎么将刚刚导出的文件上传到“百度云”)
上传不说了,这里看看怎么获取刚刚上传资源的完整地址。
用Goog 42、le浏览器(码农用这个没有什么大问题吧?)登上“百度云”,找到刚刚上传的文件,点击下载,然后按Ctrl+J打开下载列表,右击正在下载的文件,选择“复制链接地址”就可以取到该文件的完整地址了。
这个是我的:
这里我们暂且先这样用着,在真正的项目开发中,资源的地址肯定会直接或间接的给出来的,这里不必纠结。
我们来具体看看下载,这里下载要使用到的类是WWW。在实例化WWW的时候,我们只需将资源的url地址给它,即可开始下载,实例化完WWW后我们只需判断这个实例是否下载完成,如果完成了,即可以取下载来的资源来用了。代码是这样的:(这个类不是UnityEditor里的类,新建一个C#类 43、并继承自MonoBehaviour)
[csharp] view plaincopy
1. using UnityEngine;
2. using System.Collections;
3. /// 44、
13. this.www = new WWW(this.url);
14. }
15.
16. void Update ()
17. {
18. if(www == null) return;
19. if(www.isDone)
20. {
21. print ("Download completed");
22. }
23. }
24. }
当启动了Unity之后,会发现很快就会在Console视图中打印出来了“Download completed”,而且还孜孜不倦的一直不肯停歇,这里我们下载完了,只要对下载完的资源处理一次就够了,没必要 45、没完没了的处理,多浪费感情啊,所以我们该定义一个标志,来标记下载完成这么一个状态:
[csharp] view plaincopy
1. private bool isCompleted = false;
2. void Update ()
3. {
4. if(www == null) return;
5. if(!isCompleted && www.isDone)
6. {
7. print ("Download completed");
8. isCompleted = true;
9. }
10. }
现在是不是只有这么一条打印信息了?
46、
这段代码是非常简单的,这里也没有什么要多说的,就是提这么一点,这里我们是直接根据资源的URL去访问下载的该资源,但在实际项目中,我们经常要处理的是根据不同的条件访问同一地址而返回不同的数据来使用,这里要使用的是WWW的另一个构造方法,可以带除URL外的其它请求参数:
[csharp] view plaincopy
1. private void WWWWithParameter(string url, string parameter)
2. {
3. WWWForm form = new WWWForm();
4. form.AddField("Content", par 47、ameter);
5. WWW www = new WWW(url, form);
6. }
可以看到,只需将参数封装在WWWForm中再去用WWW访问服务器就可以了。
(本例中我们没有采用带参数的访问是因为这样的话,我们还要加一个后台处理程序,要根据请求参数来返回数据,这样我们就必须要在本机上安装服务器,书写服务器代码等等等等,这样就得多做很多其它与我们这个话题相去深远的工作了。。。。。。。。(好吧,我承认我不会配置服务器))
到此本节的全部代码是这样子的:
[csharp] view plaincopy
1. using UnityEngine;
2. using System.Collections;
3. ///






