2019-11-20 17:32:53 qq_37310110 阅读数 84
  • unity3D游戏/AR/VR在线就业班

    本套课程是一套完整的 Unity3D-游戏/AR/VR 学习课程,具体的课程内容包括《C#语言》、《Unity引擎》、《编程思想》,《商业级项目实践》等开发课程,引导您一步步循序渐进、由易到难,终获得Unity 3D/游戏/AR/VR工程师的岗位技能。

    12825 人正在学习 去看看 宋晓波

欢迎加入Unity业内qq交流群:956187480

qq扫描二维码加群,行业纵横颇多,每个人精通领域各异,旨在交流,


最近项目优化的太差了,里面内容比较多,经常启动后内存溢出导致闪退

一:理论理解

常用资源加载的方法有三种:场景内预制资源,Resources内动态加载资源,AssetBundle外部资源

资源释放的方式 有二种:用过不再使用后立刻释放;在某一阶段统一释放不用的资源

1.场景内提前预制

静态就是资源直接放场景,静态资源无法立刻释放,但场景关闭由引擎统一释放。但静态过于死板,除了整个场景生命周期中必须使用的资源外,不建议静态加载,为什么,消耗时间,占用内存;比如说,游戏场景持续使用的背景音乐,那么静态的确是最佳选择;但如果场景的开场要播放一个5M的音乐后面不再使用,这个资源用静态就是白白增加了5M音乐的加载时间和浪费了5M内存,得不偿失。静态资源的加载策略是: 只加载必要固定的资源,或者极其微小损耗不大的资源。释放策略是:由引擎负责

2.Resources动态加载资源

Resources的资源一般和游戏一起打包,一般是游戏内使用的关键资源。Resources主要加载一些动态的或者需要灵活处理的资源。加载的办法是先Reources.Load,然后GameObject.Instantiate,立刻释放的办法是Reources.UnloadAsset,统一释放的办法是Resources.UnloadUnusedAssets。在用UnloadAsset()去释放GameObject的时候会报错:UnloadAsset may only be used on individual assets and can not be used on GameObject's/Components or AssetBundles。也就是说不能释放除individual assets类型以外的资源类型,只能释放(mesh / texture / material / shader )。如果想要释放不用的gameObject就用UnloadUnusedAssets(),在调用之前需要把对象设置为null。另外还有一点需要注意这个UnloadAsset释放的对象是通过Resources加载出来的资源。

Resources的主要优点是使用灵活,缺点是开发复杂;加载的时候注意一个资源只需要Reources.Load一次,管理好Load出来资源的引用,尽量不要用静态引用,给释放制造困难。UnloadUnusedAssets前必须注意释放所有引用,所有Instantiate出来的对象必须销毁。

Resources的加载释放策略是: 只加载必须使用的资源,加载以后如果无需重复使用,或者使用频率非常的低,应该在明确使用完毕以后(如图片显示结束,音乐播放完毕)立刻用Reources.UnloadAsset释放。对于后续仍然会反复采用的资源,建议在内存中缓存引用,以防止后续加载不再反复调用Load函数,场景关闭前释放缓存,销毁所有实例化对象,等待Resources.UnloadUnusedAssets统一释放。UnloadUnusedAssets释放建议在当前场景完全销毁以后,再后续场景中执行最为稳妥。

3.AssetBundle外部资源

 AssetBundle资源的机制基本和Resources类似,但可以独立打包,支持网络下载和文件读取,一般是游戏中使用的辅助资源。

 AssetBundle机制要比Resources多一步,首先要通过AssetBundle.LoadFromXXX把AssetBundle加载到内存,然后使用AssetBundle.LoadAsset加载资源,最后GameObject.Instantiate为实例化对象。所以一个完整实例化的 AssetBundle存在三处内存引用,在释放策略上相对复杂。

 AssetBundle的释放功能主要通过AssetBundle.Unload,而且也可以使用Reources.UnloadAsset和Resources.UnloadUnusedAssets进行释放。

 AssetBundle的加载释放策略分两种情况:

a.加载完毕以后不再需要使用的资源包,可以再加载完毕以后立刻调用AssetBundle.Unload(false)释放资源包内存,而在此基础上,对于AssetBundle.LoadAsset以后并不再使用的资源,可以通过Reources.UnloadAsset释放,如需持续使用,也可能等待Resources.UnloadUnusedAssets后续统一释放。

b.加载完毕以后仍需持续使用的资源包,则必须保留AssetBundle引用,这种方式下,比较建议的方式是在场景完全销毁以后,一次性调用AssetBundle.Unload(true)完全释放改资源包和其所有加载资源。

二:实例演示

1.场景内提前预制

运行后可以看到镜像文件里没有,但是在Assets里面存在一个Cube,Scene里存在一个Cube

运行状态下分别执行 Destory()和Resources.UnloadAsset(),执行Desotry确实是可以把场景内资源释放掉。

2.Resources动态加载资源

中间可能会有报错:Instantiating a non-readable 'Girl' texture is not allowed! Please mark the texture readable in the inspector or don't instantiate it.

public Object o;
    public GameObject go;

    private void Start()
    {
        o = Resources.Load("Girl") ;
        go = Instantiate(o) as GameObject;
    }
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.U))
        {
            Resources.UnloadAsset(o);
            Debug.Log("释放成功");
        }
    }

3.AssetBundle外部资源

 private void LoadBundle()
    {
        string assetBundlePath = Application.streamingAssetsPath + "/cube.assetbundle";
        byte[] s = File.ReadAllBytes(assetBundlePath);
        var a = AssetBundle.LoadFromMemory(s);//a加载到内存
        var b = a.LoadAsset<GameObject>("Cube");//b加载内存的资源
        c = Instantiate(b);//c实例化对象
        Debug.LogError("LoadAssetBundle over!");
    }

 

释放:加载完直接释放的资源

 var a = AssetBundle.LoadFromMemory(s);//a加载到内存
 var b = a.LoadAsset<GameObject>("Cube");//b加载内存的资源
 c = Instantiate(b);//c实例化对象
 a.Unload(false);//加载完毕以后不再需要使用的资源包直接释放

 

---------------------------------------------------------------

UnloadUnusedAssets释放后assets内资源释放,destory()场景实例

    

 


欢迎加入Unity业内qq交流群:956187480

qq扫描二维码加群,行业纵横颇多,每个人精通领域各异,旨在交流,

 

2014-07-23 16:14:54 hackdjh 阅读数 3346
  • unity3D游戏/AR/VR在线就业班

    本套课程是一套完整的 Unity3D-游戏/AR/VR 学习课程,具体的课程内容包括《C#语言》、《Unity引擎》、《编程思想》,《商业级项目实践》等开发课程,引导您一步步循序渐进、由易到难,终获得Unity 3D/游戏/AR/VR工程师的岗位技能。

    12825 人正在学习 去看看 宋晓波

我们在移动端的开发中,异步网络图片加载用的非常的多,在unity当中虽然有AssetBundle的存在,一般是先加载好游戏资源然后再进入场景,但是还有不少地方能够用到异步网络图片的加载以及其缓存机制。

我之前也写过两个版本的ios中的异步网络图片加载helper类,所以今天按照同样的思路,也想做一个好用的helper类给大家使用以及简单的说下实现原理。

首先我们加载一张网络图片,要做的事情分步来讲为:

0.开始之前设置一张固定的图片作为占位图(placeholder),表示我们的图片还没加载好,来填充当前图片控件区域,让用户知道

1.这个图片有个url地址,我们的程序第一次加载这个url地址的图片时

a.异步开始下载这张图片

b.保存到某个指定的目录

c.将图片控件上占位图替换为下载好的图片,可以适当的添加图片切换动画(动画本文略)

2.我们的程序已经加载过这个url地址的图片

a.从上次存进的目录中读取文件,转换为图片

b.将图片控件上占位图替换为下载好的图片



总的来说我们的程序应该是在第一次加载一批网络图片的时候,是需要哪张下哪张,然后显示出来,而后,是可以直接从文件系统中去读取的(第三种情况,从内存中直接读取,取决于设备配置,这里就不做了)


开始做这个工具类之前,先要知道一些unity的特殊限制:

1.MonoBehaviour基类的成员方法 StartCoroutine开启异步任务是不支持静态方法中调用的

