Unity热更新之Addressables——基础(壹)

目录

目录

一、概述

1、学习前置知识点

  • C#委托
  • AssetBundle
  • Resources同步异步加载
  • ScriptableObject
  • 协同程序基本原理

2、Addressables概述

Addressables是可寻址的意思,这是可寻址资源管理系统。

从Unity2018.2版本开始,建议用于替代AssetBundle的高阶资源管理系统,后面版本中AssetBundle将渐渐被淘汰。

3、主要作用

Addressables和AssetBundle主要作用是一样的:

  • 管理资源
  • 热更新
  • 减小包的体积

4、Addressables优点

  • 自动化管理AB包的打包、发布、加载
  • 可以更方便进行本地和远程资源的加载
  • 系统会自动处理资源关联性
  • 内存管理更方便
  • 迭代更方便

5、导入

(1)在Unity中,打开Window——Package Manager

(2)设置包为Unity Registry

(3)安装Addressables包

二、基础

(一)创建配置相关文件

  • 方法一:
    • Window——>Asset Maangerment ——> Addressables ——> Groups
    • 窗口中点击Create Addressables Settings按钮,创建配置文件
  • 方法二:
    • Insperctor窗口中为资源勾选Addressabe,若没有配置文件,则会自动创建相关文件

(二)寻址资源设置

1、设置可寻址资源

  • 方式一:选中资源,勾选Inspector窗口的Addressable

  • 方式二:选中资源,拖入Addressables Groups窗口中

注意:

  • C#代码无法作为可寻址资源
  • Resources文件夹下的资源如果变为可寻址资源,会被移入Resources_moved文件夹中(因为不需要再通过Resources方式去打包加载了,避免重复打包)

2、右键选择资源的菜单内容

  • Move Addressables to Group:将该资源放入到现有的另一个组中
  • Move Addressables to New Group:使用与当前组相同设置创建一个新租,并将该资源放入该新组中
  • Rmove Addressables:移除资源,该资源会变为不可寻址资源
  • Simplify Addressable Names:简化可寻址资源名,会删除名称中的路径和拓展,简化缩短名称
  • Copy Address to Clipboard:将地址复制到剪贴板
  • Change Address:改名
  • Create New Group:创建新租

image-20260131105723621

3、资源信息

  • GroupName\Addressable Name:分组名 \ 可寻址名(可重名,描述资源)

  • Path:路径(不可重复,资源定位)

  • Labels:标签(可重复、可用于区分资源种类,例如青铜装备、黄金装备)

4、创建分组

创建:Create—>Group

分类:

  • Packed Assets:打包资源分组,默认自带默认打包加载相关设置信息

  • Blank (no schema):空白(无架构),没有相关信息需要自己关联

组对于我们来说意义重大,之后在资源打包时,一个组可以作为一个或多个 AB 包

5、右键选择某一组的菜单内容

  • Remove Group (s):移除组,组中所有资源恢复为不可寻址资源

  • Simplify Addressable Names:简化可寻址名称,会删除名称中的路径和拓展,简化缩短名称

  • Set as Default:设置为默认组,当直接勾选资源中的 Addressable 时,会自动加入该组

  • Inspect Group Setting:快速选中关联的组相关配置文件

  • Rename:重命名

  • Create New Group:创建新组

image-20260131110756244

6、配置概述相关

在Addressables Groups窗口中,左上角的Profile可以打开Manage Profiles,用于配置相关打包信息,后续会详细讲解

7、Tools工具选项

在左上角Profile的右边,目前简单了解下,后续会详细讲解

  • Inspect System Settings:检查系统设置

  • Check for content Update Restrictions:检查内容更新限制

  • Window:打开 Addressables 相关窗口

  • Groups View:分组视图相关

  • Show Sprite and Subobject Addressable:显示可寻址对象的精灵和子对象,一般想要看到图集资源内内容时可以勾选该选项

  • Group Hierarchy with Dashes:带破折号的组层次结构

