Unity笔记(八)——资源动态加载、场景异步加载

发布于:2025-08-31 ⋅ 阅读:(27) ⋅ 点赞:(0)

写在前面:

写本系列(自用)的目的是回顾已经学过的知识、记录新学习的知识或是记录心得理解,方便自己以后快速复习,减少遗忘。

九、资源动态加载

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


网站公告

今日签到

点亮在社区的每一天
去签到