【游戏开发探究】Unity Addressables资源管理方式用起来太爽了,资源打包、加载、热更变得如此轻松(Addressable Asset System | 简称AA)

2023-05-16

文章目录

      • 一、前言
      • 二、为什么推荐使用Addressables
      • 三、Addressables基础操作教程
        • 1、下载Addressables插件
        • 2、创建Addressables Settings
        • 3、给Group添加资源
        • 4、创建新的Group
        • 5、设置Build Path与Load Path
        • 6、修改RemoteBuildPath和RemoteLoadPath
        • 7、打Addressable资源包
        • 8、打Android APK
        • 9、加载Addressable资源
          • 9.1、方式一:通过Addressable Name来加载资源
          • 9.2、方式二:通过AssetReference来加载资源
        • 10、Addressable资源三个加载模式
          • 10.1、Use Asset Database (fastest)
          • 10.2、Simulate Groups (advanced)
          • 10.3、Use Exising Build(requires built groups)
        • 11、加载远程Addressable资源
          • 11.1、启用Hosting Services
          • 11.2、加载远程包的柯南图片
          • 11.3、Addressables是如何知道去哪里加载资源的
          • 11.4、打个APK包瞧瞧
        • 12、如何把Group里的资源打成多个bundle文件
        • 13、使用Labels对Group内的资源进行二级分组
          • 13.1、新建Label
          • 13.2、给资源设置Label
          • 13.3、修改Bundle Mode为Pack Together By Label
          • 13.3、一个资源标记多个Label
        • 14、批量加载同一个Label的所有资源(AssetLabelReference)
        • 15、打资源热更包
          • 15.1、开启Build Remote Catalog
          • 15.2、打包Addressable资源包
          • 15.3、加载小兰的图片
          • 15.4、打成APK
          • 15.5、替换小兰的图片
          • 15.6、打热更包,Update a Previous Build
          • 15.7、上传热更包
          • 15.8、热更测试
        • 16、提前检测更新并下载(预下载)
        • 17、Addressable资源释放
        • 18、打包工具集成Addressable打包流程
      • 四、答疑补充(Q&A)
        • 1、Addressables加载场景进度监听
      • 五、完毕

一、前言

嗨,大家好,我是新发。
之前就有看过UnityAddressable Asset System,简称AA,但那时候这个功能刚出来,出于稳定性考虑,所以暂时没有去使用它。现在,它已经迭代到1.16.19 Release版本了(中国版是1.19.16),经过了时间考验,可以拿出来讲讲啦,网上其实已经有不少讲Addressables系统的文章,不过很多不是最新版的教程,今天我就来写一下最新版的Addressables系统的使用教程吧~

二、为什么推荐使用Addressables

我在之前的好几篇文章中都介绍过Unity加载资源的几种方式,我还画过一个图,详细可以看我之前写的这篇文章:《Unity游戏开发——新发教你做游戏(三):3种资源加载方式》
在这里插入图片描述
可以看出来,资源的加载方式要根据应用场景进行区分,要注意资源存放的文件夹,要注意不同平台下的差异,如果使用AssetBundle形式,加载的时候要小心AB包之间的依赖关系,我之前也写了一篇文章讲如何去加载AB的依赖资源,《Unity 打包与加载AssetBundle(加载对应的依赖)》
如果要做资源热更新,也要自己去写工具实现增量资源包的打包,然后自己实现热更检测、资源下载、MD5比对,解压等等逻辑,我之前还专门写了一篇文章讲了热更新的流程,《【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)》
在这里插入图片描述
相信你看完我的这些文章,就会吐槽,哇,好麻烦啊,对于新手来说,可能就是劝退,现在呢,不用怕了,Addressables系统统统搞定,用起来实在是太香了,本博主强烈推荐!

三、Addressables基础操作教程

1、下载Addressables插件

点击菜单Window / Package Manager,打开插件包管理界面,
在这里插入图片描述
搜索addressables,可以看到有两个插件包,带.CN结尾的是中国增强版,
在这里插入图片描述
这里要补充说一下,Addressables的打包方式其实也是AssetBundle格式,只是在此基础上做了一层封装,方便进行管理(比如打包、加载、依赖等)。而我们知道,没有加密的AssetBundle是可以使用AssetStudio等工具轻易进行资源逆向的,

注:AssetStudio资源逆向工具开源地址:https://github.com/Perfare/AssetStudio

Addressables.CN版本会对AssetBundle做加密处理,
在这里插入图片描述
为了方便下文演示资源逆向,我这里就先下载不带.CN结尾的版本,
在这里插入图片描述

注:实际项目中,建议大家下载Addressables.CN版本。

安装成功后,可以看到多出了一个Window / Asset Management / Addressables菜单,在这里插入图片描述

2、创建Addressables Settings

点击Groups菜单,
在这里插入图片描述
点击Create Addressables Settings按钮,
在这里插入图片描述
此时工程目录中会生成一个AddressableAssetsData文件夹,里面有很多设置文件,
在这里插入图片描述
我们看回Addressables Groups窗口,可以看到它默认创建了一个组:Default Local Group (Default)
在这里插入图片描述
Addressables 默认是按Group为颗粒进行AssetBundle打包的,比如我把资源A、B、C都放在这个Default Local Group组里,那么它们会被打在同一个AssetBundle中(我们也可以修改成按单独的资源文件为颗粒进行打包,下文我会讲如何设置),下面我演示一下如何给Group添加资源。

3、给Group添加资源

我这里先随便制作两个预设,
在这里插入图片描述
在这里插入图片描述
放在Prefabs文件夹中,
在这里插入图片描述
文件名字可以任意起,注意不要跟Unity默认的文件系统文件夹名字相同,比如ResourcesStreamingAssets等,除非你真的是想使用这些特定目录的功能,