8、Play Mode Script 播放模式脚本

在窗口右上角,用于确定在编辑器播放模式下运行游戏时,可寻址系统如何访问可寻址资源

  • Use Asset Database (fastest)

使用资源数据库(最快的),一般在开发阶段使用,使用此选项时,您不必构建打包可寻址内容,它会直接使用文件夹中的资源

在实际开发时,可不使用这种模式,因为没有测试的意义

  • Simulate Groups (advanced)

模拟组(后期),一般在测试阶段使用,分析布局和依赖项的内容,而不创建资产绑定。通过 ResourceManager 从资产数据库加载资产,就像通过捆绑包加载一样。通过引入时间延迟,模拟远程资产绑定的下载速度和本地绑定的文件加载速度

在开发阶段可以使用这个模式来进行资源加载

  • Use Existing Build (requires built groups)

正经的远端加载资源,使用现有构建(需要构建组),一般在最终发布测试阶段使用

从早期内容版本创建的捆绑包加载资产,在使用此选项之前,必须使用生成脚本(如默认生成脚本)打包资源

远程内容必须托管在用于生成内容的配置文件的 RemoteLoadPath 上

9、Build构建打包相关

  • New Build:构建资源(相当于打包资源分组)
  • Update a Previour Build:更新以前的版本(更新以前的AB包)
  • Clean Build:清空之前构建的资源

10、资源注意事项

  • 资源路径一定不能相同(后缀不同,名字相同可以)
  • 资源名可以随意修改(Addressable Name)
  • 之后加载资源时,可以使用名字和标签作为双标识加载指定资源
  • 资源可以按照一定的规则进行分组,如角色、装备、怪物、UI等

(三)加载指定资源

1、Addressables中的资源标识类

如果设置为公开,可以在Inspector窗口中拖入可寻址资源。

这里提供了很多方法,就不一一列举了,都是AssetReference开头的

using UnityEngine.AddressablesAssets;

AssetReference // 资源标识类,可以装载任意类型资源
AssetReferenceT<> // 加载指定类型资源
AssetReferenceAtlasedSprite // 图集资源标识类
AssetReferenceGameObject // 游戏对象资源标识类
......

2、加载资源

  • Addressables加载都用异步加载,同步加载已经弃用了。使用AssetReference.LoadAssetAsync可以加载资源
  • 加载资源后用异步加载的句柄AsyncOperationHandle接收,里面提供了很多异步加载的方法
  • handle.Completed添加资源加载完成后的事件,handle.Status查看加载状态,handle.Result获取加载的资源
public AssetReference assetReference;

private void Start()
{
    // 加载资源
    AsyncOperationHandle<GameObject> handle = assetReference.LoadAssetAsync<GameObject>();
    // 加载完成的回调事件
    handle.Completed += Handle_Completed;
}

private void Handle_Completed(AsyncOperationHandle<GameObject> handle)
{
    // 获取加载的资源并处理
    if (handle.Status == AsyncOperationStatus.Succeeded)
    {
        Instantiate(handle.Result);
    }
    
    // 另一种获取资源的方式是assetReference,但不建议
    if (assetReference.IsDone)
    {
        Instantiate(assetReference.Asset as GameObject);
    }
}

句柄(Handle):

是一个由系统(如操作系统)提供的、用于间接引用其管理的内部资源的一个抽象标识符。

它的核心思想是:“你不要直接碰资源,给我一个凭证(句柄),我来替你操作。”

3、加载场景

public AssetReference sceneReference;

private void Start()
{
    sceneReference.LoadSceneAsync().Completed += (handle) =>
    {
        Debug.Log("场景加载结束");
    };
}

4、释放资源

assetReference.ReleaseAsset();
  • 释放资源后,这一帧资源标识类assetReference中的资源会置空,但是AsyncOperationHandle中的对象好像不为空(这里唐老狮说的不是很清楚,我自己测试了一下,然后写在了第(七)节资源释放那里)