2.MonoBehaviour基类的子类不能直接使用关键字new 来创建,相应的,需要创建一个空的GameObject,然后调用这个object的AddComponent方法来实例化,也就是说unity中的脚本对象都依附于游戏对象才能被执行

3.由于unity的跨平台特性,每个不同的平台的文件目录结构有区别


基于上述一些特殊性,我打算把这个工具类作成一个MonoBehaviour的子类,并以单例的形式让其他脚本调用,其中的异步网络请求就可以使用StartCoroutine函数了

首先是这个单例的实现方法:


  1. using UnityEngine;  
  2. using System.Collections;  
  3. using System.IO;  
  4.   
  5. public class AsyncImageDownload :MonoBehaviour {  
  6.   
  7.     public  Texture placeholder;  
  8.     public static AsyncImageDownload  Instance=null;  
  9.   
  10.     private   string path=Application.persistentDataPath+"/ImageCache/" ;  
  11.   
  12.     //构建单例  
  13.     public static AsyncImageDownload CreateSingleton()  
  14.     {  
  15.         if (!Directory.Exists(Application.persistentDataPath+"/ImageCache/")) {  
  16.             Directory.CreateDirectory(Application.persistentDataPath+"/ImageCache/");  
  17.         }  
  18.   
  19.         GameObject obj = new GameObject ();  
  20.         obj.AddComponent<AsyncImageDownload> ();  
  21.   
  22.         AsyncImageDownload loader= obj.GetComponent<AsyncImageDownload>();  
  23.         Instance=loader;  
  24.         loader.placeholder=Resources.Load("placeholder"as Texture;  
  25.         return loader;  
  26.   
  27.     }  


这里没有使用构造函数来创建单例,原因是MonoBehaviour的子类压根不支持new关键字,所以怎么去写构造函数呢

然后这个构建单例的方法是跟构造方法差不多的,只是不是去new出来,而是类名.方法名()来创建这个单例,在创建单例的时候需要创建一个空白游戏体来依附我们的脚本组件,并且把静态成员Instance指向创建出来的这个单例,多次使用这个单例的时候只需要使用 类名.Instance()来获取已经存在的这个脚本组件单例。

这里的Resources.Load(“placeholder”)需要注意,用代码读取项目目录中的图片资源并转换成Texutre对象,需要再Project窗口下的Assets中创建Resources文件夹,然后导入任何一张图片进去,代码中获取他不需要加后缀名。


这个脚本写好以后,我们的程序初始化的时候应该执行以下代码来创建这个单例:

  1. AsyncImageDownload.CreateSingleton()  

而后,给某个控件加载网络图片,我们应该给这个单例加一个方法public  void SetAsyncImage(string url,UITexture texture),那么简化的写法就可以写为:

  1. AsyncImageDownload.Instance.SetAsyncImage ("http://www.cfanz.cn/uploads/jpg/2013/07/13/0/XEPLd7d2C5.jpg", page.GetComponentInChildren<UITexture> ());  



public  void SetAsyncImage(string url,UITexture texture)

按照前文分析的图片加载步骤来


  1. public  void SetAsyncImage(string url,UITexture texture){  
  2.   
  3.   
  4.   
  5.         //开始下载图片前,将UITexture的主图片设置为占位图  
  6.         texture.mainTexture = placeholder;  
  7.   
  8.         //判断是否是第一次加载这张图片  
  9.   
  10.         if (!File.Exists (path + url.GetHashCode())) {  
  11.                         //如果之前不存在缓存文件  
  12.   
  13.                   
  14.                     StartCoroutine (DownloadImage (url, texture));  
  15.                           
  16.   
  17.                 }  
  18.         else {  
  19.   
  20.                     StartCoroutine(LoadLocalImage(url,texture));  
  21.   
  22.                 }  
  23.   
  24.     }  

这里判断缓存文件是否存在使用的是url.GetHashCode()方法,因为我们的图片文件名采用的是原URL的哈希码直接作为文件名来保存,重名概率可以忽略不计,也缩短了文件名的长度提高效率,这个做法借鉴于 iOS开源框架EGOImageView。

如果是第一次加载图片,这个URL对应的文件不存在,那么我们就去原URL下载图片然后赋值给控件

如果缓存文件夹中已有该文件,直接读取加载

由于前文的铺垫,我们的工具类已经是MonoBehaviour的单例子类,所以可以使用unity的异步函数StartCorutine()


接下来完成方法DownloadImage(string url,UITexture texture)

  1. IEnumerator  DownloadImage(string url,UITexture texture){  
  2.         Debug.Log("downloading new image:"+path+url.GetHashCode());    
  3.         WWW www = new WWW (url);  
  4.         yield return www;  
  5.   
  6.         Texture2D image = www.texture;  
  7.         //将图片保存至缓存路径  
  8.         byte[] pngData = image.EncodeToPNG();    
  9.         File.WriteAllBytes(path+url.GetHashCode(), pngData);    
  10.   
  11.         texture.mainTexture = image;  
  12.   
  13.     }  


这个方法很简单,然后是从缓存文件夹读取已经存在的图片方法LoadLocalImage(string url,UITexture texture)  

注意这里不能使用Resources.Load()方法,因为我们的图片并没有存放在工程目录中,我仔细查阅了相关资料发现比较合适的方法应该还是使用unity的WWW类去加载文件url,即在文件路径的前方加上file:///使之成为一个文件url,然后使用www类读取,但是这个过程是本地的还是比较快


  1. IEnumerator  LoadLocalImage(string url,UITexture texture){  
  2.         string filePath = "file:///" + path + url.GetHashCode ();  
  3.   
  4.         Debug.Log("getting local image:"+filePath);  
  5.         WWW www = new WWW (filePath);  
  6.         yield return www;  
  7.           
  8.   
  9.         //直接贴图  
  10.         texture.mainTexture = www.texture;  
  11.           
  12.     }  


我们的工具类写好了

随便找个Panel添加一个带UITexture组件的节点,然后调用我们的工具方法,测试下运行结果:

第一次运行:



打开这个文件所在的文件夹(我这里使用的是windows系统,不同系统路径不一样)




这个图片已经加载在我们的图片控件上并且已经保存至了本地路径,也就是说我们再次运行之后,不会再进入if的第一种情况了,我们关掉程序再次运行:



正如我们所想,实际上这次是没有产生网络请求的,说明我的缓存已经有用,而且图片一下就出来了,不像上一次要等一会



接下来我们删掉缓存文件再次执行,又会调用第一个方法~

这个工具类先做到这里了,接下来图片切换效果已经加载过程的等待HUD等后面再研究。完整代码:

  1. using UnityEngine;  
  2. using System.Collections;  
  3. using System.IO;  
  4.   
  5. public class AsyncImageDownload :MonoBehaviour {  
  6.   
  7.     public  Texture placeholder;  
  8.     public static AsyncImageDownload  Instance=null;  
  9.   
  10.     private   string path=Application.persistentDataPath+"/ImageCache/" ;  
  11.   
  12.     //构建单例  
  13.     public static AsyncImageDownload CreateSingleton()  
  14.     {  
  15.         if (!Directory.Exists(Application.persistentDataPath+"/ImageCache/")) {  
  16.             Directory.CreateDirectory(Application.persistentDataPath+"/ImageCache/");  
  17.         }  
  18.   
  19.         GameObject obj = new GameObject ();  
  20.         obj.AddComponent<AsyncImageDownload> ();  
  21.   
  22.         AsyncImageDownload loader= obj.GetComponent<AsyncImageDownload>();  
  23.         Instance=loader;  
  24.         loader.placeholder=Resources.Load("placeholder"as Texture;  
  25.         return loader;  
  26.   
  27.     }  
  28.   
  29.   
  30.     public  void SetAsyncImage(string url,UITexture texture){  
  31.   
  32.   
  33.   
  34.         //开始下载图片前,将UITexture的主图片设置为占位图  
  35.         texture.mainTexture = placeholder;  
  36.   
  37.         //判断是否是第一次加载这张图片  
  38.   
  39.         if (!File.Exists (path + url.GetHashCode())) {  
  40.                         //如果之前不存在缓存文件  
  41.   
  42.                   
  43.                     StartCoroutine (DownloadImage (url, texture));  
  44.                           
  45.   
  46.                 }  
  47.         else {  
  48.   
  49.                     StartCoroutine(LoadLocalImage(url,texture));  
  50.   
  51.                 }  
  52.   
  53.     }  
  54.   
  55.     IEnumerator  DownloadImage(string url,UITexture texture){  
  56.         Debug.Log("downloading new image:"+path+url.GetHashCode());  
  57.         WWW www = new WWW (url);  
  58.         yield return www;  
  59.   
  60.         Texture2D image = www.texture;  
  61.         //将图片保存至缓存路径  
  62.         byte[] pngData = image.EncodeToPNG();    
  63.         File.WriteAllBytes(path+url.GetHashCode(), pngData);    
  64.   
  65.         texture.mainTexture = image;  
  66.   
  67.     }  
  68.   
  69.     IEnumerator  LoadLocalImage(string url,UITexture texture){  
  70.         string filePath = "file:///" + path + url.GetHashCode ();  
  71.   
  72.         Debug.Log("getting local image:"+filePath);  
  73.         WWW www = new WWW (filePath);  
  74.         yield return www;  
  75.           
  76.   
  77.         //直接贴图  
  78.         texture.mainTexture = www.texture;  
  79.           
  80.     }  
  81. }  


2017-03-14 20:34:37 u012632851 阅读数 2861
  • unity3D游戏/AR/VR在线就业班

    本套课程是一套完整的 Unity3D-游戏/AR/VR 学习课程,具体的课程内容包括《C#语言》、《Unity引擎》、《编程思想》,《商业级项目实践》等开发课程,引导您一步步循序渐进、由易到难,终获得Unity 3D/游戏/AR/VR工程师的岗位技能。

    12825 人正在学习 去看看 宋晓波

版本:unity 5.4.1  语言:C#

 

海水先在这里坑一会,看了里面的算法深深感到自己数学能力的不足,经过同学的推荐,我准备先看会《数值分析》闭关修炼一下。

 

至于我在看的实战核心技术的第十章MVC设计框架,提供了一堆无用的代码,然后让你去看他的课程,我就自己研究一下如何从Asset中加载到场景中,又如何在场景中释放资源。

 

总结一下网上一共有三种方法(这次最终解决了AssetBundle的加载问题,AssetBundle原来是要打包后才能加载……):静态加载、Resources.Load、AssetBundle.LoadAsset。

 

各有不同的用处吧,本地的话前两个更加容易管理使用,AssetBundle可以用在网络环境下。

 

接下来看代码:

(参考:http://www.taidous.com/portal.php?mod=view&aid=445&page=1)

public class ResManager : MonoBehaviour {

    public GameObject tps;

	void Start ()
    {
        //StartCoroutine(LoadResByResources("TPS"));
        //StartCoroutine(LoadResByStatic());
        StartCoroutine(LoadResByBundle("file://" + Application.dataPath + @"/Resources/rs.assetbundle", "assets/standard assets/characters/thirdpersoncharacter/prefabs/thirdpersoncontroller.prefab"));
    }

    // Resources的方法加载资源,只能在Resources文件夹下读取,不想创建Resources文件夹的可以考虑静态加载
    IEnumerator LoadResByResources(string path)
    {
        Resources.UnloadUnusedAssets();

        // 读取,此时仅读取了资源的少量信息,并不占多少内存
        GameObject go = Resources.Load<GameObject>(path);

        yield return new WaitForSeconds(5f);

        // 创建,如果Instantiate过多会产生卡顿,所以在加载大量资源的时候,考虑对象池
        // 先在读取进度的时候把对象创建出来,并SetActive(false),使用的时候SetActive(true)
        GameObject goInit = Instantiate(go);
        goInit.transform.parent = transform;
        goInit.transform.localPosition = Vector3.zero;
        goInit.transform.localRotation = Quaternion.identity;

        yield return new WaitForSeconds(5f);

        // 销毁对象,但各种资源还占据内存
        Destroy(goInit);

        yield return new WaitForSeconds(5f);

        // 卸载所有没有引用的资源,相当于清空了内存
        // 但最好不要主动使用,转换场景的时候会清空资源
        Resources.UnloadUnusedAssets();
    }

    // 静态的方法加载资源,效果与Resources加载类似
    IEnumerator LoadResByStatic()
    {
        Resources.UnloadUnusedAssets();

        yield return new WaitForSeconds(5f);

        GameObject goInit = Instantiate(tps);
        goInit.transform.parent = transform;
        goInit.transform.localPosition = Vector3.zero;
        goInit.transform.localRotation = Quaternion.identity;

        yield return new WaitForSeconds(5f);
        Destroy(goInit);

        yield return new WaitForSeconds(5f);
        Resources.UnloadUnusedAssets();
    }

    // 使用AssetBundle加载资源,在LoadAsset的时候就会加载大量的资源
    // 所以实例化时加载的资源量反而小,可以考虑利用这种方法做加载而不卡顿
    IEnumerator LoadResByBundle(string path, string name)
    {
        Resources.UnloadUnusedAssets();

        // 建立WWW读取,这边是本地
        WWW bundle = new WWW(path);
        yield return bundle;

        yield return new WaitForSeconds(5f);

        // 从读取到的资源中获取对应prefab
        //Object obj = bundle.assetBundle.Load(name);   //方法已经被废弃
        Object obj = bundle.assetBundle.LoadAsset(name);

        yield return new WaitForSeconds(5f);

        // 实例化
        GameObject goInit = Instantiate(obj) as GameObject;
        goInit.transform.parent = transform;
        goInit.transform.localPosition = Vector3.zero;
        goInit.transform.localRotation = Quaternion.identity;

        yield return new WaitForSeconds(5f);

        // 删除
        Destroy(goInit);

        yield return new WaitForSeconds(5f);

        // Unload释放内存镜像,参数是是否强制删除内存资源
        // true强制删除,即使内存有引用也删除
        //bundle.assetBundle.Unload(true);

        // 删除没有引用的资源,比如说这边,如果调用的是这个方法的的话
        // 虽然资源已经被删除了,但内存中还保留的该prefab,直到调用Resources.UnloadUnusedAssets()
        bundle.assetBundle.Unload(false);

        Resources.UnloadUnusedAssets();
    }

    // 打包方法是jiange啊啊啊撰写的(http://blog.csdn.net/janeky/article/details/17652021)
    // 这个方法是打包当前项目的资源,从而形成assetbundle文件,供LoadResByBundle加载
    [MenuItem("Assets/Build AssetBundle From Selection")]
    static void ExportResourceRGB2()
    {
        // 打开保存面板,获得用户选择的路径  
        string path = EditorUtility.SaveFilePanel("Save Resource", "", "New Resource", "assetbundle");

        if (path.Length != 0)
        {
            // 选择的要保存的对象
            Object[] selection = Selection.GetFiltered(typeof(Object), SelectionMode.DeepAssets);
            //打包  
            BuildPipeline.BuildAssetBundle(Selection.activeObject, selection, path, BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets, BuildTarget.StandaloneWindows);
        }
    }
}

静态读取和Resources.Load最好先把要用到的实例出来,嗯,在进度读取的时候做,然后SetActive(false)将其隐藏,这样卡顿现象应该不会出现吧。

 

好了,这本书我看了,再写个热更新吧,其他章节也没什么好看的了。


2014-08-08 11:24:05 s10141303 阅读数 4295
  • unity3D游戏/AR/VR在线就业班

    本套课程是一套完整的 Unity3D-游戏/AR/VR 学习课程,具体的课程内容包括《C#语言》、《Unity引擎》、《编程思想》,《商业级项目实践》等开发课程,引导您一步步循序渐进、由易到难,终获得Unity 3D/游戏/AR/VR工程师的岗位技能。

    12825 人正在学习 去看看 宋晓波

用Unity3D制作基于web的网络游戏,不可避免的会用到一个技术-资源动态加载。比如想加载一个大场景的资源,不应该在游戏的开始让用户长时间等待全部资源的加载完毕。应该优先加载用户附近的场景资源,在游戏的过程中,不影响操作的情况下,后台加载剩余的资源,直到所有加载完毕。

本文包含一些代码片段讲述实现这个技术的一种方法。本方法不一定是最好的,希望能抛砖引玉。代码是C#写的,用到了Json,还有C#的事件机制。

在讲述代码之前,先想象这样一个网络游戏的开发流程。首先美工制作场景资源的3D建模,游戏设计人员把3D建模导进Unity3D,托托拽拽编辑场景,完成后把每个gameobject导出成XXX.unity3d格式的资源文件(参看BuildPipeline),并且把整个场景的信息生成一个配置文件,xml或者Json格式(本文使用Json)。最后还要把资源文件和场景配置文件上传到服务器,最好使用CMS管理。客户端运行游戏时,先读取服务器的场景配置文件,再根据玩家的位置从服务器下载相应的资源文件并加载,然后开始游戏,注意这里并不是下载所有的场景资源。在游戏的过程中,后台继续加载资源直到所有加载完毕。

一个简单的场景配置文件的例子:
MyDemoSence.txt

复制代码

  1. {
  2.     "AssetList" : [{
  3.         "Name" : "Chair 1",
  4.         "Source" : "Prefabs/Chair001.unity3d",
  5.         "Position" : [2,0,-5],
  6.         "Rotation" : [0.0,60.0,0.0]
  7.     },
  8.     {
  9.         "Name" : "Chair 2",
  10.         "Source" : "Prefabs/Chair001.unity3d",
  11.         "Position" : [1,0,-5],
  12.         "Rotation" : [0.0,0.0,0.0]
  13.     },
  14.     {
  15.         "Name" : "Vanity",
  16.         "Source" : "Prefabs/vanity001.unity3d",
  17.         "Position" : [0,0,-4],
  18.         "Rotation" : [0.0,0.0,0.0]
  19.     },
  20.     {
  21.         "Name" : "Writing Table",
  22.         "Source" : "Prefabs/writingTable001.unity3d",
  23.         "Position" : [0,0,-7],
  24.         "Rotation" : [0.0,0.0,0.0],
  25.         "AssetList" : [{
  26.             "Name" : "Lamp",
  27.             "Source" : "Prefabs/lamp001.unity3d",
  28.             "Position" : [-0.5,0.7,-7],
  29.             "Rotation" : [0.0,0.0,0.0]
  30.         }]
  31.     }]
  32. }


AssetList:场景中资源的列表,每一个资源都对应一个unity3D的gameobject
Name:gameobject的名字,一个场景中不应该重名
Source:资源的物理路径及文件名
Position:gameobject的坐标
Rotation:gameobject的旋转角度
你会注意到Writing Table里面包含了Lamp,这两个对象是父子的关系。配置文件应该是由程序生成的,手工也可以修改。另外在游戏上线后,客户端接收到的配置文件应该是加密并压缩过的。

主程序:
复制代码

  1. 。。。
  2. public class MainMonoBehavior : MonoBehaviour {

  3.     public delegate void MainEventHandler(GameObject dispatcher);

  4.     public event MainEventHandler StartEvent;
  5.     public event MainEventHandler UpdateEvent;

  6.     public void Start() {
  7.         ResourceManager.getInstance().LoadSence("Scenes/MyDemoSence.txt");

  8.         if(StartEvent != null){
  9.             StartEvent(this.gameObject);
  10.         }
  11.     }

  12.     public void Update() {
  13.         if (UpdateEvent != null) {
  14.             UpdateEvent(this.gameObject);
  15.         }
  16.     }
  17. }
  18. 。。。
  19. }


这里面用到了C#的事件机制,大家可以看看我以前翻译过的国外一个牛人的文章。C# 事件和Unity3D
在start方法里调用ResourceManager,先加载配置文件。每一次调用update方法,MainMonoBehavior会把update事件分发给ResourceManager,因为ResourceManager注册了MainMonoBehavior的update事件。

ResourceManager.cs
复制代码

  1. 。。。
  2. private MainMonoBehavior mainMonoBehavior;
  3. private string mResourcePath;
  4. private Scene mScene;
  5. private Asset mSceneAsset;

  6. private ResourceManager() {
  7.     mainMonoBehavior = GameObject.Find("Main Camera").GetComponent<MainMonoBehavior>();
  8.     mResourcePath = PathUtil.getResourcePath();
  9. }

  10. public void LoadSence(string fileName) {
  11.     mSceneAsset = new Asset();
  12.     mSceneAsset.Type = Asset.TYPE_JSON;
  13.     mSceneAsset.Source = fileName;

  14.     mainMonoBehavior.UpdateEvent += OnUpdate;
  15. }
  16. 。。。


在LoadSence方法里先创建一个Asset的对象,这个对象是对应于配置文件的,设置type是Json,source是传进来的“Scenes/MyDemoSence.txt”。然后注册MainMonoBehavior的update事件。
复制代码

  1. public void OnUpdate(GameObject dispatcher) {
  2.     if (mSceneAsset != null) {
  3.         LoadAsset(mSceneAsset);
  4.         if (!mSceneAsset.isLoadFinished) {
  5.             return;
  6.         }

  7.         //clear mScene and mSceneAsset for next LoadSence call
  8.         mScene = null;
  9.         mSceneAsset = null;
  10.     }

  11.     mainMonoBehavior.UpdateEvent -= OnUpdate;
  12. }


OnUpdate方法里调用LoadAsset加载配置文件对象及所有资源对象。每一帧都要判断是否加载结束,如果结束清空mScene和mSceneAsset对象为下一次加载做准备,并且取消update事件的注册。

最核心的LoadAsset方法:
复制代码

  1. private Asset LoadAsset(Asset asset) {

  2.     string fullFileName = mResourcePath + "/" + asset.Source;
  3.     
  4.     //if www resource is new, set into www cache
  5.     if (!wwwCacheMap.ContainsKey(fullFileName)) {
  6.         if (asset.www == null) {
  7.             asset.www = new WWW(fullFileName);
  8.             return null;
  9.         }

  10.         if (!asset.www.isDone) {
  11.             return null;
  12.         }
  13.         wwwCacheMap.Add(fullFileName, asset.www);
  14.     }
  15. 。。。


传进来的是要加载的资源对象,先得到它的物理地址,mResourcePath是个全局变量保存资源服务器的网址,得到fullFileName类似http://www.mydemogame.com/asset/Prefabs/xxx.unity3d。然后通过wwwCacheMap判断资源是否已经加载完毕,如果加载完毕把加载好的www对象放到Map里缓存起来。看看前面Json配置文件,Chair 1和Chair 2用到了同一个资源Chair001.unity3d,加载Chair 2的时候就不需要下载了。如果当前帧没有加载完毕,返回null等到下一帧再做判断。这就是WWW类的特点,刚开始用WWW下载资源的时候是不能马上使用的,要等待诺干帧下载完成以后才可以使用。可以用yield返回www,这样代码简单,但是C#要求调用yield的方法返回IEnumerator类型,这样限制太多不灵活。

继续LoadAsset方法:
复制代码

  1. 。。。
  2.     if (asset.Type == Asset.TYPE_JSON) { //Json
  3.         if (mScene == null) {
  4.             string jsonTxt = mSceneAsset.www.text;
  5.             mScene = JsonMapper.ToObject<Scene>(jsonTxt);
  6.         }
  7.         
  8.         //load scene
  9.         foreach (Asset sceneAsset in mScene.AssetList) {
  10.             if (sceneAsset.isLoadFinished) {
  11.                 continue;
  12.             } else {
  13.                 LoadAsset(sceneAsset);
  14.                 if (!sceneAsset.isLoadFinished) {
  15.                     return null;
  16.                 }
  17.             }
  18.         }
  19.     }
  20. 。。。


代码能够运行到这里,说明资源都已经下载完毕了。现在开始加载处理资源了。第一次肯定是先加载配置文件,因为是Json格式,用JsonMapper类把它转换成C#对象,我用的是LitJson开源类库。然后循环递归处理场景中的每一个资源。如果没有完成,返回null,等待下一帧处理。

继续LoadAsset方法:
复制代码

  1. 。。。
  2.     else if (asset.Type == Asset.TYPE_GAMEOBJECT) { //Gameobject
  3.         if (asset.gameObject == null) {
  4.             wwwCacheMap[fullFileName].assetBundle.LoadAll();
  5.             GameObject go = (GameObject)GameObject.Instantiate(wwwCacheMap[fullFileName].assetBundle.mainAsset);
  6.             UpdateGameObject(go, asset);
  7.             asset.gameObject = go;
  8.         }

  9.         if (asset.AssetList != null) {
  10.             foreach (Asset assetChild in asset.AssetList) {
  11.                 if (assetChild.isLoadFinished) {
  12.                     continue;
  13.                 } else {
  14.                     Asset assetRet = LoadAsset(assetChild);
  15.                     if (assetRet != null) {
  16.                         assetRet.gameObject.transform.parent = asset.gameObject.transform;
  17.                     } else {
  18.                         return null;
  19.                     }
  20.                 }
  21.             }
  22.         }
  23.     }

  24.     asset.isLoadFinished = true;
  25.     return asset;
  26. }


终于开始处理真正的资源了,从缓存中找到www对象,调用Instantiate方法实例化成Unity3D的gameobject。UpdateGameObject方法设置gameobject各个属性,如位置和旋转角度。然后又是一个循环递归为了加载子对象,处理gameobject的父子关系。注意如果LoadAsset返回null,说明www没有下载完毕,等到下一帧处理。最后设置加载完成标志返回asset对象。

UpdateGameObject方法:
复制代码

  1. private void UpdateGameObject(GameObject go, Asset asset) {
  2.     //name
  3.     go.name = asset.Name;

  4.     //position
  5.     Vector3 vector3 = new Vector3((float)asset.Position[0], (float)asset.Position[1], (float)asset.Position[2]);
  6.     go.transform.position = vector3;

  7.     //rotation
  8.     vector3 = new Vector3((float)asset.Rotation[0], (float)asset.Rotation[1], (float)asset.Rotation[2]);
  9.     go.transform.eulerAngles = vector3;
  10. }


这里只设置了gameobject的3个属性,眼力好的同学一定会发现这些对象都是“死的”,因为少了脚本属性,它们不会和玩家交互。设置脚本属性要复杂的多,编译好的脚本随着主程序下载到本地,它们也应该通过配置文件加载,再通过C#的反射创建脚本对象,赋给相应的gameobject。

最后是Scene和asset代码:
复制代码

  1. public class Scene {
  2.     public List<Asset> AssetList {
  3.         get;
  4.         set;
  5.     }
  6. }

  7. public class Asset {

  8.     public const byte TYPE_JSON = 1;
  9.     public const byte TYPE_GAMEOBJECT = 2;

  10.     public Asset() {
  11.         //default type is gameobject for json load
  12.         Type = TYPE_GAMEOBJECT;
  13.     }

  14.     public byte Type {
  15.         get;
  16.         set;
  17.     }

  18.     public string Name {
  19.         get;
  20.         set;
  21.     }

  22.     public string Source {
  23.         get;
  24.         set;
  25.     }

  26.     public double[] Bounds {
  27.         get;
  28.         set;
  29.     }
  30.     
  31.     public double[] Position {
  32.         get;
  33.         set;
  34.     }

  35.     public double[] Rotation {
  36.         get;
  37.         set;
  38.     }

  39.     public List<Asset> AssetList {
  40.         get;
  41.         set;
  42.     }

  43.     public bool isLoadFinished {
  44.         get;
  45.         set;
  46.     }

  47.     public WWW www {
  48.         get;
  49.         set;
  50.     }

  51.     public GameObject gameObject {
  52.         get;
  53.         set;
  54.     }
  55. }



代码就讲完了,在我实际测试中,会看到gameobject一个个加载并显示在屏幕中,并不会影响到游戏操作。代码还需要进一步完善适合更多的资源类型,如动画资源,文本,字体,图片和声音资源

动态加载资源除了网络游戏必需,对于大公司的游戏开发也是必须的。它可以让游戏策划(负责场景设计),美工和程序3个角色独立出来,极大提高开发效率。试想如果策划改变了什么NPC的位置,美工改变了某个动画,或者改变了某个程序,大家都要重新倒入一遍资源是多么低效和麻烦的一件事。

 

2017-05-12 11:06:27 qq_29639547 阅读数 162
  • unity3D游戏/AR/VR在线就业班

    本套课程是一套完整的 Unity3D-游戏/AR/VR 学习课程,具体的课程内容包括《C#语言》、《Unity引擎》、《编程思想》,《商业级项目实践》等开发课程,引导您一步步循序渐进、由易到难,终获得Unity 3D/游戏/AR/VR工程师的岗位技能。

    12825 人正在学习 去看看 宋晓波

Unity3D 里有两种动态加载机制:一个是Resources.Load,另外一个通过AssetBundle,其实两者区别不大。 Resources.Load就是从一个缺省打进程序包里的AssetBundle里加载资源,而一般AssetBundle文件需要你自己创建,运行时 动态加载,可以指定路径和来源的。

其实场景里所有静态的对象也有这么一个加载过程,只是Unity3D后台替你自动完成了。

详细说一下细节概念:
AssetBundle运行时加载:
来自文件就用CreateFromFile(注意这种方法只能用于standalone程序)这是最快的加载方法
也可以来自Memory,用CreateFromMemory(byte[]),这个byte[]可以来自文件读取的缓冲,www的下载或者其他可能的方式。
其实WWW的assetBundle就是内部数据读取完后自动创建了一个assetBundle而已
Create完以后,等于把硬盘或者网络的一个文件读到内存一个区域,这时候只是个AssetBundle内存镜像数据块,还没有Assets的概念。
Assets加载:
用AssetBundle.Load(同Resources.Load) 这才会从AssetBundle的内存镜像里读取并创建一个Asset对象,创建Asset对象同时也会分配相应内存用于存放(反序列化)
异步读取用AssetBundle.LoadAsync
也可以一次读取多个用AssetBundle.LoadAll
AssetBundle的释放:
AssetBundle.Unload(flase)是释放AssetBundle文件的内存镜像,不包含Load创建的Asset内存对象。
AssetBundle.Unload(true)是释放那个AssetBundle文件内存镜像和并销毁所有用Load创建的Asset内存对象。

一个Prefab从assetBundle里Load出来 里面可能包括:Gameobject transform mesh texture material shader script和各种其他Assets。
你 Instaniate一个Prefab,是一个对Assets进行Clone(复制)+引用结合的过程,GameObject transform 是Clone是新生成的。其他mesh / texture / material / shader 等,这其中些是纯引用的关系的,包括:Texture和TerrainData,还有引用和复制同时存在的,包括:Mesh/material /PhysicMaterial。引用的Asset对象不会被复制,只是一个简单的指针指向已经Load的Asset对象。这种含糊的引用加克隆的混合, 大概是搞糊涂大多数人的主要原因。
专门要提一下的是一个特殊的东西:Script Asset,看起来很奇怪,Unity里每个Script都是一个封闭的Class定义而已,并没有写调用代码,光Class的定义脚本是不会工作的。其 实Unity引擎就是那个调用代码,Clone一个script asset等于new一个class实例,实例才会完成工作。把他挂到Unity主线程的调用链里去,Class实例里的OnUpdate OnStart等才会被执行。多个物体挂同一个脚本,其实就是在多个物体上挂了那个脚本类的多个实例而已,这样就好理解了。在new class这个过程中,数据区是复制的,代码区是共享的,算是一种特殊的复制+引用关系。
你可以再Instaniate一个同样的Prefab,还是这套mesh/texture/material/shader...,这时候会有新的GameObject等,但是不会创建新的引用对象比如Texture.
所以你Load出来的Assets其实就是个数据源,用于生成新对象或者被引用,生成的过程可能是复制(clone)也可能是引用(指针)
当你Destroy一个实例时,只是释放那些Clone对象,并不会释放引用对象和Clone的数据源对象,Destroy并不知道是否还有别的object在引用那些对象。
等到没有任何 游戏场景物体在用这些Assets以后,这些assets就成了没有引用的游离数据块了,是UnusedAssets了,这时候就可以通过 Resources.UnloadUnusedAssets来释放,Destroy不能完成这个任 务,AssetBundle.Unload(false)也不行,AssetBundle.Unload(true)可以但不安全,除非你很清楚没有任何 对象在用这些Assets了。
配个图加深理解:

Unity3D占用内存太大怎么解决呢?

虽然都叫Asset,但复制的和引用的是不一样的,这点被Unity的暗黑技术细节掩盖了,需要自己去理解。

关于内存管理
按照传统的编程思维,最好的方法是:自己维护所有对象,用一个Queue来保存所有object,不用时该Destory的,该Unload的自己处理。
但这样在C# .net框架底下有点没必要,而且很麻烦。
稳妥起见你可以这样管理

创建时:
先建立一个AssetBundle,无论是从www还是文件还是memory
用AssetBundle.load加载需要的asset
加载完后立即AssetBundle.Unload(false),释放AssetBundle文件本身的内存镜像,但不销毁加载的Asset对象。(这样你不用保存AssetBundle的引用并且可以立即释放一部分内存)
释放时:
如果有Instantiate的对象,用Destroy进行销毁
在合适的地方调用Resources.UnloadUnusedAssets,释放已经没有引用的Asset.
如果需要立即释放内存加上GC.Collect(),否则内存未必会立即被释放,有时候可能导致内存占用过多而引发异常。
这样可以保证内存始终被及时释放,占用量最少。也不需要对每个加载的对象进行引用。

当然这并不是唯一的方法,只要遵循加载和释放的原理,任何做法都是可以的。

系统在加载新场景时,所有的内存对象都会被自动销毁,包括你用AssetBundle.Load加载的对象和Instaniate克隆的。但是不包括AssetBundle文件自身的内存镜像,那个必须要用Unload来释放,用.net的术语,这种数据缓存是非托管的。

总结一下各种加载和初始化的用法:
AssetBundle.CreateFrom.....:创建一个AssetBundle内存镜像,注意同一个assetBundle文件在没有Unload之前不能再次被使用
WWW.AssetBundle:同上,当然要先new一个再 yield return 然后才能使用
AssetBundle.Load(name): 从AssetBundle读取一个指定名称的Asset并生成Asset内存对象,如果多次Load同名对象,除第一次外都只会返回已经生成的Asset 对象,也就是说多次Load一个Asset并不会生成多个副本(singleton)。
Resources.Load(path&name):同上,只是从默认的位置加载。
Instantiate(object):Clone 一个object的完整结构,包括其所有Component和子物体(详见官方文档),浅Copy,并不复制所有引用类型。有个特别用法,虽然很少这样 用,其实可以用Instantiate来完整的拷贝一个引用类型的Asset,比如Texture等,要拷贝的Texture必须类型设置为 Read/Write able。

总结一下各种释放
Destroy: 主要用于销毁克隆对象,也可以用于场景内的静态物体,不会自动释放该对象的所有引用。虽然也可以用于Asset,但是概念不一样要小心,如果用于销毁从文 件加载的Asset对象会销毁相应的资源文件!但是如果销毁的Asset是Copy的或者用脚本动态生成的,只会销毁内存对象。
AssetBundle.Unload(false):释放AssetBundle文件内存镜像
AssetBundle.Unload(true):释放AssetBundle文件内存镜像同时销毁所有已经Load的Assets内存对象
Reources.UnloadAsset(Object):显式的释放已加载的Asset对象,只能卸载磁盘文件加载的Asset对象
Resources.UnloadUnusedAssets:用于释放所有没有引用的Asset对象
GC.Collect()强制垃圾收集器立即释放内存 Unity的GC功能不算好,没把握的时候就强制调用一下

在3.5.2之前好像Unity不能显式的释放Asset

举两个例子帮助理解
例子1:
一个常见的错误:你从某个AssetBundle里Load了一个prefab并克隆之:obj = Instaniate(AssetBundle1.Load('MyPrefab”);
这个prefab比如是个npc
然后你不需要他的时候你用了:Destroy(obj);你以为就释放干净了
其实这时候只是释放了Clone对象,通过Load加载的所有引用、非引用Assets对象全都静静静的躺在内存里。
这种情况应该在Destroy以后用:AssetBundle1.Unload(true),彻底释放干净。
如果这个AssetBundle1是要反复读取的 不方便Unload,那可以在Destroy以后用:Resources.UnloadUnusedAssets()把所有和这个npc有关的Asset都销毁。
当然如果这个NPC也是要频繁创建 销毁的 那就应该让那些Assets呆在内存里以加速游戏体验。
由此可以解释另一个之前有人提过的话题:为什么第一次Instaniate 一个Prefab的时候都会卡一下,因为在你第一次Instaniate之前,相应的Asset对象还没有被创建,要加载系统内置的 AssetBundle并创建Assets,第一次以后你虽然Destroy了,但Prefab的Assets对象都还在内存里,所以就很快了。

顺便提一下几种加载方式的区别:
其实存在3种加载方式:
一是静态引用,建一个public的变量,在Inspector里把prefab拉上去,用的时候instantiate
二是Resource.Load,Load以后instantiate
三是AssetBundle.Load,Load以后instantiate
三种方式有细 节差异,前两种方式,引用对象texture是在instantiate时加载,而assetBundle.Load会把perfab的全部assets 都加载,instantiate时只是生成Clone。所以前两种方式,除非你提前加载相关引用对象,否则第一次instantiate时会包含加载引用 assets的操作,导致第一次加载的lag。

例子2:
从磁盘读取一个1.unity3d文件到内存并建立一个AssetBundle1对象
AssetBundle AssetBundle1 = AssetBundle.CreateFromFile("1.unity3d");
从AssetBundle1里读取并创建一个Texture Asset,把obj1的主贴图指向它
obj1.renderer.material.mainTexture = AssetBundle1.Load("wall") as Texture;
把obj2的主贴图也指向同一个Texture Asset
obj2.renderer.material.mainTexture =obj1.renderer.material.mainTexture;
Texture是引用对象,永远不会有自动复制的情况出现(除非你真需要,用代码自己实现copy),只会是创建和添加引用
如果继续:
AssetBundle1.Unload(true) 那obj1和obj2都变成黑的了,因为指向的Texture Asset没了
如果:
AssetBundle1.Unload(false) 那obj1和obj2不变,只是AssetBundle1的内存镜像释放了
继续:
Destroy(obj1),//obj1被释放,但并不会释放刚才Load的Texture
如果这时候:
Resources.UnloadUnusedAssets();
不会有任何内存释放 因为Texture asset还被obj2用着
如果
Destroy(obj2)
obj2被释放,但也不会释放刚才Load的Texture
继续
Resources.UnloadUnusedAssets();
这时候刚才load的Texture Asset释放了,因为没有任何引用了
最后CG.Collect();
强制立即释放内存
由此可以引申出论坛里另一个被提了几次的问题,如何加载一堆大图片轮流显示又不爆掉
不考虑AssetBundle,直接用www读图片文件的话等于是直接创建了一个Texture Asset
假设文件保存在一个List里
TLlist<string> fileList;
int n=0;
IEnumerator OnClick()
{
WWW image = new www(fileList[n++]);
yield return image;
obj.mainTexture = image.texture;

n = (n>=fileList.Length-1)?0:n;
Resources.UnloadUnusedAssets();
}
这样可以保证内存里始终只有一个巨型Texture Asset资源,也不用代码追踪上一个加载的Texture Asset,但是速度比较慢
或者:
IEnumerator OnClick()
{
WWW image = new www(fileList[n++]);
yield return image;
Texture tex = obj.mainTexture;
obj.mainTexture = image.texture;

n = (n>=fileList.Length-1)?0:n;
Resources.UnloadAsset(tex);
}
这样卸载比较快

 

 

Hog的评论引用:

感觉这是Unity内存管理暗黑和混乱的地方,特别是牵扯到Texture
我最近也一直在测试这些用AssetBundle加载的asset一样可以用Resources.UnloadUnusedAssets卸载,但必须先AssetBundle.Unload,才会被识别为无用的asset。比较保险的做法是
创建时:
先建立一个AssetBundle,无论是从www还是文件还是memory
用AssetBundle.load加载需要的asset
用完后立即AssetBundle.Unload(false),关闭AssetBundle但不摧毁创建的对象和引用
销毁时:
对Instantiate的对象进行Destroy
在合适的地方调用Resources.UnloadUnusedAssets,释放已经没有引用的Asset.
如果需要立即释放加上GC.Collect()
这样可以保证内存始终被及时释放
只要你Unload过的AssetBundle,那些创建的对象和引用都会在LoadLevel时被自动释放。

 

全面理解Unity加载和内存管理机制之二:进一步深入和细节
Unity几种动态加载Prefab方式的差异:

其实存在3种加载prefab的方式:
一是静态引用,建一个public的变量,在Inspector里把prefab拉上去,用的时候instantiate
二是Resource.Load,Load以后instantiate
三是AssetBundle.Load,Load以后instantiate
三种方式有细节差异,前两种方式,引用对象texture是在instantiate时加载,而assetBundle.Load会把perfab的全部 assets都加载,instantiate时只是生成Clone。所以前两种方式,除非你提前加载相关引用对象,否则第一次instantiate时会 包含加载引用类assets的操作,导致第一次加载的lag。官方论坛有人说Resources.Load和静态引用是会把所有资源都预先加载的,反复测试的结果,静态引用和Resources.Load也是OnDemand的,用到时才会加载。

几种AssetBundle创建方式的差异:
CreateFromFile:这种方式不会把整个硬盘AssetBundle文件都加载到 内存来,而是类似建立一个文件操作句柄和缓冲区,需要时才实时Load,所以这种加载方式是最节省资源的,基本上AssetBundle本身不占什么内 存,只需要Asset对象的内存。可惜只能在PC/Mac Standalone程序中使用。
CreateFromMemory和www.assetBundle:这两种方式AssetBundle文件会整个镜像于内存中,理论上文件多大就需要多大的内存,之后Load时还要占用额外内存去生成Asset对象。

什么时候才是UnusedAssets?
看一个例子:
Object obj = Resources.Load("MyPrefab");
GameObject instance = Instantiate(obj) as GameObject;
.........
Destroy(instance);
创建随后销毁了一个Prefab实例,这时候 MyPrefab已经没有被实际的物体引用了,但如果这时:
Resources.UnloadUnusedAssets();
内存并没有被释放,原因:MyPrefab还被这个变量obj所引用
这时候:
obj  = null;
Resources.UnloadUnusedAssets();
这样才能真正释放Assets对象
所以:UnusedAssets不但要没有被实际物体引用,也要没有被生命周期内的变量所引用,才可以理解为 Unused(引用计数为0)
所以所以:如果你用个全局变量保存你Load的Assets,又没有显式的设为null,那 在这个变量失效前你无论如何UnloadUnusedAssets也释放不了那些Assets的。如果你这些Assets又不是从磁盘加载的,那除了 UnloadUnusedAssets或者加载新场景以外没有其他方式可以卸载之。

一个复杂的例子,代码很丑陋实际也不可能这样做,只是为了加深理解

IEnumerator OnClick()

{

Resources.UnloadUnusedAssets();//清干净以免影响测试效果

yield return new WaitForSeconds(3);

float wait = 0.5f;

//用www读取一个assetBundle,里面是一个Unity基本球体和带一张大贴图的材质,是一个Prefab

WWW aa = new WWW(@"file://SpherePrefab.unity3d");

yield return aa;

AssetBundle asset = aa.assetBundle;

yield return new WaitForSeconds(wait);//每步都等待0.5s以便于分析结果

Texture tt = asset.Load("BallTexture") as  Texture;//加载贴图

yield return new WaitForSeconds(wait);

GameObject ba = asset.Load("SpherePrefab") as  GameObject;//加载Prefab

yield return new WaitForSeconds(wait);

GameObject obj1 = Instantiate(ba) as GameObject;//生成实例

yield return new WaitForSeconds(wait);

Destroy(obj1);//销毁实例

yield return new WaitForSeconds(wait);

asset.Unload(false);//卸载Assetbundle

yield return new WaitForSeconds(wait);

Resources.UnloadUnusedAssets();//卸载无用资源

yield return new WaitForSeconds(wait);

ba = null;//将prefab引用置为空以后卸无用载资源

Resources.UnloadUnusedAssets();

yield return new WaitForSeconds(wait);

tt = null;//将texture引用置为空以后卸载无用资源

Resources.UnloadUnusedAssets();

}

这是测试结果的内存Profile曲线图

Unity3D占用内存太大怎么解决呢?

图片:p12.jpg

很经典的对称造型,用多少释放多少。

这是各阶段的内存和其他数据变化

说明:
1        初始状态
2        载入AssetBundle文件后,内存多了文件镜像,用量上升,Total Object和Assets增加1(AssetBundle也是object)
3        载入Texture后,内存继续上升,因为多了Texture Asset,Total Objects和Assets增加1
4        载入Prefab后,内存无明显变化,因为最占内存的Texture已经加载,Materials上升是因为多了Prefab的材质,Total Objects和Assets增加6,因为 Perfab 包含很多 Components
5        实例化Prefab以后,显存的Texture Memory、GameObjectTotal、Objects in Scene上升,都是因为实例化了一个可视的对象
6        销毁实例后,上一步的变化还原,很好理解
7        卸载AssetBundle文件后,AssetBundle文件镜像占用的内存被释放,相应的Assets和Total Objects Count也减1
8        直接Resources.UnloadUnusedAssets,没有任何变化,因为所有Assets引用并没有清空
9        把Prefab引用变量设为null以后,整个Prefab除了Texture外都没有任何引用了,所以被UnloadUnusedAssets销毁,Assets和Total Objects Count减6
10        再把Texture的引用变量设为null,之后也被UnloadUnusedAssets销毁,内存被释放,assets和Total Objects Count减1,基本还原到初始状态

从中也可以看出:
Texture加载以后是到内存,显示的时候才进入显存的Texture Memory。
所有的东西基础都是Object
Load的是Asset,Instantiate的是GameObject和Object in Scene
Load的Asset要Unload,new的或者Instantiate的object可以Destroy

 

Unity 3D中的内存管理

Unity3D在内存占用上一直被人诟病,特别是对于面向移动设备的游戏开发,动辄内存占用飙上一两百兆,导致内存资源耗尽,从而被系统强退造成极 差的体验。类似这种情况并不少见,但是绝大部分都是可以避免的。虽然理论上Unity的内存管理系统应当为开发者分忧解难,让大家投身到更有意义的事情中 去,但是对于Unity对内存的管理方式,官方文档中并没有太多的说明,基本需要依靠自己摸索。最近在接手的项目中存在严重的内存问题,在参照文档和 Unity Answer众多猜测和证实之后,稍微总结了下Unity中的内存的分配和管理的基本方式,在此共享。

虽然Unity标榜自己的内存使用全都是“Managed Memory”,但是事实上你必须正确地使用内存,以保证回收机制正确运行。如果没有做应当做的事情,那么场景和代码很有可能造成很多非必要内存的占用, 这也是很多Unity开发者抱怨内存占用太大的原因。接下来我会介绍Unity使用内存的种类,以及相应每个种类的优化和使用的技巧。遵循使用原则,可以 让非必要资源尽快得到释放,从而降低内存占用。

 

Unity中的内存种类

实际上Unity游戏使用的内存一共有三种:程序代码、托管堆(Managed Heap)以及本机堆(Native Heap)。

程序代码包括了所有的Unity引擎,使用的库,以及你所写的所有的游戏代码。在编译后,得到的运行文件将会被加载到设备中执行,并占用一定内存。

这部分内存实际上是没有办法去“管理”的,它们将在内存中从一开始到最后一直存在。一个空的Unity默认场景,什么代码都不放,在iOS设备上占 用内存应该在17MB左右,而加上一些自己的代码很容易就飙到20MB左右。想要减少这部分内存的使用,能做的就是减少使用的库,稍后再说。

托管堆是被Mono使用的一部分内存。Mono项目一个开源的.net框架的一种实现,对于Unity开发,其实充当了基本类库的角色。

托管堆用来存放类的实例(比如用new生成的列表,实例中的各种声明的变量等)。“托管”的意思是Mono“应该”自动地改变堆的大小来适应你所需要的内存,

并且定时地使用垃圾回收(Garbage Collect)来释放已经不需要的内存。关键在于,有时候你会忘记清除对已经不需要再使用的内存的引用,

从而导致Mono认为这块内存一直有用,而无法回收。

最后,本机堆是Unity引擎进行申请和操作的地方,比如贴图,音效,关卡数据等。Unity使用了自己的一套内存管理机制来使这块内存具有和托管堆类似的功能。

基本理念是,如果在这个关卡里需要某个资源,那么在需要时就加载,之后在没有任何引用时进行卸载。听起来很美好也和托管堆一样,

但是由于Unity有一套自动加载和卸载资源的机制,让两者变得差别很大。自动加载资源可以为开发者省不少事儿,

但是同时也意味着开发者失去了手动管理所有加载资源的权力,这非常容易导致大量的内存占用(贴图什么的你懂的),

也是Unity给人留下“吃内存”印象的罪魁祸首。


 

优化程序代码的内存占用

这部分的优化相对简单,因为能做的事情并不多:主要就是减少打包时的引用库,改一改build设置即可。

对于一个新项目来说不会有太大问题,但是如果是已经存在的项目,可能改变会导致原来所需要的库的缺失(虽说一般来说这种可能性不大),

因此有可能无法做到最优。

 

当使用Unity开发时,默认的Mono包含库可以说大部分用不上,在Player Setting(Edit->Project Setting->Player或者Shift+Ctrl(Command)+B里的Player Setting按钮)

面板里,将最下方的Optimization栏目中“Api Compatibility Level”选为.NET 2.0 Subset,表示你只会使用到部分的.NET 2.0 Subset,不需要Unity将全部.NET的Api包含进去。接下来的“Stripping Level”表示从build的库中剥离的力度,每一个剥离选项都将从打包好的库中去掉一部分内容。你需要保证你的代码没有用到这部分被剥离的功能,

选为“Use micro mscorlib”的话将使用最小的库(一般来说也没啥问题,不行的话可以试试之前的两个)。库剥离可以极大地降低打包后的程序的尺寸以及程序代码的内存占用,唯一的缺点是这个功能只支持Pro版的Unity。

这部分优化的力度需要根据代码所用到的.NET的功能来进行调整,有可能不能使用Subset或者最大的剥离力度。

如果超出了限度,很可能会在需要该功能时因为找不到相应的库而crash掉(iOS的话很可能在Xcode编译时就报错了)。

比较好地解决方案是仍然用最强的剥离,并辅以较小的第三方的类库来完成所需功能。

一个最常见问题是最大剥离时Sysytem.Xml是不被Subset和micro支持的,如果只是为了xml,完全可以导入一个轻量级的xml库来解决依赖(Unity官方推荐这个)。

关于每个设定对应支持的库的详细列表,可以在这里找到。关于每个剥离级别到底做了什么,Unity的文档也有说明。

实际上,在游戏开发中绝大多数被剥离的功能使用不上的,因此不管如何,库剥离的优化方法都值得一试。


 

托管堆优化

Unity有一篇不错的关于托管堆代码如何写比较好的说明,在此基础上我个人有一些补充。

首先需要明确,托管堆中存储的是你在你的代码中申请的内存(不论是用js,C#还是Boo写的)。

一般来说,无非是new或者Instantiate两种生成object的方法(事实上Instantiate中也是调用了new)。

在接收到alloc请求后,托管堆在其上为要新生成的对象实例以及其实例变量分配内存,如果可用空间不足,则向系统申请更多空间。

当你使用完一个实例对象之后,通常来说在脚本中就不会再有对该对象的引用了(这包括将变量设置为null或其他引用,超出了变量的作用域,

或者对Unity对象发送Destory())。在每隔一段时间,Mono的垃圾回收机制将检测内存,将没有再被引用的内存释放回收。总的来说,

你要做的就是在尽可能早的时间将不需要的引用去除掉,这样回收机制才能正确地把不需要的内存清理出来。但是需要注意在内存清理时有可能造成游戏的短时间卡顿,

这将会很影响游戏体验,因此如果有大量的内存回收工作要进行的话,需要尽量选择合适的时间。

如果在你的游戏里,有特别多的类似实例,并需要对它们经常发送Destroy()的话,游戏性能上会相当难看。比如小熊推金币中的金币实例,按理说每枚金币落下台子后

都需要对其Destory(),然后新的金币进入台子时又需要Instantiate,这对性能是极大的浪费。一种通常的做法是在不需要时,不摧毁这个GameObject,而只是隐藏它,

并将其放入一个重用数组中。之后需要时,再从重用数组中找到可用的实例并显示。这将极大地改善游戏的性能,相应的代价是消耗部分内存,一般来说这是可以接受的。

关于对象重用,可以参考Unity关于内存方面的文档中Reusable Object Pools部分,或者Prime31有一个是用Linq来建立重用池的视频教程(Youtube,需要翻墙,上,下)。

如果不是必要,应该在游戏进行的过程中尽量减少对GameObject的Instantiate()和Destroy()调用,因为对计算资源会有很大消耗。在便携设备上短时间大量生成和摧毁物体的

话,很容易造成瞬时卡顿。如果内存没有问题的话,尽量选择先将他们收集起来,然后在合适的时候(比如按暂停键或者是关卡切换),将它们批量地销毁并 且回收内存。Mono的内存回收会在后台自动进行,系统会选择合适的时间进行垃圾回收。在合适的时候,也可以手动地调用 System.GC.Collect()来建议系统进行一次垃圾回收。

要注意的是这里的调用真的仅仅只是建议,可能系统会在一段时间后在进行回收,也可能完全不理会这条请求,不过在大部分时间里,这个调用还是靠谱的。


 

本机堆的优化

当你加载完成一个Unity的scene的时候,scene中的所有用到的asset(包括Hierarchy中所有GameObject上以及脚本中赋值了的的材质,贴图,动画,声音等素材),

都会被自动加载(这正是Unity的智能之处)。也就是说,当关卡呈现在用户面前的时候,所有Unity编辑器能认识的本关卡的资源都已经被预先加 入内存了,这样在本关卡中,用户将有良好的体验,不论是更换贴图,声音,还是播放动画时,都不会有额外的加载,这样的代价是内存占用将变多。Unity最 初的设计目的还是面向台式机,

几乎无限的内存和虚拟内存使得这样的占用似乎不是问题,但是这样的内存策略在之后移动平台的兴起和大量移动设备游戏的制作中出现了弊端,因为移动设 备能使用的资源始终非常有限。因此在面向移动设备游戏的制作时,尽量减少在Hierarchy对资源的直接引用,而是使用Resource.Load的方 法,在需要的时候从硬盘中读取资源,

在使用后用Resource.UnloadAsset()和Resources.UnloadUnusedAssets()尽快将其卸载掉。总之,这里是一个处理时间和占用内存空间的trade off,

如何达到最好的效果没有标准答案,需要自己权衡。

在关卡结束的时候,这个关卡中所使用的所有资源将会被卸载掉(除非被标记了DontDestroyOnLoad)的资源。注意不仅是DontDestroyOnLoad的资源本身,

其相关的所有资源在关卡切换时都不会被卸载。DontDestroyOnLoad一般被用来在关卡之间保存一些玩家的状态,比如分数,级别等偏向文 本的信息。如果DontDestroyOnLoad了一个包含很多资源(比如大量贴图或者声音等大内存占用的东西)的话,这部分资源在场景切换时无法卸 载,将一直占用内存,

这种情况应该尽量避免。

另外一种需要注意的情况是脚本中对资源的引用。大部分脚本将在场景转换时随之失效并被回收,但是,在场景之间被保持的脚本不在此列(通常情况是被附 着在DontDestroyOnLoad的GameObject上了)。而这些脚本很可能含有对其他物体的Component或者资源的引用,这样相关的 资源就都得不到释放,

这绝对是不想要的情况。另外,static的单例(singleton)在场景切换时也不会被摧毁,同样地,如果这种单例含有大量的对资源的引用,也会成为大问题。

因此,尽量减少代码的耦合和对其他脚本的依赖是十分有必要的。如果确实无法避免这种情况,那应当手动地对这些不再使用的引用对象调用Destroy()

或者将其设置为null。这样在垃圾回收的时候,这些内存将被认为已经无用而被回收。

需要注意的是,Unity在一个场景开始时,根据场景构成和引用关系所自动读取的资源,只有在读取一个新的场景或者reset当前场景时,才会得到清理。

因此这部分内存占用是不可避免的。在小内存环境中,这部分初始内存的占用十分重要,因为它决定了你的关卡是否能够被正常加载。因此在计算资源充足

或是关卡开始之后还有机会进行加载时,尽量减少Hierarchy中的引用,变为手动用Resource.Load,将大大减少内存占用。在 Resource.UnloadAsset()和Resources.UnloadUnusedAssets()时,只有那些真正没有任何引用指向的资源 会被回收,因此请确保在资源不再使用时,将所有对该资源的引用设置为null或者Destroy。

同样需要注意,这两个Unload方法仅仅对Resource.Load拿到的资源有效,而不能回收任何场景开始时自动加载的资源。与此类似的还有 AssetBundle的Load和Unload方法,灵活使用这些手动自愿加载和卸载的方法,是优化Unity内存占用的不二法则。

总之这些就是关于Unity3d优化细节,具体还是查看Unity3D的技术手册,以便实现最大的优化。

没有更多推荐了,返回首页