接着我们把预设文件直接拖到对应的Group中即可,如下,
请添加图片描述

注:我演示的是预设资源,你也可以是其他任意的资源,比如声音、图集、动画、材质等等)

选中资源文件,在Inspector窗口中勾选Addressable,它也会自动添加到默认的Group中,如下
请添加图片描述

4、创建新的Group

上面的默认Group一般是作为包内资源,现在我们创建一个新的Group作为包外资源的组(通过远程加载资源)。
如下,在Addressables Groups窗口中,点击左上角的Create按钮,点击Group / Packed Assets菜单,
在这里插入图片描述
此时会创建一个新的Group,如下,
在这里插入图片描述
我们右键它,可以对它重命名,
在这里插入图片描述
这里我就将其重命名为RemoteGroup吧,
在这里插入图片描述
其实每个Group都是一个配置文件,可以在AddressableAssetsData / AssetGroups目录中找到对应的文件,如下
在这里插入图片描述
如果你用文本编辑器打开它,就会看到它里面是YAML格式的配置,比如打开Default Local Group,可以看到它里面记录了包含的资源文件,
在这里插入图片描述
继续我们的操作,下面我们给这个RemoteGroup添加资源,我找了一张柯南的图片,
在这里插入图片描述
把柯南图片拖到RemoteGroup中,如下,
在这里插入图片描述
现在RemoteGroup如何打成一个包体外的资源包呢?又如何去加载这些Group资源包呢?请继续往下看~

5、设置Build Path与Load Path

我们选中RemoteGroup,在Inspector窗口中,将Build Path改为RemoteBuildPath,将Load Path改为RemoteLoadPath,如下,
在这里插入图片描述
这样子,RemoteGroup打出来的资源就不会在包体内了,它会被打到工程目录的ServerData/Android目录中,
在这里插入图片描述
而加载的时候,会去远程http://localhost/Android这个地址加载(这里是localhost,我们可以改成别的IP或域名地址)
在这里插入图片描述
画个图
在这里插入图片描述

6、修改RemoteBuildPath和RemoteLoadPath

如果你想修改RemoteBuildPathRemoteLoadPath,可以在Addressables Groups窗口中点击Manage Profiles菜单,
在这里插入图片描述
也可以点Window / Asset Management /Addressables / Profiles菜单,
在这里插入图片描述
此时会打开Addressables Profiles窗口,我们可以修改修改RemoteBuildPathRemoteLoadPath
在这里插入图片描述
这里我先不修改,下文搭建Web服务器后再来改RemoteLoadPath

7、打Addressable资源包

Addressables Groups窗口中,点击Build / New Build / Default Build Script,就会开始打Addressable资源包了,等它打包完毕即可,
在这里插入图片描述
上文中我们建了两个Group,其中Default Local Group作为包体内的资源包,RemoteGroup作为包体外的资源包,
在这里插入图片描述
现在我们去看看它们分别Build到哪里去了吧~
首先看下Default Local Group,可以看到它的Build Path是在Library/com.unity.addressables/aa/Android/Android中,
在这里插入图片描述
我们进如到这个目录中,可以看到对应的.bundle文件,
在这里插入图片描述
其实它就是一个AssetBundle格式的文件,我们可以使用AssetStudio对它进行逆向,可以看到逆向后,里面正是我们上文中添加的两个预设文件,
在这里插入图片描述
接着我们看下Remote Group打出来的资源包,它是在ServerData/Android目录中,
在这里插入图片描述
我进入工程目录的ServerData/Android目录中,可以看到RemoteGroup的资源包,
在这里插入图片描述画个图
在这里插入图片描述

同样使用AssetStudio对它进行逆向,可以看到我们的柯南就在里面~
在这里插入图片描述

8、打Android APK

现在,我们打个AndroidAPK包看看,
在这里插入图片描述
正常打出APK
在这里插入图片描述
我们把.apk改为.zip,然后使用7z等解压工具解压它,
在这里插入图片描述
进入文件夹里面asset/aa/Android目录,可以看到我们的Default Local Group资源包就在里面,
在这里插入图片描述
没错,其实Unity就是把整个Library/com.unity.addressables/aa/Android目录塞到包内的assets/aa目录中,
在这里插入图片描述
我们的RemoteGroup因为是远程资源包,它被留在了工程目录的ServerData/Android中,没有进入包体内,
在这里插入图片描述
好了,现在Addressable包也打了,APK包也打了,还没讲如何加载资源,接下来就来讲讲如何加载Addressable资源吧~

9、加载Addressable资源

我们创建一个C#脚本,我这里就创建一个Main.cs脚本吧,
在这里插入图片描述

9.1、方式一:通过Addressable Name来加载资源

我们加载资源的时候,并不需要知道目标资源到底是在哪个Group中,也不需要知道这个Group到底是本地资源包还是远程资源包,统一通过资源的Addressable Name来加载,资源的Addressable Name在哪里看呢?
比如Cube预设,在Inspector窗口中,可以看到它的Addressable NameAssets/Prefabs/Cube.prefab,这个Addressable Name默认是资源被加入Group时的相对路径,
在这里插入图片描述
我们可以修改Addressable Name,比如我改成HelloCube也是可以的,它仅仅是作为一个索引的字符串,当我们把Cube预设移动到其他的目录中,这个Addressable地址并不会变,
在这里插入图片描述
这里我们还是保持为Assets/Prefabs/Cube.prefab吧,
在这里插入图片描述
开始写代码,首先引入命名空间

using UnityEngine.AddressableAssets;

然后使用Addressables.LoadAssetAsync方法加载资源,监听Completed 回调,在回调中拿到资源然后进行操作,示例:

using UnityEngine;
using UnityEngine.AddressableAssets;

