文章目录
- 一、前言
- 二、为什么推荐使用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资源
- 10、Addressable资源三个加载模式
- 11、加载远程Addressable资源
- 12、如何把Group里的资源打成多个bundle文件
- 13、使用Labels对Group内的资源进行二级分组
- 14、批量加载同一个Label的所有资源(AssetLabelReference)
- 15、打资源热更包
- 16、提前检测更新并下载(预下载)
- 17、Addressable资源释放
- 18、打包工具集成Addressable打包流程
- 四、答疑补充(Q&A)
- 五、完毕
一、前言
嗨,大家好,我是新发。
之前就有看过Unity
的Addressable 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
默认的文件系统文件夹名字相同,比如Resources
、StreamingAssets
等,除非你真的是想使用这些特定目录的功能,
接着我们把预设文件直接拖到对应的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
如果你想修改RemoteBuildPath
和RemoteLoadPath
,可以在Addressables Groups
窗口中点击Manage Profiles
菜单,
也可以点Window / Asset Management /Addressables / Profiles
菜单,
此时会打开Addressables Profiles
窗口,我们可以修改修改RemoteBuildPath
和RemoteLoadPath
,
这里我先不修改,下文搭建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
现在,我们打个Android
的APK
包看看,
正常打出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 Name
为Assets/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;
};
有些人可能不喜欢使用回调的方式,喜欢使用async
、await
的方式,示例:
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
并实例化了,
我们顺便测试一下把Cube
的Addressable Name
改为HelloCube
,
加载的代码也改下,
运行测试,可以看到,依然可以正常加载Cube
,
这个时候,应该有同学会疑惑了,我改了Addressable Name
,但我都没有重新Build
资源包,怎么可以正常加载到资源呢?其实Addressables
系统为了方便我们在Editor
环境下方便测试,默认都是直接从Asset Database
加载的,我们可以在Addressables Groups
窗口的Play Mode Script
中进行切换,建议编辑器环境下使用Use Asset Database (fastest)
即可,下面两个选项下文我会讲具体使用,这里先维持原样~
9.2、方式二:通过AssetReference来加载资源
我们知道,脚本中如果声明了一个public
变量,默认会进行序列化,可以在Inspector
窗口中对它进行设置,我们声明一个public
的AssetReference
成员,如下,
// 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
加载资源,我们 不需要 Build
打Addressable
资源包,这个加载速度最快,建议在项目开发阶段使用这个模式,加载速度快。
10.2、Simulate Groups (advanced)
这个模式下,也是 不需要 Build
打Addressable
资源包的,那它与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 Name
为Assets/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
的地址,如下
然后我执行Addressables
的Build
打出资源包,如下是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
目录下的文件都会塞入APK
的assets
目录中,
我们打开它,可以看到我们配置的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
内的资源进行二级分组,默认只有一个default
的Label
,
13.1、新建Label
我们可以点击Tools / Labels
菜单,新建一个Label
,
点击+
号,
新建一个texture
吧,
13.2、给资源设置Label
接着我们把柯南和小兰的Label
都设置为texture
,
如下
为了演示,我再加一个柯南主题曲,并标记它的Label
为audio
,
13.3、修改Bundle Mode为Pack Together By Label
接着,我们把RemoteGroup
的Bundle Mode
改为Pack Together By Label
,
这样,一个Group
就会以Label
为颗粒细分成多个.bundle
,组织是更加灵活,我们执行Addressable
资源打包,如下,
13.3、一个资源标记多个Label
事实上,一个资源可以同时标记多个Label
,比如我把柯南同时标记为default
和texture
,如下,
我们执行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
这个工作流。
这个过程中可能有强退、断网等异常,我们可以判断AsyncOperationHandle
的Status
状态码然后进行处理,并提示重试。
示例:
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的人,共勉~