天天看点

[unity3d]从服务器端获取资源动态加载到场景

我们的游戏制作完发布出去提供给玩家,为了给玩家带来更好的游戏体验,要做各种的优化以及设计,首先,游戏资源的加载就是一个非常重要的方面(尤其是网页游戏)。由于我们的游戏资源比较大,不能一下全部加载出来,如果是这样,可能会造成玩家长时间的等待。所以我们应该采取动态加载的方式,让玩家在玩游戏的过程中来一点一点从服务器加载游戏资源。要实现这样的效果,首先就必须要制作用于一点点加载的游戏资源。

(注:本文只是谈及这些游戏资源的制作和下载,关于游戏运行中的动态加载不做讨论)

(再注:本文涉及到的代码都是以C#语言来编写的)

开发环境:

Windows 7

Unity3D 3.5.1f2

本文中将会涉及到以下的内容:

3、 导出功能的具体实现

4、 资源的下载

5、 下载后使用

1、 UnityEditor命名空间

这个命名空间下的类是在Unity的编辑模式下使用的,我们可以用它来制作各种小工具来辅助开发,提高开发效率。这里的所有的类都不能在Unity的运行时里使用。只能在编辑器下使用,并且在使用他们的时候还必须要放到项目Project视图下的Editor文件夹中。需要注意一点的就是,我们的项目代码里如果有使用到UnityEditor命名空间时,在项目的最后编译是不能通过的,必须要移除他们。

我们来看一个我们即将使用到的一个Attribute:

MenuItem是UnityEditor命名空间下的一个属性标志,它可以定义出一个菜单条目,并添加在Unity编辑器的菜单栏中,语法:

[MenuItem(“Tools/Export”)]  

我们来新建一个工程看一下效果(具体创建步骤这里真的不说了)

(注:我的项目中加了很多装饰性的东西,这里就不一一说明怎么实现了哈)

完成之后,先在Project下创建Editor文件夹并创建一个脚本文件,输入以下内容:

using UnityEditor;  

using UnityEngine;  

using System.Collections;  

/// <summary>  

/// author : qyxls  

/// </summary>  

public class ExportTools : MonoBehaviour   

{  

    [MenuItem("Tools/Export")]  

    static void Execute ()   

    {  

        Debug.Log("Menu is selected !!");  

    }  

}  

当我们点击菜单栏上的对应菜单选项:ToolsàExport时,

菜单项会调用静态的Execute()方法,即可在Console面板中打印出”Menu is selected”。

这里要注意两点:

1、 引入UnityEditor命名空间。

2、 MenuItem要调用的方法需要是static的。

关于UnityEditor的更多详细内容,请参照官方文档,这里不做重点讲解。

2、Editor模式下窗口制作

       要制作一个小工具,提供出一个友好界面是很有必要的。UnityEditor下的类可以很方便的完成这一需求。我们通过这些类,可以实现各种不同的控件:

怎么样,还算丰富吧?这些控件的具体实现我不想说,请自行查看API吧。

这里我还是遵循本文的主旨,围绕本文的中心思想(本文我们是要导出资源到服务器,并在游戏中下载这个资源过来使用)实现一个界面。

用例描述:

导出场景中的一个模型,并带着默认材质,如果该模型有多个可替换的贴图,也把这些贴图作为该模型的资源一并导出到一个资源包中。

按照这个需求,我猜想界面应该是这样的:

一个导出模型的口,一个提供可选贴图数量的口,根据用户输入的可选数量,给提供出对应的贴图导出口,最后填写完毕之后有一个按钮用于导出交互。

,不好意思,这哪里是猜想,我其实早就写好了。其实也没骗你了,我在写之前是猜想的!

要实现上面这个窗口,我该怎么做呢?

       首先,定义一个继承EditorWindow的类,然后,重写OnGUI方法即可。我们这里在之前的代码基础上做修改添加:

public class ExportTools :<strong> <span style="color:#ff0000;">EditorWindow</span> </strong>  

// 实例化一个Window窗口 //  

        ExportTools windows = EditorWindow.GetWindow<ExportTools>(true, "Export Tools");  

    void OnGUI()  

这里要注意的就是将原来的脚本有继承自MonoBehaviour 修改为继承自EditorWindow。并在Execute ()方法中对当前的Window实例化。这时我们就可以得到一个Window窗口了:

[unity3d]从服务器端获取资源动态加载到场景

private string savePath;  

private GameObject exportObject;  

private int optionalCount = 0;  

private Texture2D[] optionalTexture = new Texture2D[0];  

void OnGUI()  

    /* 

     * ObjectField: 

     * 是这里的第一个控件,它可以允许用户拖拽将一个Object的对象赋给它。 

     * 如果要限制可接收的对象类型,可以通过第三个参数来限制类型这里表示直接收GameObject类型 

     * 第四个bool型的参数标志能否接受当前scene里的对象,true表示接受 

     * 这个方法返回的是一个Object类型的值,最后要将它转化为需要的类型 

     */  

    exportObject = EditorGUILayout.ObjectField("Export Object", exportObject,   

                                                typeof(GameObject), true)   

                                                as GameObject;  

    // 就相当于提供一个换行,用于格式化控件的 //  

    EditorGUILayout.Space();  

    // IntField:该控件只能输入 int 类型的值//  

    optionalCount = EditorGUILayout.IntField("Optional Count", optionalCount);  

    for(int i=0; i<optionalCount; i++)  

        if(optionalTexture.Length != optionalCount)  

        {  

            optionalTexture = new Texture2D[optionalCount];  

        }  

        EditorGUILayout.Space();  

        // 这里将 ObjectField 限制只接受Texture2D类型的值 //  

        optionalTexture[i] = EditorGUILayout.ObjectField("Optional Textures " + i, optionalTexture[i],   

                                                          typeof(Texture2D), false)   

                                                          as Texture2D;  

    EditorGUILayout.BeginHorizontal();  

    // 导出按钮 //  

    if(GUILayout.Button("EXPORT", GUILayout.Width(100), GUILayout.Height(20)))  

    EditorGUILayout.EndHorizontal();  

这里一些必要的东西我都添加都注释理了,就不重复了。

到这里这个窗口就基本算是完成了。

[unity3d]从服务器端获取资源动态加载到场景

3、导出功能的具体实现

以上只是实现出了这样一个窗口,具体响应功能,以及必要的逻辑实现还都不具备,这里我们将为这个窗口添加具体的功能实现代码。

<span style="white-space:pre">  </span>private void ExportAndSave(GameObject go)  

        //该方法将打开保存对话框,选择导出文件的保存位置//  

        savePath = EditorUtility.SaveFilePanel("Save", @"E:\", go.name, "unity3d");  

        Export(go, savePath);  

    private void Export(GameObject go, string filePath)  

        // IsPersistent 判断传入的对象是磁盘文件还是场景文件(即是否是Project视图下的文件,是返回true)//  

        if(!EditorUtility.IsPersistent(go))  

            GameObject tmp = GameObject.Instantiate(go) as GameObject;  

            go = GetPrefab(tmp, go.name) as GameObject;  

        Object[] asset = optionalTexture;  

        if(File.Exists(filePath)) File.Delete(filePath);  

        /* 

            BuildPipeline.BuildAssetBundle():该方法是将提供的对象导出成Unity能识别的二进制文件 

            第一个参数是提供一个要导出的对象,第二个参数是一个Object[]类型,它可以将数据附加到第一个 

            参数定义的主数据一起整体导出.但是这两个参数要求必须是磁盘文件的格式,所以上面的if语句判断 

            是否是磁盘文件类型,如果不是,先将其转化为prefab,在Assets下临时保存一下。这个转化就是要 

            用到 PrefabUtility 类里的方法。 

        */  

        BuildPipeline.BuildAssetBundle(go, asset, filePath, BuildAssetBundleOptions.CollectDependencies, BuildTarget.StandaloneWindows);  

        // 将暂时生成的prefab文件使用完后删除 //  

        AssetDatabase.DeleteAsset(AssetDatabase.GetAssetPath(go));  

    /// <summary>  

    /// 该方法来产生临时prefab文件  

    /// </param>  

    private Object GetPrefab(GameObject go, string name)  

        Object result = PrefabUtility.CreateEmptyPrefab("Assets/" + name + ".prefab");  

        result = PrefabUtility.ReplacePrefab(go, result);  

        Object.DestroyImmediate(go);  

        return result;  

这里我又新添加了三个方法来具体实现导出并保存的逻辑:

 private voidExportAndSave(GameObject go):

在这个方法里只要关注一下怎么打开一个保存对话框就可以了

//该方法将打开保存对话框,选择导出文件的保存位置。第二和第三个参数表示默认保存位置和默认文件名//  

savePath =EditorUtility.SaveFilePanel("Save", @"E:\", go.name,"unity3d");  

private void Export(GameObjectgo, string filePath)

这个方法具体实现了导出二进制文件的功能。这里需要说明的是 BuildPipeline.BuildAssetBundle(): 该方法是将提供的对象导出成Unity能识别的二进制文件第一个参数是提供一个要导出的对象,第二个参数是一个Object[]类型,它可以将数据附加到第一个参数定义的主数据一起整体导出.但是这两个参数要求必须是磁盘文件的格式,所以上面的if语句判断是否是磁盘文件类型,如果不是,先将其转化为prefab,在Assets下临时保存一下。这个转化就是要用到 PrefabUtility 类里的方法。具体判断是否是磁盘文件,是通过 if(!EditorUtility.IsPersistent(go))这一句来判断的:如果go不是磁盘文件,是场景对象,则执行该语句里的代码来生成磁盘文件,具体的是下面这个方法来实现的。

private ObjectGetPrefab(GameObject go, string name)

我们在导出前,如果导出信息设置的不正确,可能会致使导出的文件有问题或者不可用,所以在导出之前对信息有效性的验证也是必要的:

<span style="white-space:pre">  </span>/// <summary>  

    /// 数据验证,如果导出信息填写有误,将给用户错误提示  

    /// </summary>  

    private bool Validate()  

        bool b1 = (exportObject == null);  

        bool b2 = false;  

        foreach(Texture2D t in optionalTexture)  

            b2 = b2 || (t == null);  

        return !(b1 || b2);  

如果用户全部信息都填写完整了,该方法会返回true,导出时可以根据返回值状态来做相应的响应。

<span style="white-space:pre">      </span>// 导出按钮 //  

        if(GUILayout.Button("EXPORT", GUILayout.Width(100), GUILayout.Height(20)))  

            if(Validate())  

            {  

                ExportAndSave(exportObject);  

                Clear();//成功导出数据后,清除导出信息//  

            }  

            else  

                //导出信息填写有误时,给出提示//  

                EditorUtility.DisplayDialog("错误提示", "导出信息设置有误,请返回检查!", "确定");  

这里可以看到我还添加了一个Clear()方法,该方法在用户导出完毕时,将导出工具面板的信息清除掉,以便开始导出其它资源:

    /// 所有数据正确导出后,清除填写的导出信息,以便导出下一条数据  

    private void Clear()  

        exportObject = null;  

        optionalCount = 0;  

到这里,我们导出的所有逻辑就完成了,这样子的一个导出工具也基本完成了。此时,我们的完整代码应该是这个样子的:

using System.IO;  

public class ExportTools : EditorWindow   

        // 实例化一个Window窗口 //  

        EditorWindow.GetWindow<ExportTools>(true, "Export Tools");  

    private string savePath;  

    private GameObject exportObject;  

    private int optionalCount = 0;  

    private Texture2D[] optionalTexture = new Texture2D[0];  

         * ObjectField: 

         * 是这里的第一个控件,它可以允许用户拖拽将一个Object的对象赋给它。 

         * 如果要限制可接收的对象类型,可以通过第三个参数来限制类型这里表示直接收GameObject类型 

         * 第四个bool型的参数标志能否接受当前scene里的对象,true表示接受 

         * 这个方法返回的是一个Object类型的值,最后要将它转化为需要的类型 

         */  

        exportObject = EditorGUILayout.ObjectField("Export Object", exportObject,   

                                                    typeof(GameObject), true)   

                                                    as GameObject;  

        // 就相当于提供一个换行,用于格式化控件的 //  

        // IntField:该控件只能输入 int 类型的值//  

        optionalCount = EditorGUILayout.IntField("Optional Count", optionalCount);  

        for(int i=0; i<optionalCount; i++)  

            if(optionalTexture.Length != optionalCount)  

                optionalTexture = new Texture2D[optionalCount];  

            EditorGUILayout.Space();  

            // 这里将 ObjectField 限制只接受Texture2D类型的值 //  

            optionalTexture[i] = EditorGUILayout.ObjectField("Optional Textures " + i, optionalTexture[i],   

                                                              typeof(Texture2D), false)   

                                                              as Texture2D;  

        EditorGUILayout.BeginHorizontal();  

        // 导出按钮 //  

        EditorGUILayout.EndHorizontal();  

    private void ExportAndSave(GameObject go)  

        //该方法将打开保存对话框,选择导出文件的保存位置。第二和第三个参数表示默认保存位置和默认文件名//  

        //Texture2D本身就是磁盘文件了,这里就没必要再转化了//  

工具界面应该是这样子:

[unity3d]从服务器端获取资源动态加载到场景

到这里我们通过这个小小的导出工具就可以制作出要需要的资源文件了,这些资源文件是存放在服务器上的,接下来我们一起看看关于这些资源文件的下载。

4、获取资源文件

这些文件是可以就从本地磁盘加载进游戏里使用的,但这里为了模拟从远程服务器下载这样一个模式,我还是将刚刚制作好的文件上传到远程主机来给大家展示一下这种的从远端获取的做法(其实从本地磁盘加载几乎是一样的)。

第一步:将文件上传到服务器。

我真的没有服务器,但是我感觉度娘很热情,估计能帮上我们什么忙。

(此处省略好几个字。。。。。。其实就是怎么将刚刚导出的文件上传到“百度云”)

上传不说了,这里看看怎么获取刚刚上传资源的完整地址。

用Google浏览器(码农用这个没有什么大问题吧?)登上“百度云”,找到刚刚上传的文件,点击下载,然后按Ctrl+J打开下载列表,右击正在下载的文件,选择“复制链接地址”就可以取到该文件的完整地址了。

[unity3d]从服务器端获取资源动态加载到场景

这个是我的:

<a target="_blank">http://bj.baidupcs.com/file/8752f8cf08e92dded7127aa4dc0489f7?xcode=28baeb9afc859429429dd4c38dda1979442b8d6833d75b4f&amp;fid=604160625-250528-2314552676&amp;time=1377828806&amp;sign=FDTAXER-DCb740ccc5511e5e8fedcff06b081203-cULgvhDQRRDEe32IavH35RKmn1Y%3D&amp;to=bb&amp;fm=N,B,U&amp;expires=8h&amp;rt=pr&amp;r=392549107&amp;logid=3993021536&amp;fn=dice.unity3d</a>

这里我们暂且先这样用着,在真正的项目开发中,资源的地址肯定会直接或间接的给出来的,这里不必纠结。

我们来具体看看下载,这里下载要使用到的类是WWW。在实例化WWW的时候,我们只需将资源的url地址给它,即可开始下载,实例化完WWW后我们只需判断这个实例是否下载完成,如果完成了,即可以取下载来的资源来用了。代码是这样的:(这个类不是UnityEditor里的类,新建一个C#类并继承自MonoBehaviour)

public class Downloader : MonoBehaviour   

{     

    private string url = " http://qd.baidupcs.com/file/5d06bd73f17afc5a5eb3e5497d0b6007?xcode=71a918e1fad4242470466f5cc4a869c8ec18245ec1f0d579&amp;fid=604160625-250528-2002528139&amp;time=1377831362&amp;sign=FDTAXER-DCb740ccc5511e5e8fedcff06b081203-FsMzIz4cENbozwsto38a47bDc64%3D&amp;to=qb&amp;fm=Q,B,U&amp;expires=8h&amp;rt=pr&amp;r=709102273&amp;logid=1896966947&amp;fn=dice.unity3d ";  

    private WWW www;  

    void Start ()   

        this.www = new WWW(this.url);  

    void Update ()   

        if(www == null) return;  

        if(www.isDone)  

            print ("Download completed");  

当启动了Unity之后,会发现很快就会在Console视图中打印出来了“Download completed”,而且还孜孜不倦的一直不肯停歇,这里我们下载完了,只要对下载完的资源处理一次就够了,没必要没完没了的处理,多浪费感情啊,所以我们该定义一个标志,来标记下载完成这么一个状态:

private bool isCompleted = false;  

void Update ()   

    if(www == null) return;  

    if(!isCompleted &amp;&amp; www.isDone)  

        print ("Download completed");  

        isCompleted = true;  

现在是不是只有这么一条打印信息了?

这段代码是非常简单的,这里也没有什么要多说的,就是提这么一点,这里我们是直接根据资源的URL去访问下载的该资源,但在实际项目中,我们经常要处理的是根据不同的条件访问同一地址而返回不同的数据来使用,这里要使用的是WWW的另一个构造方法,可以带除URL外的其它请求参数:

private void WWWWithParameter(string url, string parameter)  

    WWWForm form = new WWWForm();  

    form.AddField("Content", parameter);  

    WWW www = new WWW(url, form);  

可以看到,只需将参数封装在WWWForm中再去用WWW访问服务器就可以了。

(本例中我们没有采用带参数的访问是因为这样的话,我们还要加一个后台处理程序,要根据请求参数来返回数据,这样我们就必须要在本机上安装服务器,书写服务器代码等等等等,这样就得多做很多其它与我们这个话题相去深远的工作了。。。。。。。。(好吧,我承认我不会配置服务器))

到此本节的全部代码是这样子的:

    private bool isCompleted = false;  

        if(!isCompleted &amp;&amp; www.isDone)  

            isCompleted = true;  

    private void WWWWithParameter(string url, string parameter) 

    { 

        WWWForm form = new WWWForm(); 

        form.AddField("Content", parameter); 

        WWW www = new WWW(url, form); 

    } 

    */  

5、下载回来资源的使用

通过上面的操作,我们已经将资源下载到了本机,但是,大家也都看到了,我通过上面的方法的操作,说是下载完了资源,但我们场景中还是什么都没有啊,这个怎么解释?我用迅雷下完东东的时候,都在磁盘上有个文件的。

这里下载好的资源已经保存在内存中了,我们只要取出来使用就好了:

        GameObject.Instantiate(www.assetBundle.mainAsset) as GameObject;  

只需这样一句代码,你就在场景中可以看到这个令人兴奋的小东西了,哈哈,是不是很简单呢?

但是有没有发现什么问题呢?

我们当初导出的可不仅仅这点东西啊,我们回过头来看看:

[unity3d]从服务器端获取资源动态加载到场景

起码还有这些个贴图怎么不见了?当初导出时可是明明放到Object[]一起导出了的。莫着急,其实它们也都一起下载过来了,只是我们还没有取来用罢了。

        // 取回打包在资源内部的数据 由于我们当初放进去的全是Texture2D类型的,所以使用LoadAll的  

        // 带类型的重载方法,将Texture2D类型传入进去,表示我只取出Texture2D类型的数据  

        Object[] opticals = www.assetBundle.LoadAll(typeof(Texture2D));  

        foreach(Object o in opticals)  

            Texture2D tmp = o as Texture2D;  

            print("name : " + tmp.name);  

[unity3d]从服务器端获取资源动态加载到场景

这里打印除了 6 条记录我们当初打包到Object[]数组里的是 4 张贴图:black-dots、blue-dots、green-dots、yellow-dots。这里明显多出了red-dots和normal-dots,这不合适啊。细心的你也一定会发现,多出的那 2 张贴图,正是刚刚导出的模型上本身的一张漫反射贴图和一张法线贴图。原来,LoadAll()这个方法会将存在于下载过来的这个文件中符合类型的所有资源都取过来的,这也很简单处理,只要我们把不符合要求的剔除掉就好了。这里实现起来很简单,我就不说了,我这里想说的是另一种方法,这个是开发中比较常用的。

我们使用的时候,一般都是取确定的某个对象,可以通过Load(string name)方法来取得,这个方法返回的是一个AssetBundleRequest类型的值,我们可以通过它里面的asset属性取到需要的数据:

AssetBundleRequest abr = www.assetBundle.LoadAsync("black-dots", typeof(Texture2D));  

到现在,我们就下载过来了所有数据,并且可以取出需要的数据来使用了。接下来,我们完善一下这个小例子,把下载过来的资源充分的使用起来,就是给这个小东西换一个贴图。

这里完整的代码是这样子的:

    private string url = "http://qd.baidupcs.com/file/5d06bd73f17afc5a5eb3e5497d0b6007?xcode=71a918e1fad4242470466f5cc4a869c8ec18245ec1f0d579&amp;fid=604160625-250528-2002528139&amp;time=1377831362&amp;sign=FDTAXER-DCb740ccc5511e5e8fedcff06b081203-FsMzIz4cENbozwsto38a47bDc64%3D&amp;to=qb&amp;fm=Q,B,U&amp;expires=8h&amp;rt=pr&amp;r=709102273&amp;logid=1896966947&amp;fn=dice.unity3d";  

    private GameObject dice;  

    private Texture2D tex;  

            dice = GameObject.Instantiate(www.assetBundle.mainAsset) as GameObject;  

            /* 

            // 取回打包在资源内部的数据 由于我们当初放进去的全是Texture2D类型的,所以使用LoadAll的// 

            // 带类型的重载方法,将Texture2D类型传入进去,表示我只取出Texture2D类型的数据// 

            Object[] opticals = www.assetBundle.LoadAll(typeof(Texture2D)); 

            foreach(Object o in opticals) 

            { 

                Texture2D tmp = o as Texture2D; 

                print("name : " + tmp.name); 

            } 

            */  

            AssetBundleRequest abr = www.assetBundle.LoadAsync("black-dots", typeof(Texture2D));  

            tex = abr.asset as Texture2D;             

        if(GUI.Button(new Rect(20, 20, 100, 40), "CHANGE"))  

            // 如果还没下载完,这时候是不能执行替换功能的 //  

            if(dice == null || tex == null) return;  

            dice.renderer.material.mainTexture = tex;  

}  

运行结果:

开始未替换之前:

[unity3d]从服务器端获取资源动态加载到场景

替换之后:

[unity3d]从服务器端获取资源动态加载到场景

到现在我们这个的流程以及要求就基本实现了,这里别忘了最后一步,清理使用完的无用资源,释放内存。

下载完的数据都保存在内存中,这时候它们都是一个AssetBundle的内存镜像,我们在使用数据时,只是从内存镜像里取出数据,通过Instance方法新实例化出来的一个对象,当我们有了这样一个对象,以后的操作都是针对这样的一个,而内存中保存的那块镜像已经没有用处了,我们可以释放掉:

AssetBundle.Unload(flase) : 是释放AssetBundle文件的内存镜像,不包含Load创建的Asset内存对象。

AssetBundle.Unload(true) : 是释放那个AssetBundle文件内存镜像和并销毁所有用Load创建的Asset内存对象。

这里我们使用

www.assetBundle.Unload(false);  

之所以不使用

www.assetBundle.Unload(true);  

是因为我们不能销毁掉实例化出来的Asset对象,我们还要继续操作它(下面的换贴图等)。否则,该对象会在场景里消失,彻底销毁掉了。

所有代码再给大家列一遍:

导出工具的代码:

下载实现代码:

自己测试的代码:从服务器上获取模型资源动态加载

[unity3d]从服务器端获取资源动态加载到场景

本文转蓬莱仙羽51CTO博客,原文链接:http://blog.51cto.com/dingxiaowei/1366372,如需转载请自行联系原作者

下一篇: 也谈IO模型