public class Main : MonoBehaviour
{
    void Start()
    {
        Addressables.LoadAssetAsync<GameObject>("Assets/Prefabs/Cube.prefab").Completed += (handle) =>
        {
            // 预设物体
            GameObject prefabObj = handle.Result;
            // 实例化
            GameObject cubeObj = Instantiate(prefabObj);
        };
    }
}

Addressables还提供了InstantiateAsync接口,方便直接一步到位实例化,示例:

Addressables.InstantiateAsync("Assets/Prefabs/Cube.prefab").Completed += (handle) =>
{
    // 已实例化的物体
    GameObject cubeObj = handle.Result;
};

有些人可能不喜欢使用回调的方式,喜欢使用asyncawait的方式,示例:

using UnityEngine;
using UnityEngine.AddressableAssets;
using System.Threading.Tasks;

public class Main : MonoBehaviour
{
    void Start()
    {
        InstantiateCube();
    }
	
	private async void InstantiateCube()
	{
		// 虽然这里使用了Task,但并没有使用多线程
		GameObject prefabObj = await Addressables.LoadAssetAsync<GameObject>("Assets/Prefabs/Cube.prefab").Task;
		// 实例化
		GameObject cubeObj = Instantiate(prefabObj);
		
		// 也可直接使用InstantiateAsync方法
		// GameObject cubeObj = await Addressables.InstantiateAsync("Assets/Prefabs/Cube.prefab").Task;
	}
}

我们把Main.cs脚本挂到Main Camera相机上,
在这里插入图片描述
运行Unity,可以看到,正常加载了Cube并实例化了,
在这里插入图片描述
我们顺便测试一下把CubeAddressable Name改为HelloCube
在这里插入图片描述
加载的代码也改下,
在这里插入图片描述
运行测试,可以看到,依然可以正常加载Cube
在这里插入图片描述
这个时候,应该有同学会疑惑了,我改了Addressable Name,但我都没有重新Build资源包,怎么可以正常加载到资源呢?其实Addressables系统为了方便我们在Editor环境下方便测试,默认都是直接从Asset Database加载的,我们可以在Addressables Groups窗口的Play Mode Script中进行切换,建议编辑器环境下使用Use Asset Database (fastest)即可,下面两个选项下文我会讲具体使用,这里先维持原样~
在这里插入图片描述

9.2、方式二:通过AssetReference来加载资源

我们知道,脚本中如果声明了一个public变量,默认会进行序列化,可以在Inspector窗口中对它进行设置,我们声明一个publicAssetReference 成员,如下,

// Asset弱引用
public AssetReference spherePrefabRef;

我们把Sphere预设拖给这个spherePrefabRef成员,
在这里插入图片描述
如果我们声明的不是AssetReference 类型,而是GameObject类型,那么场景就直接依赖了Sphere预设,这个Sphere预设会被打到场景中,但我们这里用的是AssetReference ,场景并不会真的依赖Sphere预设,它是一个弱引用。
好了我们继续,我们使用AssetReference LoadAssetAsync方法进行异步加载,监听Completed 回调,如下

using UnityEngine;
using UnityEngine.AddressableAssets;

public class Main : MonoBehaviour
{
    public AssetReference spherePrefabRef;

    void Start()
    {
        spherePrefabRef.LoadAssetAsync<GameObject>().Completed += (obj) =>
        {
            // 预设
            GameObject spherePrefab = obj.Result;
            // 实例化
            GameObject sphereObj = Instantiate(spherePrefab);
        };
    }
}

同样,AssetReference 也提供了InstantiateAsync方法,方便一步到位进行实例化,例,

spherePrefabRef.InstantiateAsync().Completed += (obj) =>
{
    // 已实例化的物体
    GameObject sphereObj = obj.Result;
};

我们运行Unity,测试效果如下,可以看到球体预设正常被加载并实例化了,
在这里插入图片描述

10、Addressable资源三个加载模式

Addressables资源加载模式有三个,如下,默认情况下是Use Asset Database (fastest)
在这里插入图片描述

Use Asset Database (fastest) 直接加载文件而不打包,快速但Profiler获取的信息较少
Simulate Groups (advanced) 在不打包的情况下模拟AssetBundle的操作
Use Exising Build(requires built groups) 实际上是从AssetBundle打包和加载

10.1、Use Asset Database (fastest)

在这个模式下,Addressables系统会直接从AssetDatabase加载资源,我们 不需要 BuildAddressable资源包,这个加载速度最快,建议在项目开发阶段使用这个模式,加载速度快。

10.2、Simulate Groups (advanced)

这个模式下,也是 不需要 BuildAddressable资源包的,那它与Use Asset Database (fastest)有什么区别呢?
让我先来操作一波,首先,我们选中AddressableAssetSettings,然后勾选Send Profiler Events,勾选之后,我们可以在Addressable的分析面板中查看到一些调试信息,
在这里插入图片描述
我们点击菜单Window / Asset Management / Addressables / Event Viewer,打开分析器,
在这里插入图片描述
我们运行Unity,在分析器中可以看到我们实例化出来的预设所依赖的资源,还可以看到引用计数等信息,虽然我们没有打出AssetBundle包,但却模拟了类似从AssetBundle包中加载资源的效果,这样可以方便我们快速分析加载策略,
在这里插入图片描述
我们对比一下,如果我切换为Use Asset Database (fastest)模式,看,资源加载、依赖、引用等相关信息都没有了,
在这里插入图片描述
结论就是Simulate Groups (advanced)会进行AssetBundle加载的模拟,并统计分析数据,方便我们进行快速分析,它并不是真的去加载AssetBundle,所以我们不需要执行Build操作。

10.3、Use Exising Build(requires built groups)

这个模式下,需要先执行Build打出Addressable资源包,它会根据Load Path去加载真正的AssetBundle文件并读取资源。如果不先Build,运行时会报错,