5、直接实例化

使用提供的InstantiateAsync方法可以直接把对象实例化出来,方便使用,当然只适用于能够实例化的对象

public AssetReferenceGameObject gameObjectReference;

private void Start()
{
    gameObjectReference.InstantiateAsync();
}

6、自定义标识类

了解即可:老版本(unity2020.1之前),是无法直接使用AssetReferenceT<>的,Inspector窗口会无法显示,需要自定义一个标识类,继承AssetReferenceT<>才可以使用

(四)Label标签的作用

1、关于指定资源加载方式

实际商业项目开发中,并不会经常使用这种方式,而是根据配置文件决定加载什么资源,是动态加载。因此需要学习根据名字或标签去加载对应资源,这样就可以读表进行加载

2、Label的设置

在Addressables Groups窗口中,可以为资源设置标签

3、Label的作用

相同作用的不同资源,可以让他们资源名相同,而通过Label区分他们来加载使用

如同样是一套UI,资源名一样,但是可能不同节日的UI样式不一样,这样资源名可以都相同,而只通过标签来区分

4、标签相关的特性

通过这个特性,可以限制AssetReference只能使用指定Label的资源

[AssetReferenceUILabelRestriction("Label1", "Label2", ...)]
public AssetReference assetReference;

(五)动态加载单个资源

1、通过资源名/标签名加载资源

Addressables.LoadAssetAsync<资源类型>("资源名/标签名");

AsyncOperationHandle<GameObject> handle = Addressables.LoadAssetAsync<GameObject>("Cube");
handle.Completed += (handle) =>
{
    if (handle.Status == AsyncOperationStatus.Succeeded)
    {
        Instantiate(handle.Result);
    }
};
  • 如果存在同名或同标签的同类型资源,那么无法确定加载哪一个,会自动加载第一个满足条件的
  • 如果存在同名或同标签的不同类型资源,可以通过泛型指定加载哪一个

2、释放资源

Addressables.Release(handle);

