Unity笔记(八)——资源动态加载、场景异步加载
写在前面:
写本系列(自用)的目的是回顾已经学过的知识、记录新学习的知识或是记录心得理解,方便自己以后快速复习,减少遗忘。
九、资源动态加载
1、特殊文件夹
(1)工程路径获取
该方式下获取到的路径一般只在编辑模式下使用,游戏发布后,该方法就不存在了。
void Start()
{print(Application.dataPath);
}
(2)Resources资源文件夹
一般不获取它的路径,直接使用Resources相关API进行加载。如果一定要获取,只能工程路径拼接。
void Start()
{
//最好不用print(Application.dataPath + "/Resources");
}
资源文件夹需要手动创建,名字不能出错。并且有以下几点需要注意:
1、需要通过Resources相关API动态加载的资源需要放在其中。
2、该文件夹下所有文件都会被打包出去,打包时Unity会对其压缩加密
3、该文件夹打包后只读,只能通过Resources相关API加载
(3)流动资源文件夹
流动资源文件夹是streamingAssets,也需要我们自己创建,获取路径的方法是:
void Start()
{print(Application.streamingAssetsPath);
}
流动资源文件夹的作用是:打包出去不会被压缩加密、移动平台只读,PC平台可读可写、可以放入一些需要自定义动态加载的初始资源。
(4)持久数据文件夹
持久数据文件夹persistentDataPath,不需要我们自己创建。
void Start()
{print(Application.persistentDataPath);
}
作用是:固定的数据文件夹。所有平台都可读可写。一般用于放置动态下载或者动态创建的文件夹。游戏中创建或者获取的文件都放在其中。
(5)Plugins插件文件夹
一般不获取,需要我们手动创建。不同平台(如第三平台的开发工具)的插件相关的文件放在其中。
(6)Editor编辑器文件夹
一般不获取,需要我们手动创建。作用是开发Unity编辑器时,编辑器相关脚本放在该文件夹中,该文件夹的内容不会被打包出去。
(7)Standard Assets默认资源文件夹
一般不获取,需要我们自己创建,一般Unity自带资源都放在这个文件夹下,代码和资源优先被编译。
2、Resources同步加载
(1)作用
通过代码动态加载Resources文件夹下指定路径资源,避免繁琐的拖拽操作。
(2)常用资源类型
1、预设体对象:GameObject
2、音效文件:AudioClip
3、文本文件:TextAsset
4、图片文件:Texture
5、其他类型
其中,预设体加载需要实例化,其他资源加载一般直接用。
(3)资源同步加载
在一个工程当中,Resources可以有多个,通过API加载时,它会自己去这些同名文件夹中找资源。打包时,Resources文件夹里的内容都会打包在一起。
资源加载使用的API都是Resources.Load(),括号内传入需要加载的预设体/音效/文本相对路径
1、预设体对象加载
这里预设体位于Resources文件中,名字叫Cube
void Start()
{//第一步,要去加载预设体的资源文件(本质上 就是加载配置数据在内存中)Object obj = Resources.Load("Cube");//第二步,实例化Instantiate(obj);
}
2、音效加载
这里音频文件位于Resources/Music文件中,名字叫BKMusic
public AudioSource audioS;
void Start()
{//第一步加载数据Object obj1 = Resources.Load("Music/BKMusic");//第二步赋值脚本audioS.clip = obj1 as AudioClip;
}
3、文本加载
文本资源支持的格式:.txt .xml .bytes .json .html .csv
void Start()
{TextAsset ta = Resources.Load("Txt/Test") as TextAsset;//文本内容print(ta.text);//字节数据组//print(ta.bytes);
}
4、图片
private Texture tex;void Start()
{tex = Resources.Load("Tex/TestJPG") as Texture;
}private void OnGUI()
{GUI.DrawTexture(new Rect(0, 0, 100, 100), tex);
}
5、资源同名时
加载同名资源时,无法准确加载出你想要的内容。可以指定类型:
private Texture tex;void Start()
{tex = Resources.Load("Tex/TestJPG", typeof(Texture)) as Texture;
}
加载所有类型的指定资源
void Start()
{Object[] objs = Resources.LoadAll("Tex/TestJPG");
}
(4)资源同步加载泛型方法
void Start()
{Texture tex2 = Resources.Load<Texture>("Tex/TestJPG");
}
3、Resources异步加载
使用同步加载时,如果加载过大的资源可能造成程序卡顿。越大的资源耗时越长,就会造成掉帧卡顿。Resources异步加载就是内部新开一个线程进行资源加载,不会造成主线程卡顿。在完成加载过后,将加载的内容放入公共区域,主线程去公共区域中取数据。
需要注意的是,异步加载不能马上得到加载的资源,至少要等一帧。
(1)完成时间监听异步加载
异步加载使用的API是:Resources.LoadAsync<>()(为了方便这里只介绍泛型),<>中传入需要加载的类型,括号内传入的是加载数据在Resources文件夹中的路径,和同步加载一样。该API的返回值是ResourceRequest类型的变量,可以定义一个变量接收,例如:
ResourceRequest rq = Resources.LoadAsync<Texture>("Tex/TestJPG");
这句代码的意思就是:在Unity内部新开一个线程进行资源下载。
在ResourceRequest类型中,提供了completed事件。也就是当你调用Resources.LoadAsync时,会马上进行资源下载结束的事件函数监听,当它加载完毕时,内部会自动调用这个事件函数。
此时,我们使用rq.completed += LoadOver;可以自行为这个事件添加函数LoadOver,在LoadOver中实现资源赋值、提醒加载完成等一系列操作。此外,按照规定,函数LoadOver必须传入一个AsyncOperation类型的参数,AsyncOperation是ResourceRequest的父类。传入的这个参数可以理解为,当加载完毕后,Unity会自动将你加载的数据转化为AsyncOperation类型,并且在你为completed添加函数时传入作为参数,因此必须传入一个AsyncOperation类型的参数。
为了得到加载的资源,ResourceRequest类提供了.asset方法,可以通过该方法获得加载的资源。只需要将传入的AsyncOperation类型的变量转为ResourceRequest类型的变量,即可使用。如下:
private Texture tex;void Start()
{ResourceRequest rq = Resources.LoadAsync<Texture>("Tex/TestJPG");rq.completed += LoadOver;
}private void LoadOver(AsyncOperation rq)
{print("加载结束");tex = (rq as ResourceRequest).asset as Texture;
}private void OnGUI()
{if(tex != null){GUI.DrawTexture(new Rect(0, 0, 100, 100), tex);}
}
(2)通过协程
还可以使用协程来完成资源异步加载。首先需要创建一个协程函数。
在协程函数中使用:ResourceRequest rq = Resources.LoadAsync<Texture>("Tex/TestJPG");来开启异步资源加载。直接使用yield return rq;Unity知道该返回值意味着你在异步加载资源,Unity会自己判断该资源是否加载完毕了,加载完毕后,才会继续执行后面的代码。在该代码后直接使用 tex = rq.asset as Texture; 即可获得加载的资源。
private Texture tex;void Start()
{StartCoroutine(Load());
}IEnumerator Load()
{ResourceRequest rq = Resources.LoadAsync<Texture>("Tex/TestJPG");yield return rq;tex = rq.asset as Texture;
}private void OnGUI()
{if(tex != null){GUI.DrawTexture(new Rect(0, 0, 100, 100), tex);}
}
此外,还有另外一种方式,可以直接使用rq.isDone判断数据加载是否完成,使用print(rq.priority)打印当前进度。
private Texture tex;void Start()
{StartCoroutine(Load());
}IEnumerator Load()
{ResourceRequest rq = Resources.LoadAsync<Texture>("Tex/TestJPG");while(!rq.isDone){//打印当前的加载进度,如果加载的内容少,该进度不会特别准确,过度也不是特别明显print(rq.priority);yield return null;}
}private void OnGUI()
{if(tex != null){GUI.DrawTexture(new Rect(0, 0, 100, 100), tex);}
}
(3)区别
第一种方式的写法简单,但是只能在资源加载结束后进行处理
第二种方式写法稍麻烦,但是可以在协程中处理复杂逻辑,比如进度条更新
4、Resources资源卸载
Resources加载过一次资源后,该资源就已知存放在内存中作为缓存。第二次加载时发现缓存中存在该资源,会直接取出来使用。因此,多次加载不会浪费内存,但是会浪费性能。
(1)卸载指定资源
使用方法:Resources.UnloadAsset()方法可以卸载指定资源。需要注意的是,该方法不能释放GameObject对象,因为它会用于实例化对象。它只能用于一些不需要实例化的内容,比如图片、音效、文本等。
void Update()
{if(Input.GetKeyDown(KeyCode.Alpha1)){print("加载资源");tex = Resources.Load<Texture>("Tex/TestJPG");}if(Input.GetKeyDown(KeyCode.Alpha2)){print("卸载资源");Resources.UnloadAsset(tex);tex = null;}
}
(2)卸载未使用资源
一般在过场景时和GC一起使用,可以顺带把垃圾回收了。会卸载所有未使用的资源。
Resources.UnloadUnusedAssets();
GC.Collect();
十、场景异步加载
1、场景同步切换
之前学到的利用SceneManager.LoadScene()切换场景时,Unity会删除当前场景上的所有对象,并且去加载下一个场景的相关信息。如果当前场景对象过多或者下一个场景对象过多,这个过程会非常耗时,会让玩家感到卡顿。
2、场景异步切换
场景异步加载和资源异步加载几乎一致。
(1)通过事件回调函数异步加载
这里使用的GUI是:SceneManager.LoadSceneAsync(),括号内传入场景名即可。场景异步加载结束后,会自行切换到新场景。和资源异步加载一样我们可以在场景加载结束时,可以在completed事件函数中写处理逻辑:
void Start()
{AsyncOperation ao = SceneManager.LoadSceneAsync("Scene2");ao.completed += (a) =>{print("加载结束");};
}
(2)通过协程异步加载
由于加载场景时会把当前场景上没有特别处理的对象都删除了,所以协程中的部分逻辑可能执行不了(协程的脚本是挂载在当前场景的对象上的,所以当场景切换完毕后,对象会被删除,协程就不会继续执行。因此在这之后执行的逻辑将不能执行)。
但事件回调函数在场景切换完成后,依然可以执行completed事件函数。原因是此时对象虽然被删除了,但这个对象仍然存储在内存中,之后垃圾回收时(GC)进行统一删除。即使在当时就触发了垃圾回收,系统会发现,这个对象依然有脚本占用着它,因此不会立马删除,还可以继续执行completed事件函数。
协程逻辑执行不了解决思路是:让处理场景加载的脚本依附的对象过场景时不被移除。这个点之前提到过:DontDestroyOnLoad(this.gameObject),这句代码可以让当前脚本依附的对象过场景不被移除。如下:
void Start()
{DontDestroyOnLoad(this.gameObject);StartCoroutine(LoadScene("Scene2"));
}IEnumerator LoadScene(string name)
{AsyncOperation ao = SceneManager.LoadSceneAsync(name);while(ao.isDone){print(ao.progress);yield return null;}print("加载结束");
}
这样,就可以在加载结束后,还继续执行协程打印出“加载结束”。当然,这里还显示了进度条、不断判断加载是否结束。和资源异步加载相同,也可以不执行判断直接加载:
void Start()
{StartCoroutine(LoadScene("Scene2"));
}IEnumerator LoadScene(string name)
{AsyncOperation ao = SceneManager.LoadSceneAsync(name);yield return ao;
}