Player content must be built before entering play mode with packed data.  
This can be done from the Addressables window in the Build->Build Player Content menu command.
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)

如下
在这里插入图片描述
如果执行了Build打出了Addressable资源包,但是把.bundle包手动删掉,运行Unity也会报错,

Exception encountered in operation
Resource<IAssetBundleResource>(xxxxxxxxxxxxx.bundle), status=Failed, result= : Invalid path in AssetBundleProvider:
...

如下:
在这里插入图片描述
我们执行一下Build / New Build / Default Build Script,打出Addressable资源包,
在这里插入图片描述
运行Unity,可以正常从Addressable.bundle资源包中加载资源,如下
在这里插入图片描述
在这个模式下,我们也是可以在Event Viewer窗口中对资源加载进行分析的,
在这里插入图片描述

11、加载远程Addressable资源

我们上面的Remote Group是打成包外资源的,我们想要在Editor环境下测试远程加载,这个时候就需要先搭建一个Web服务器了,支持通过http请求来获取资源。Addressable系统已经帮我们做了一个Hosting Services工具,方便我们快速启动一个Web服务器。

11.1、启用Hosting Services

点击菜单Window / Asset Management / Addressables / Hosting
在这里插入图片描述
接着点击Create / Local Hosting,创建一个本地Web服务器,
在这里插入图片描述
然后勾选Enable
在这里插入图片描述
这样我们就开启了一个本地的服务器了,IP地址是我本机的局域网IP,我可以通过localhost进行访问,注意这里的端口号是62762
我们可以看到,它对我们上文中提到的两个文件夹目录进行了Hosting
在这里插入图片描述

11.2、加载远程包的柯南图片

因为我们上面已经指向过Build打了资源包,所以这里我们就直接写加载资源的代码吧。
我们改一下Main.cs脚本,让它去加载柯南的图片,我们知道,柯南图片在Remote Group组里,它是在包外的,但我们代码上并不用管它到底是包内还是包外,我们使用Addressable Name来加载,柯南图片的Addressable NameAssets/Textures/kenan.jpg
在这里插入图片描述
上代码,

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.UI;

public class Main : MonoBehaviour
{
    public RawImage img;
    void Start()
    {
       	Addressables.LoadAssetAsync<Texture2D>("Assets/Textures/kenan.jpg").Completed += (obj) =>
        {
            // 图片
            Texture2D tex2D = obj.Result;
            img.texture = tex2D;
            img.GetComponent<RectTransform>().sizeDelta = new Vector2(tex2D.width, tex2D.height);
        };
    }
}

使用UGUI创建一张RawImage,并赋值给Main脚本的img成员,如下,
在这里插入图片描述
运行Unity,效果如下,可以看到,成功加载了远程资源,
在这里插入图片描述

注:如果你是win11系统,可能使用Hosting Services会无法访问,具体原因不明,win11各种恶心的问题,放弃治疗,把资源放到https服务器可以正常加载。

11.3、Addressables是如何知道去哪里加载资源的

假设我现在把资源托管到GitCode上,我把RemoteLoadPath改为GitCode的地址,如下
在这里插入图片描述
然后我执行AddressablesBuild打出资源包,如下是RemoteGroup资源包,里面包含了柯南的图片,
在这里插入图片描述
我们把它丢到GitCode上,如下
在这里插入图片描述
我运行Unity,可以看到,它从GitCode上正常加载了柯南的图片,
在这里插入图片描述
如何证明它是从GitCode上下载下来的呢?我们把工程里的柯南图片删了,
在这里插入图片描述
ServerData/Android目录中的RemoteGroup.bundle包也删了,
在这里插入图片描述
由于我们已经运行过一次,远程的资源会在本地缓存,我们使用Everythine搜索一下.bundle文件的那串哈希值,可以看到一个同名的文件夹,我们进入里面,
在这里插入图片描述
可以看到有个__data文件,它就是.bundle的缓存文件,柯南就在里面,我们可以使用AssetStudio__data进行逆向,看到了吗,我们的柯南又出现了,
在这里插入图片描述
好,我们把缓存也干掉,
在这里插入图片描述
这个时候,我们再运行一下Unity,柯南又重新加载出来了,并且刚刚的缓存目录又出现了,
在这里插入图片描述它是从GitCode上加载并缓存到本地的。
我们通过Assets/Textures/kenan.jpg这个Addressable Name就可以加载到GitCode的柯南图片,这个对应关系是记录在哪里的呢?答案就在catalog.json文件中,这个文件我们在上文中解APK包的时候也有看到,事实上整个aa/Android目录下的文件都会塞入APKassets目录中,
在这里插入图片描述
我们打开它,可以看到我们配置的GitCode地址就在里面,
在这里插入图片描述

11.4、打个APK包瞧瞧

我们发布成APK,在Android模拟器中去看看效果,
在这里插入图片描述
运行,效果如下
在这里插入图片描述
我们打开文件管理器,进入Android/data/包名/files目录中,可以看到有个UnityCache文件夹,
在这里插入图片描述
继续往里走,那串熟悉的哈希值又出现了,它就是我们放在GitCode上的RemoteGroup.bundle文件的哈希值,它被下载到本地缓存在这个目录中,在这里插入图片描述
我们继续进入,里面的__data文件就是.bundle文件本君,柯南就在里面,
在这里插入图片描述

12、如何把Group里的资源打成多个bundle文件