3、场景加载

  • 参数1:场景名
  • 参数2:加载模式(单独还是叠加)
  • 参数3:加载完后是否激活(false则加载完后不会切换,需要自己使用返回的加载对象SceneInstanceActivateAsync()方法
  • 参数4:场景加载异步操作的优先级
Addressables.LoadSceneAsync("SampleScene",
                            UnityEngine.SceneManagement.LoadSceneMode.Single,
                            true,
                            100);

(六)动态加载多个资源

1、根据单个信息动态加载多个资源

动态加载多个资源使用Addressables.LoadAssetsAsync,有多个重载,但常用的就两种方式:

方式一:加载完资源后,直接对多个资源进行处理

  • 参数1:资源名/标签名
  • 参数2:每个资源加载完后的回调函数
  • 参数3(可选):资源加载失败时,是否自动释放已加载和依赖的资源。如果为false,则手动管理释放
Addressables.LoadAssetsAsync<Object>("Cube", (obj) =>
{
    Debug.Log(obj.name); // 每个资源加载结束后的回调函数
});

方式二:使用handle(推荐)

AsyncOperationHandle<IList<Object>> handle = Addressables.LoadAssetsAsync<Object>("Cube", 
    (obj) => {
        // 可不处理,交给handle处理
    });

handle.Completed += (handle) =>
{
    foreach (var obj in handle.Result)
    {
        Debug.Log(obj.name);
    }

    // 如果要释放资源,这种方式方便些,因为已经记录了handle
    Addressables.Release(handle);
};

2、根据多个信息动态加载多个资源

之前只能通过资源名或标签名加载资源,现在可以多个资源或标签名加载了:

使用Addressables.LoadAssetsAsync的重载函数即可实现:

  • 参数1:加载资源的条件列表(资源名/标签名)
  • 参数2:每个资源加载完后的回调函数
  • 参数3:可寻址合并模式,即资源结果的合并方式,使用Addressables.MergeMode枚举
  • 参数4(可选):资源加载失败时,是否自动释放已加载和依赖的资源。如果为false,则手动管理释放
List<string> strs = new List<string>() { "Cube","Red" };
Addressables.LoadAssetsAsync<Object>(strs, (obj) => 
{
    Debug.Log(obj.name); // 每个资源加载结束后的回调函数
}, Addressables.MergeMode.Intersection);

当然这里也是推荐用handle去处理

3、可寻址合并模式

假设上面例子中两个条件CubeRed,找到的资源分别为[1,2,3][1,3,4]

  • None:不发生合并,使用第一组结果[1,2,3]
  • UseFirst:使用第一组结果[1,2,3]
  • Union:合并所有结果(并集),结果为[1,2,3,4]
  • Intersection:使用相交的结果(交集),结果为[1,3](找出满足所有标签的)

(七)关于资源获取释放的补充

1、简单概述

在之前资源加载的学习中,我们发现资源可以从AssetReference中加载/释放,也可以从Addressables中加载/释放。

获取资源时,若使用AssetReference加载,则可以从assetReference.Asset获取,或从返回的句柄handle.Result中获取。

而使用Addressables加载,则可以从返回的句柄handle.Result中获取。

2、两者关系

那么AssetReferenceAddressables有什么关系呢?实际上AssetReference资源加载和释放都是通过调用Addressables的相关API实现的。而assetReference.Asset其实获取的是handle.Result,只是包装成了属性。因此AssetReference可以看作是对Addressables的再一层封装。

3、关于第(三)节的小问题

  • 如果看不太懂直接看总结

使用assetReference加载资源后会返回一个句柄handle,之后在加载完成的回调函数中,assetReference.ReleaseAsset()释放资源后,发现assetReference.Asset为null,但是handle.Result还有资源:

AsyncOperationHandle<GameObject> handle = assetReference.LoadAssetAsync<GameObject>();

handle.Completed += (handle) => {
    GameObject cube = Instantiate(handle.Result); // 使用资源
    assetReference.ReleaseAsset(); // 释放资源

    Debug.Log(assetReference.Asset); // 打印 null
    Debug.Log(handle.Result); // 打印 Cube (UnityEngine.GameObject)
};

此时就有个疑问,难道handle的资源没释放吗,是因为handle引用了这个资源?

其实不是的,看了源码之后,发现这个AsyncOperationHandle是struct,即值类型,也就是说应该没有资源引用的问题,资源是被卸载了的,然后看了下assetReference.ReleaseAsset()的源码,并且问了AI,这个资源释放大概是这样的流程,不讲太详细,知道会大概会发生什么就行:

  • 调用assetReference.ReleaseAsset()时:
    • 内部调用类似handle.Release()方法,释放掉assetReference创建资源时的句柄
    • 释放句柄时,资源引用计数-1,然后句柄引用的底层的AsyncOperationBase类成员变量m_InternalOp会被置空,此时句柄的IsValid()会变成false,表示无效,而使用assetReference.Asset获取的资源其实就是从句柄中获取的资源,因此这时会返回null
    • 但是回调函数里的句柄目前还不受影响,因为该句柄是值类型,目前这个句柄还保留之前的状态,即底层的m_InternalOp引用还在,因此此时IsValid()还是true,所以handle.Result可以返回资源。
    • 但是到了帧末,m_InternalOp被销毁,handle.Result就无法获取资源了

4、总结

如果上面看不太懂,那么就看这个总结好了:

使用assetReference.ReleaseAsset()释放资源时:

  • assetReference.Asset立刻为null,它内部的句柄已经无效
  • 但当前帧内,资源还在,可以从加载完成回调的handle.Result获取
  • 等到下一帧,底层资源已被彻底销毁,handle 也变为无效状态,资源就完全获取不到了。