unity Addressable的基本使用
Addrsssable是什么
addressable 是unity提供的一套基于assetbundle的一项新的热更技术,它比assetbundle更简洁更好用,通过配置的方式减少了许多客户端热更包处理逻辑。
assetbundle的使用流程如下:
- 1.先将热更资源在一个工程上进行打包将我们需要的热更资源导出为各个平台的热更包,(大多数小伙伴可能喜欢用assetbundlebrowser简单方便,也有小伙伴喜欢自己写脚本配置更灵活自由)
- 2.将打包后的各个平台包放到服务器上
- 3.客户端工程处理 assetbundle的下载或者更新热更包逻辑
- 4.客户端工程处理 assetbundle的加载逻辑
- 5.客户端工程处理 assetbundle的销毁逻辑最终达到释放内存的处理
相信大多数Unity开发的小伙伴对于上面这一过程都不会陌生,如果能熟练使用assetbundle使用assetbundle作为热更资源首选项肯定是没问题的, 那么addressable的优势体现在哪里换句话说我们为什么要选择addressable。下面我就介绍下addressable的使用流程。
addressable的使用流程
- 1.配置打包工程指定远端路径以及打包信息
- 2.打包(或热更)各个平台包
- 3.将打包后的输出内容放到服务器上
- 4.配置客户端工程相关选项
- 5.通过api加载资源
通过图表展示addressable流程如下
#mermaid-svg-ODXKnChI0P5jJ2w2 {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-ODXKnChI0P5jJ2w2 .error-icon{fill:#552222;}#mermaid-svg-ODXKnChI0P5jJ2w2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ODXKnChI0P5jJ2w2 .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-ODXKnChI0P5jJ2w2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ODXKnChI0P5jJ2w2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ODXKnChI0P5jJ2w2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ODXKnChI0P5jJ2w2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ODXKnChI0P5jJ2w2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ODXKnChI0P5jJ2w2 .marker.cross{stroke:#333333;}#mermaid-svg-ODXKnChI0P5jJ2w2 svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ODXKnChI0P5jJ2w2 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ODXKnChI0P5jJ2w2 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ODXKnChI0P5jJ2w2 .actor-line{stroke:grey;}#mermaid-svg-ODXKnChI0P5jJ2w2 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ODXKnChI0P5jJ2w2 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ODXKnChI0P5jJ2w2 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ODXKnChI0P5jJ2w2 .sequenceNumber{fill:white;}#mermaid-svg-ODXKnChI0P5jJ2w2 #sequencenumber{fill:#333;}#mermaid-svg-ODXKnChI0P5jJ2w2 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ODXKnChI0P5jJ2w2 .messageText{fill:#333;stroke:#333;}#mermaid-svg-ODXKnChI0P5jJ2w2 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ODXKnChI0P5jJ2w2 .labelText,#mermaid-svg-ODXKnChI0P5jJ2w2 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ODXKnChI0P5jJ2w2 .loopText,#mermaid-svg-ODXKnChI0P5jJ2w2 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ODXKnChI0P5jJ2w2 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ODXKnChI0P5jJ2w2 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ODXKnChI0P5jJ2w2 .noteText,#mermaid-svg-ODXKnChI0P5jJ2w2 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ODXKnChI0P5jJ2w2 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ODXKnChI0P5jJ2w2 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ODXKnChI0P5jJ2w2 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ODXKnChI0P5jJ2w2 .actorPopupMenu{position:absolute;}#mermaid-svg-ODXKnChI0P5jJ2w2 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ODXKnChI0P5jJ2w2 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ODXKnChI0P5jJ2w2 .actor-man circle,#mermaid-svg-ODXKnChI0P5jJ2w2 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ODXKnChI0P5jJ2w2 :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} 打包工程 服务器 真正客户端 将打好的热更包放到服务器 自动下载热更包并初始化 通过名称或者其他方式加载对应资源 打包工程 服务器 真正客户端
由上面对比可知addressable跟assetbundle相比有以下优点:
- 1.不需要我们自己手动下载热更包(如果我们自己写下载assetbundle可能要有许多校验判断)
- 2.加载数据更方便(使用assetbundle我们需要判断assetbundle是否初始化,初始化后才能加载资源此外多次初始化assetbundle还会抛出异常导致无法加载)
下面我们来介绍下整个流程的配置(package的安装就不介绍了unity registry里面有)
step1 打包工程配置
1.配置远端路径
通过window -> AssetManager ->Addressable -> Profiles 打开 Addressable Profiles 窗口,窗口一般如下:
这里我们重点关注的内容是Remote,这里面我们要将RemoteLoadPath设置为我们远端加载的资源路径配置到这里去 (如果服务端暂时没有的话可以考虑使用gitee将热更文件放到gitee上测试)
-
通过window -> AssetManager ->Addressable -> Groups 打开 Addressable Groups 窗口
Groups窗口如下:
在这里我们创建一个分组点击分组右键可以设置这个分组为默认分组,在空白区域点击右键可以创建一个分组,对于分组内容的设置选中分组后在inspector窗口可以看到分组详细设置信息,截图如下:
这块我们需要关注的一点是 build &Load Paths我们选择remote -
将我们需要热更的资源拖到需要的分组里面去,也可以在资源的Inspector窗口上的addressable复选框选中然后选择对应的分组即可,如果你觉得这些资源我需要配合一些标签(比如高分辨率用标签high 中分辨率使用middle)来加载的话可以自定义一些标签给对应资源后面会介绍怎么使用这些标签。
-
有了基本设置有了热更资源接下来就是构建打包的过程了 ,首选在Groups窗口的Play Mode Script窗口选择Use Existing Build代表我要打远端工程,上面还有俩选项感兴趣的话可以自己研究下,其次在窗口上选择Build -> new build ->defaultbuild Script 代表我要打个新的addressable集合,有一点要注意第一次build成功后后面再构建就千万别选择new build了一定要选择Update a Previous Build否则会出现重新生成catlog以及对应的不需要修改的资源等客户端下载了catlog后之前没变的资源还要重新下载一次
-
等编译结束后就是需要我们找到资源上传了,编译后的文件内容如下:
这个文件夹最最重要的两个文件catalog_0.1.hash以及catalog_0.1.json,catalog_0.1.json是对于资源的描述这里面记载了addressable如何找到对应资源的路径没有这个文件addressable将无法加载热更资源,catalog_0.1.hash是catalog_0.1.json的hash文件这个文件比较小它的作用后面会介绍到,有了这些东西我们就将这个Android文件夹里面的东西放到资源服务器上去即可。
step2 客户端工程配置
在介绍客户端工程如何配置前我先说下addressable的两种加载模式,
模式1 将资源加载更新权限交给addressable,这种模式下当addressable需要加载资源或者调用Addressables.DownloadDependenciesAsync()这个方法的时候addressable会首先获取资源配置的路径找到catalog_0.1.hash这个文件跟本地文件对比如果本地文件没有或者本地文件内容跟后台不匹配就会下载catalog_0.1.json 从而在后面加载资源的时候更新本地配置找到服务器上各个bundle的资源路径。
模式2:禁用addressable的自动更新因为频繁访问catalog_0.1.hash这个文件虽然文件内容很小但是会给服务端带来一定压力。
综合上面所述我们这边选择了模式2去加载资源当然模式1也是没问题的还是官方推荐的,addressable的目的就是减少咱们客户端的开发工作量,只不过服务端可能需要把资源配置到cdn或者其他路径下以便能快速访问。
下面我们就介绍如何使用手动加载的模式加载服务端资源:
- 首先禁用本地addressable的自动更新
在AddressableAssetSettings这个窗口中禁用build选项,因为我们是拉取远端数据的一端因此不需要build内容,然后在本地设置中选择本地路径因为我们要手动更新。
2.设置完这些后就是我们的代码设置阶段了,首先写一下加载远端catlog的代码。
public void CheckDownload(){ if (isAddressLoaded) return; remoteHash = 接口或资源包获取远端最新Hash值; string remoteCatLog = 接口或资源包获取远端最新catlog地址; string localHash = PlayerPrefsManager.GetString(\"addressable_hash\"); if (string.IsNullOrEmpty(remoteHash) || string.IsNullOrEmpty(remoteCatLog)) return; string localCatalogPath = Application.persistentDataPath+\"/cached_catalog.json\"; if (localHash == remoteHash && File.Exists(localCatalogPath)) {// 本地版本一致,且本地有缓存,直接加载本地 catalog Addressables.LoadContentCatalogAsync(\"file://\" + localCatalogPath).Completed += OnCatalogLoaded; } else {// 下载远端 catalog 并缓存到本地 DownloadAndCacheCatalogThenLoadAsync(remoteCatLog, localCatalogPath); }}private async void DownloadAndCacheCatalogThenLoadAsync(string remoteUrl, string localPath){ try { UnityWebRequest request = UnityWebRequest.Get(remoteUrl); var asyncOp = request.SendWebRequest(); while (!asyncOp.isDone) { await UniTask.Yield(); // 等一帧 } if (request.result != UnityWebRequest.Result.Success) {//下载失败 return; } File.WriteAllBytes(localPath, request.downloadHandler.data); Addressables.LoadContentCatalogAsync(\"file://\" + localPath).Completed += OnCatalogLoaded; } catch (Exception ex) { Debug.LogError($\"DownloadAndCacheCatalogThenLoadAsync 异常: {ex}\"); }}void OnCatalogLoaded(AsyncOperationHandle<IResourceLocator> handle){ if (handle.Status == AsyncOperationStatus.Succeeded) { isAddressLoaded = true; PlayerPrefsManager.SetString(\"addressable_hash\", remoteHash);//加载成功 } else { isAddressLoaded = false;//加载失败 }}
等上面代码加载完后如果配置没问题的话我们的catlog就能初始化成功了最最核心的代码是Addressables.LoadContentCatalogAsync
3.配置也有了初始化addressable的逻辑也有了,剩下的就差一件事情了addressable的加载处理。
下面是一个简单地addressable加载物体的方法:
Addressables.LoadAssetAsync<GameObject>(prefabAddress).Completed += (AsyncOperationHandle<GameObject> handle) => { if (handle.Status == AsyncOperationStatus.Succeeded) { // 实例化Prefab Instantiate(handle.Result, transform.position, Quaternion.identity); Debug.Log($\"Prefab \'{prefabAddress}\' loaded successfully!\"); } else { Debug.LogError($\"Failed to load prefab: {prefabAddress}\"); } };
只需要传入打包工程对应的资源名称即可,是不是使用起来很简单。但是这样写会有一个大问题,如果这样写了我们只关注到了加载这块,如果不找个恰当的时机(物体销毁后)卸载掉这块资源,即使物体销毁掉这块内存也不会释放所以这块代码我们要修改下:
using UnityEngine;using UnityEngine.AddressableAssets;using UnityEngine.ResourceManagement.AsyncOperations;public class SimpleAddressableLoader : MonoBehaviour{ [Header(\"加载设置\")] [SerializeField] private string prefabAddress = \"MyPrefab\"; // 你的Prefab地址 [SerializeField] private KeyCode loadKey = KeyCode.L; // 加载快捷键 [SerializeField] private KeyCode destroyKey = KeyCode.D; // 销毁快捷键 private GameObject loadedInstance; private AsyncOperationHandle<GameObject> loadHandle; void Update() { // 快捷键控制 if (Input.GetKeyDown(loadKey)) { LoadPrefab(); } if (Input.GetKeyDown(destroyKey)) { DestroyPrefab(); } } void OnDestroy() { // 确保组件销毁时释放资源 DestroyPrefab(); } public void LoadPrefab() { // 如果已加载则先销毁 if (loadedInstance != null) { DestroyPrefab(); } Debug.Log($\"Loading prefab: {prefabAddress}\"); // 异步加载Prefab loadHandle = Addressables.LoadAssetAsync<GameObject>(prefabAddress); loadHandle.Completed += OnPrefabLoaded; } private void OnPrefabLoaded(AsyncOperationHandle<GameObject> handle) { if (handle.Status == AsyncOperationStatus.Succeeded) { // 实例化Prefab loadedInstance = Instantiate(handle.Result, transform.position, Quaternion.identity); Debug.Log($\"Prefab \'{prefabAddress}\' loaded successfully!\"); // 添加旋转效果以便观察 loadedInstance.AddComponent<SimpleRotator>(); } else { Debug.LogError($\"Failed to load prefab: {prefabAddress}. Error: {handle.OperationException}\"); } } public void DestroyPrefab() { // 销毁实例化的游戏对象 if (loadedInstance != null) { Destroy(loadedInstance); loadedInstance = null; Debug.Log(\"Prefab instance destroyed.\"); } // 释放Addressables资源 if (loadHandle.IsValid()) { Addressables.Release(loadHandle); Debug.Log(\"Addressables resource released.\"); } }}// 简单的旋转组件public class SimpleRotator : MonoBehaviour{ public float rotationSpeed = 30f; void Update() { transform.Rotate(0, rotationSpeed * Time.deltaTime, 0); }}
补充:
还有一块忘了介绍,这里我们假如加载需要名字+tag的方式上面代码可以做如下修改:
using UnityEngine;using UnityEngine.AddressableAssets;using UnityEngine.ResourceManagement.AsyncOperations;using System.Collections.Generic;using UnityEngine.UI;public class AddressableLoaderByTag : MonoBehaviour{ [Header(\"加载设置\")] [SerializeField] private string assetName = \"MyPrefab\"; // 资源名称 [SerializeField] private string assetTag = \"Environment\"; // 资源标签 [SerializeField] private Transform spawnPoint; // 生成位置 [Header(\"UI元素\")] [SerializeField] private Text statusText; [SerializeField] private Button loadButton; [SerializeField] private Button destroyButton; private List<GameObject> loadedInstances = new List<GameObject>(); private AsyncOperationHandle<IList<GameObject>> loadHandle; void Start() { // 初始化UI loadButton.onClick.AddListener(LoadAssets); destroyButton.onClick.AddListener(DestroyAllAssets); UpdateStatus(\"准备加载资源\"); } void OnDestroy() { // 确保组件销毁时释放资源 DestroyAllAssets(); } public void LoadAssets() { // 清理之前的资源 DestroyAllAssets(); UpdateStatus($\"正在加载: {assetName} (标签: {assetTag})...\"); // 创建键列表(名称 + 标签) var keys = new List<object> { assetName, assetTag }; // 使用交集模式加载资源 loadHandle = Addressables.LoadAssetsAsync<GameObject>( keys, callback: null, mode: Addressables.MergeMode.Intersection ); loadHandle.Completed += OnAssetsLoaded; } private void OnAssetsLoaded(AsyncOperationHandle<IList<GameObject>> handle) { if (handle.Status == AsyncOperationStatus.Succeeded) { int count = handle.Result.Count; if (count == 0) { UpdateStatus($\"警告: 找到0个匹配 {assetName} 和标签 {assetTag} 的资源\"); return; } UpdateStatus($\"成功加载 {count} 个资源\"); // 实例化所有匹配的资源 foreach (var asset in handle.Result) { GameObject instance = Instantiate(asset, spawnPoint.position, Quaternion.identity); loadedInstances.Add(instance); // 添加随机位置偏移 Vector3 offset = new Vector3( Random.Range(-1f, 1f), 0, Random.Range(-1f, 1f) ); instance.transform.position += offset; // 添加旋转效果以便观察 var rotator = instance.AddComponent<SimpleRotator>(); rotator.rotationSpeed = Random.Range(20f, 60f); } } else { UpdateStatus($\"错误: 加载失败 - {handle.OperationException}\"); } } public void DestroyAllAssets() { // 销毁所有实例化的游戏对象 foreach (var instance in loadedInstances) { if (instance != null) { Destroy(instance); } } loadedInstances.Clear(); // 释放Addressables资源 if (loadHandle.IsValid()) { Addressables.Release(loadHandle); } UpdateStatus(\"已销毁所有资源实例\"); } private void UpdateStatus(string message) { if (statusText != null) { statusText.text = message; } Debug.Log(message); }}// 简单的旋转组件public class SimpleRotator : MonoBehaviour{ public float rotationSpeed = 30f; void Update() { transform.Rotate(0, rotationSpeed * Time.deltaTime, 0); }}
核心处理是我们通过标签+名字的组合获取到了我们需要的数据Addressables.MergeMode.Intersection代表取keys获取所有内容的交集,还有Union是取所有数据的并集。
Addressables.LoadAssetsAsync<GameObject>( keys, callback: null, mode: Addressables.MergeMode.Intersection );
上面的内容基本上介绍了addressable的加载过程,使用addressable一定要注意一点,addressable的加载与销毁一定要成对出现,否则会出现内存不能销毁问题,当然addressable还存在另外一个东西我们需要注意,那就是addressable的引用计数机制,假如我们同一个物体初始化加载两次,但是销毁一次,addressable是不能销毁掉的这块如何优化以及进一步详细使用推荐大家看下唐老师的课程B站链接如下:唐老师addressable课程。
如果小伙伴们有其他想问的,欢迎发表评论。