我们如果继续给RemoteGroup添加资源,比如我们再放一张小兰的图片,
在这里插入图片描述
把柯南小兰都放在RemoteGroup组里,
在这里插入图片描述
这个时候我们执行Addressable资源打包,柯南和小兰是在同一个.bundle文件中的,
在这里插入图片描述
AssetStudio逆向验证一下,嗯,他们是在一起的,
在这里插入图片描述
假设这个.bundle文件还包含了好多好多其他的资源,导致这个.bundle包有几十M那么大,而我们只是想要加载柯南的图片,这个时候它会先去下载整个.bundle到本地,然后再去这个.bundle文件中去加载柯南的图片,时间耗在了下载大文件上了,能不能将颗粒度细化到以单独的资源文件为颗粒呢?
答案是可以的,选中RemoteGroup,然后将Bundle` Mode改为Pack Separately,这样就是以每个独立的资源文件为颗粒打成.bundle文件了,
在这里插入图片描述

我们重新执行Build,可以看到目录根据资源类型进行细化了,.bundle文件也细分成了两个,柯南和小兰分开了,
在这里插入图片描述

13、使用Labels对Group内的资源进行二级分组

实际项目中,不会真的细到这么细的颗粒,会使用Labels来对Group内的资源进行二级分组,默认只有一个defaultLabel
在这里插入图片描述

13.1、新建Label

我们可以点击Tools / Labels菜单,新建一个Label
在这里插入图片描述
点击+号,
在这里插入图片描述
新建一个texture吧,
在这里插入图片描述

13.2、给资源设置Label

接着我们把柯南和小兰的Label都设置为texture
在这里插入图片描述
如下
在这里插入图片描述
为了演示,我再加一个柯南主题曲,并标记它的Labelaudio
在这里插入图片描述

13.3、修改Bundle Mode为Pack Together By Label

接着,我们把RemoteGroupBundle Mode改为Pack Together By Label
在这里插入图片描述
这样,一个Group就会以Label为颗粒细分成多个.bundle,组织是更加灵活,我们执行Addressable资源打包,如下,
在这里插入图片描述

13.3、一个资源标记多个Label

事实上,一个资源可以同时标记多个Label,比如我把柯南同时标记为defaulttexture,如下,
在这里插入图片描述
我们执行Addressable资源打包,如下,他们又分开了,
在这里插入图片描述

14、批量加载同一个Label的所有资源(AssetLabelReference)

我们给资源标记了Label之后,就可以批量加载指定了同一个Label的所有资源了,比如我想加载标记了texture的所有资源,就可以这样加载,

using System.Linq.Expressions;
using UnityEngine;
using UnityEngine.AddressableAssets;


public class Main : MonoBehaviour
{
    public AssetLabelReference textureLabel;

    void Start()
    {
        Addressables.LoadAssetsAsync<Texture2D>(textureLabel, (texture) =>
        {
            // 没加载完一个资源,就回调一次
            Debug.Log("加载了一个资源: " + texture.name);
        });
    }
}

Main.cs脚本的Texture Label成员设置为texture,如下,
在这里插入图片描述
运行Unity,可以看到,回调了两次,分别加载了柯南和小兰的图片,
在这里插入图片描述

15、打资源热更包

15.1、开启Build Remote Catalog

想要支持热更新,需要先开启Catalog,选中AddressableAssetSettings,然后勾选Build Remote Catalog,如下,
在这里插入图片描述

15.2、打包Addressable资源包

然后执行Build打包Addressable资源包,
此时除了生成.bundle包,还生成了catalog.hash.json文件,如下
在这里插入图片描述
有了catalog的这两个文件,我们后续打增量包的时候它才能对比出哪些文件发生了变化,把变化的资源文件打成热更的.bundle包,我们先把打出来的catalog.bundle包丢到服务器上,如下,
在这里插入图片描述

15.3、加载小兰的图片

我们先写段代码去加载小兰的图片,

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.UI;

public class Main : MonoBehaviour
{
    public RawImage img;
    void Start()
    {
        Addressables.LoadAssetAsync<Texture2D>("Assets/Textures/xiaolan.png").Completed += (obj) =>
          {
                // 图片
                Texture2D tex2D = obj.Result;
              img.texture = tex2D;
              img.GetComponent<RectTransform>().sizeDelta = new Vector2(tex2D.width, tex2D.height);
          };
    }
}

运行Unity,可以正常加载,
在这里插入图片描述

15.4、打成APK

我们打成APK包,在Android模拟器上运行,可以正常加载到小兰的图片,
在这里插入图片描述

15.5、替换小兰的图片

现在,我们想热更小兰的图片,换成这张,
在这里插入图片描述
现在RemoteGroup组里小兰的图片被我们换成了新的了,
在这里插入图片描述

15.6、打热更包,Update a Previous Build

我们点击Update a Previous Build菜单,如下
在这里插入图片描述
此时会打开一个窗口,然你选择.bin文件,因为我是Android平台,所以进入Android目录中,
在这里插入图片描述
选择addressables_content_state.bin文件,
在这里插入图片描述
它会解析catalog文件然后进行差异对比,发现小兰的图片资源发生了变化,就会把它打成一个热更的.bundle包了,
在这里插入图片描述

15.7、上传热更包

我们把热更包丢到服务器上,
在这里插入图片描述

15.8、热更测试

现在,我们在Android模拟器上重新运行APP,可以看到,小兰的图片自动热更成新的图片了,
在这里插入图片描述

16、提前检测更新并下载(预下载)

有时候我们希望启动后执行更新检测并下载所有资源后再进入游戏,可以使用CheckForCatalogUpdates -> UpdateCatalogs -> GetDownloadSizeAsync -> DownloadDependenciesAsync这个工作流。
这个过程中可能有强退、断网等异常,我们可以判断AsyncOperationHandleStatus状态码然后进行处理,并提示重试。

示例:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.UI;

// 检测更新并下载资源
public class CheckUpdateAndDownload : MonoBehaviour
{
    /// <summary>
    /// 显示下载状态和进度
    /// </summary>
    public Text updateText;

    /// <summary>
    /// 重试按钮
    /// </summary>
    public Button retryBtn;

    void Start()
    {
        retryBtn.gameObject.SetActive(false);
        retryBtn.onClick.AddListener(() =>
        {
            StartCoroutine(DoUpdateAddressadble());
        });

        // 默认自动执行一次更新检测
        StartCoroutine(DoUpdateAddressadble());
    }

    IEnumerator DoUpdateAddressadble()
    {
        AsyncOperationHandle<IResourceLocator> initHandle = Addressables.InitializeAsync();
        yield return initHandle;

        // 检测更新
        var checkHandle = Addressables.CheckForCatalogUpdates(true);
        yield return checkHandle;
        if (checkHandle.Status != AsyncOperationStatus.Succeeded)
        {
            OnError("CheckForCatalogUpdates Error\n" +  checkHandle.OperationException.ToString());
            yield break;
        }

        if (checkHandle.Result.Count > 0)
        {
            var updateHandle = Addressables.UpdateCatalogs(checkHandle.Result, true);
            yield return updateHandle;

            if (updateHandle.Status != AsyncOperationStatus.Succeeded)
            {
                OnError("UpdateCatalogs Error\n" + updateHandle.OperationException.ToString());
                yield break;
            }

            // 更新列表迭代器
            List<IResourceLocator> locators = updateHandle.Result;
            foreach (var locator in locators)
            {
                List<object> keys = new List<object>();
                keys.AddRange(locator.Keys);
                // 获取待下载的文件总大小
                var sizeHandle = Addressables.GetDownloadSizeAsync(keys.GetEnumerator());
                yield return sizeHandle;
                if (sizeHandle.Status != AsyncOperationStatus.Succeeded)
                {
                    OnError("GetDownloadSizeAsync Error\n" + sizeHandle.OperationException.ToString());
                    yield break;
                }

                long totalDownloadSize = sizeHandle.Result;
                updateText.text = updateText.text + "\ndownload size : " + totalDownloadSize;
                Debug.Log("download size : " + totalDownloadSize);
                if (totalDownloadSize > 0)
                {
                    // 下载
                    var downloadHandle = Addressables.DownloadDependenciesAsync(keys, true);
                    while (!downloadHandle.IsDone)
                    {
                        if (downloadHandle.Status == AsyncOperationStatus.Failed)
                        {
                            OnError("DownloadDependenciesAsync Error\n"  + downloadHandle.OperationException.ToString());
                            yield break;
                        }
                        // 下载进度
                        float percentage = downloadHandle.PercentComplete;
                        Debug.Log($"已下载: {percentage}");
                        updateText.text = updateText.text + $"\n已下载: {percentage}";
                        yield return null;
                    }
                    if (downloadHandle.Status == AsyncOperationStatus.Succeeded)
                    {
                        Debug.Log("下载完毕!");
                        updateText.text = updateText.text + "\n下载完毕";
                    }
                }
            }
        }
        else
        {
            updateText.text = updateText.text + "\n没有检测到更新";
        }

        // 进入游戏
        EnterGame();
    }

    // 异常提示
    private void OnError(string msg)
    {
        updateText.text = updateText.text + $"\n{msg}\n请重试! ";
        // 显示重试按钮
        retryBtn.gameObject.SetActive(true);
    }


    // 进入游戏
    void EnterGame()
    {
        // TODO
    }
}

17、Addressable资源释放

最后补充一下Addressable的资源释放。
就以加载小兰图片为例,我们加载完毕后,把RawImage销毁,并不会释放内存中的Texture2D对象,比如这样子

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.UI;

public class Main : MonoBehaviour
{
    public RawImage img;
    public Button freeResBtn;

    void Start()
    {
        Addressables.LoadAssetAsync<Texture2D>("Assets/Textures/xiaolan.png").Completed += (obj) =>
          {
              // 图片
              var tex2D = obj.Result;
              img.texture = tex2D;
              img.GetComponent<RectTransform>().sizeDelta = new Vector2(tex2D.width, tex2D.height);
          };

        freeResBtn.onClick.AddListener(() =>
        {
            // 销毁RawImage
            if(null != img)
	            Destroy(img.gameObject);
        });
    }
}

我们通过Event Viewer可以看到,虽然我们销毁了RawImage,但是Texture2D还在内存中,
请添加图片描述
我们需要通过Addressables.Release去释放资源,如下,

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.UI;

public class Main : MonoBehaviour
{
    public RawImage img;
    public Button freeResBtn;
    private Texture2D tex2D;

    void Start()
    {
        Addressables.LoadAssetAsync<Texture2D>("Assets/Textures/xiaolan.png").Completed += (obj) =>
          {
              // 图片
              tex2D = obj.Result;
              img.texture = tex2D;
              img.GetComponent<RectTransform>().sizeDelta = new Vector2(tex2D.width, tex2D.height);
          };

        freeResBtn.onClick.AddListener(() =>
        {
            // 销毁RawImage
            if(null != img)
                Destroy(img.gameObject);
            // 释放资源
            if (null != tex2D)
                Addressables.Release(tex2D);

        });
    }
}

再次运行,可以看到,小兰的Texture2D在内存中释放了,
请添加图片描述
我们也可以使用Addressables.Release去释放handle,效果是一样的,例,

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.UI;

public class Main : MonoBehaviour
{
    public RawImage img;
    public Button freeResBtn;

    void Start()
    {
        var handle = Addressables.LoadAssetAsync<Texture2D>("Assets/Textures/xiaolan.png");
        handle.Completed += (obj) =>
        {
            // 图片
            Texture2D tex2D = obj.Result;
            img.texture = tex2D;
            img.GetComponent<RectTransform>().sizeDelta = new Vector2(tex2D.width, tex2D.height);
        };

        freeResBtn.onClick.AddListener(() =>
        {
            // 销毁RawImage
            if (null != img)
                Destroy(img.gameObject);
            // 释放资源
            Addressables.Release(handle);
        });
    }
}

同理,释放预设资源也一样,如下

using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.UI;

public class Main : MonoBehaviour
{
    public Button freeResBtn;
    private GameObject cubeObj;


    void Start()
    {
        var handle = Addressables.LoadAssetAsync<GameObject>("Assets/Prefabs/Cube.prefab");
        handle.Completed += (obj) =>
         {
            // 实例化Cube
            cubeObj = Instantiate(obj.Result);
         };

        freeResBtn.onClick.AddListener(() =>
        {
            if (null != cubeObj)
                Destroy(cubeObj);

            // 释放资源
            Addressables.Release(handle);
        });
    }
}

18、打包工具集成Addressable打包流程

实际项目中我们一般都会自己写一套打包工具,我们可以在打包工具中集成Addressable的打包流程,官方给出了示例,如下
详细参见官方文档:https://docs.unity.cn/cn/current/Manual/com.unity.addressables.html
在这里插入图片描述官方示例:

#if UNITY_EDITOR
    using UnityEditor;
    using UnityEditor.AddressableAssets.Build;
    using UnityEditor.AddressableAssets.Settings;
    using System;
    using UnityEngine;

    internal class BuildLauncher
    {
        public static string build_script 
            = "Assets/AddressableAssetsData/DataBuilders/BuildScriptPackedMode.asset";
        public static string settings_asset 
            = "Assets/AddressableAssetsData/AddressableAssetSettings.asset";
        public static string profile_name = "Default";
        private static AddressableAssetSettings settings;

        static void getSettingsObject(string settingsAsset) {
            // This step is optional, you can also use the default settings:
            //settings = AddressableAssetSettingsDefaultObject.Settings;

            settings
                = AssetDatabase.LoadAssetAtPath<ScriptableObject>(settingsAsset)
                    as AddressableAssetSettings;

            if (settings == null)
                Debug.LogError($"{settingsAsset} couldn't be found or isn't " +
                               $"a settings object.");
        }

        static void setProfile(string profile) {
            string profileId = settings.profileSettings.GetProfileId(profile);
            if (String.IsNullOrEmpty(profileId))
                Debug.LogWarning($"Couldn't find a profile named, {profile}, " +
                                 $"using current profile instead.");
            else
                settings.activeProfileId = profileId;
        }

        static void setBuilder(IDataBuilder builder) {
            int index = settings.DataBuilders.IndexOf((ScriptableObject)builder);

            if (index > 0)
                settings.ActivePlayerDataBuilderIndex = index;
            else
                Debug.LogWarning($"{builder} must be added to the " +
                                 $"DataBuilders list before it can be made " +
                                 $"active. Using last run builder instead.");
        }

        static bool buildAddressableContent() {
            AddressableAssetSettings
                .BuildPlayerContent(out AddressablesPlayerBuildResult result);
            bool success = string.IsNullOrEmpty(result.Error);

            if (!success) {
                Debug.LogError("Addressables build error encountered: " + result.Error);
            }
            return success;
        }

        [MenuItem("Window/Asset Management/Addressables/Build Addressables only")]
        public static bool BuildAddressables() {
            getSettingsObject(settings_asset);
            setProfile(profile_name);
            IDataBuilder builderScript
              = AssetDatabase.LoadAssetAtPath<ScriptableObject>(build_script) as IDataBuilder;

            if (builderScript == null) {
                Debug.LogError(build_script + " couldn't be found or isn't a build script.");
                return false;
            }

            setBuilder(builderScript);

            return buildAddressableContent();
        }

        [MenuItem("Window/Asset Management/Addressables/Build Addressables and Player")]
        public static void BuildAddressablesAndPlayer() {
            bool contentBuildSucceeded = BuildAddressables();

            if (contentBuildSucceeded) {
                var options = new BuildPlayerOptions();
                BuildPlayerOptions playerSettings
                    = BuildPlayerWindow.DefaultBuildMethods.GetBuildPlayerOptions(options);

                BuildPipeline.BuildPlayer(playerSettings);
            }
        }
    }
#endif

四、答疑补充(Q&A)

评论区有一些同学的提问,我补充到这里统一进行答疑。

1、Addressables加载场景进度监听

在这里插入图片描述
示例:

using System.Collections;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class SceneLoader : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(LoadScene());
    }

    IEnumerator LoadScene()
    {
        // 异步加载场景(如果场景资源没有下载,会自动下载),
        var handle = Addressables.LoadSceneAsync("Assets/Scenes/GameScene.unity");
        if (handle.Status == AsyncOperationStatus.Failed)
        {
            Debug.LogError("场景加载异常: " + handle.OperationException.ToString());
            yield break;
        }
        while (!handle.IsDone)
        {
            // 进度(0~1)
            float percentage = handle.PercentComplete;
            Debug.Log("进度: " + percentage);
            yield return null;
        }

        Debug.Log("场景加载完毕");
    }
}

五、完毕

好了,就写到这里吧。
我是新发,https://blog.csdn.net/linxinfa
一个在小公司默默奋斗的Unity开发者,希望可以帮助更多想学Unity的人,共勉~

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【游戏开发探究】Unity Addressables资源管理方式用起来太爽了,资源打包、加载、热更变得如此轻松(Addressable Asset System | 简称AA) 的相关文章

  • 新的开始( [USACO08OCT]打井Watering Hole)

    新的开始 newstart pas c cpp 题目描述 话说小 FF 在经历了上次 寻找古代王族遗产 的探险后 xff0c 成为了世界上最伟大的探险 家并拥有了一大笔财富 当然他不能坐吃山空 xff0c 必须创造财富 xff01 xff0
  • UOS配置本地APT源和外部软件包

    root 64 skill PC mount dev sr0 mnt mount mnt WARNING device write protected mounted read only root 64 skill PC vi etc ap
  • [二分答案] 洛谷P1873 砍树

    目录 题意样例样例输入 xff1a 样例输出 思路总结代码 题意 伐木工人米尔科需要砍倒M米长的木材 这是一个对米尔科来说很容易的工作 xff0c 因为他有一个漂亮的新伐木机 xff0c 可以像野火一样砍倒森林 不过 xff0c 米尔科只被
  • [区间DP]洛谷P1063 能量项链

    目录 题意样例样例输入 xff1a 样例输出 思路总结代码 题意 样例 样例输入 xff1a 4 2 3 5 10 样例输出 710 思路 1 经典区间DP题 算是合并石子的变种 只不过由一个点变成了一个区间 不过我们也可以用结构体存储 当
  • Linux命令行初接触-1 操作文件和目录

    操作文件 amp 目录 1 通配符含义常用通配符常用字符类类型匹配范例 2 mkdir 创建目录3 cp 复制文件和目录工作方式常用选项 4 mv 移动和重命名文件工作方式常用选项 5 rm 删除文件和目录工作方式常用选项注意事项 6 ln
  • 机器学习入入入入门(1)机器学习基本概念、引出深度学习

    机器学习入入入入门 xff08 1 xff09 0 前言1 基本步骤2 基本概念2 1 Hyperparameters2 2 local minima 3 linear model3 1 基础概念 4 piecewise linear cu
  • 深度学习蒟蒻入门——从0安装pytorch(CPU版)

    从0安装pytorch 1 检查自己的电脑有没有GPU2 安装CPU版的pytorch3 测试pytorch 1 检查自己的电脑有没有GPU 首先打开任务管理器 xff0c 选择性能栏 然后滑到最下 xff0c 看是否有GPU一项 xff0
  • 系统学习iOS动画 —— Stroke和路径动画

    这是要完成的动画 xff1a 先添加需要的代码 xff0c 这里需要将storyboard的ViewController换成TableViewController xff0c 将Under Top Bars 和 Under Bottom B
  • 不知道这些网站还做什么程序员呀!

    今天我就来总结一些程序员必备的网站 xff0c 囊括开源项目 解决bug 技术分享 一线资源和自我提升的网站 xff0c 希望能对广大程序猿有所帮助 xff0c 赶紧给我收藏起来 xff0c 下次刷不到了可别说我没提醒你 我们首先来看一下国
  • (音视频开发)WebRTC进阶流媒体服务器开发-多人互动架构

    一 xff1a 多人互动架构方案 xff08 一 xff09 WebRTC回顾 xff0c 两层含义 xff1a 1 WebRTC是google开源的流媒体客户端 xff0c 可以进行实时通讯 xff0c 主要应用于浏览器之间进行实时通讯
  • 10种linux下磁盘快照方式恢复系统

    导读大家都知道windows系统有一个磁盘快照的功能 xff0c 在windows2003中系统恢复开始依赖于一个叫做硬盘快照服务 Volume Snapshot Service 的服务 xff0c 他能够自动创建系统快照 包括正在使用的文
  • ubuntu安装go开发环境

    一 为ubuntu20 04更新源 给root用户设置密码 xff1a 命令 xff1a sudo passwd root 备份原来的源 xff0c 命令 xff1a sudo cp etc apt sources list etc apt
  • 如何修复Ubuntu中包缓存文件被毁问题

    导读今天 xff0c 我尝试更新我的 Ubuntu 18 04 LTS 的仓库列表 xff0c 但收到了一条错误消息 xff1a E The package cache file is corrupted it has the wrong
  • 1002 A+B for Polynomials (25分) 详解+易错点

    注意点 xff1a 系数为0 xff0c 则不输出 xff0c 例 xff1a 其中 1和1相加为0 xff0c 则在输出时避免这一项 xff0c 而且要注意结果的K值 xff0c 不要包括这一项 xff0c 思路 xff0c 利用结构体存
  • Linux远程桌面的选择

    Linux的远程桌面主要分两个部分 xff1a Linux客户机连Linux服务器和Windows客户机连Linux服务器 xff0c 还有现在用平板电脑连远程桌面 Linux客户机连Windows服务器比较简单没啥可说的 xff0c rd
  • Kali Linux mdk3WiFi洪水攻击 攻击路由器 生成虚假WiFi WiFi身份验证攻击可使连接WiFi的手机掉线重连抓包

    将无线网卡转换为监听模式 airmon ng start wlan0 查找附近无线网络 airodump ng wlan0mon Authentication DoS xff1a xff08 洪水攻击 xff0c 又叫做身份验证攻击 xff
  • 大一java程序设计的某次作业题解

    题目描述 xff1a 设计程序实现输入日期及机票张数 xff0c 计算出应付金额 假设北京至上海的机票全价为 1200 元 张 xff0c 以 2017 年为例进行程序编写 xff0c 所有的法定假日 xff0c 机票无折扣 xff1b 除
  • D. Make It Round(1759D)

    要求n k后缀0数量最多 xff08 k lt 61 m xff09 xff0c 且n k尽可能大 比赛时思路 xff08 错误 xff09 xff1a 10是由2和5组成 xff0c 故先统计n的因子包含2的个数num2 包含5的个数nu
  • Codeforces Round #841 (Div. 2)

    B Kill Demodogs 分析 显然要选择和两斜线的元素相加 所以答案可以表示为 xff1a 即 xff1a 根据公式 得答案为 但答案不能直接得这个 因为n的范围为1e9 xff0c 而ull的范围为1e20 xff0c 答案的第一
  • Educational Codeforces Round 141 Editorial C~D

    1783C Yet Another Tournament 分析 正解思路是贪心 开始自己也想的贪心 xff1a 首先显然打败的人数越多越好 xff0c 然后选择权值最小的人打败 这个思路前半部分没问题 xff0c 后半部分过不了样例的第二个

随机推荐