前言
之前已经提到过一部分预览版的内容,由于正式版大部分的功能都已经稳定下来,还没看过的同学可以直接看这篇就好了。
最近有点忙,又有点懒,正式版出来后还是拖更了很久抱歉抱歉。
简介
在开始之前,我们也还是先做一个Addressable Assets System的简单介绍:为什么我们需要使用这样一套新的这套系统来进行资源的管理?
我们先来整理一下目前在Unity中使用到资源加载、实例化的几种方式:
直接引用
直接引用是最简单快捷,也是最难以控制的一种方法。
在一定规模的项目中,直接引用往往就是莫名奇怪出现的bug的源泉。
using UnityEngine;
public class LoadAssetScript : MonoBehaviour
{
[SerializeField]
private GameObject _referenceDirectly;
private void Start()
{
Instantiate(_referenceDirectly);
}
}
Resources 目录加载
Resources目录加载也是比较常用方便的方法,通常结合上配置表能比较准确快速的进行配置。
弊端也是相当明显的:
1、首先Resources目录下的所有文件都会随包打包,造成包体过大。
2、其次Resources目录没办法热更新资源,只能重新打包游戏。
3、再者命名对资源加载影响重大,命名的变更需要更加谨慎。
using UnityEngine;
public class LoadAssetScript : MonoBehaviour
{
[SerializeField]
private string _resourcesPath;
private void Start()
{
// 加载资源
var prefab = Resources.Load(_resourcesPath);
// 实例化资源
Instantiate(prefab);
}
}
AssetBundle资源管理
如果你的项目需要热更新资源,AssetBundle几乎是唯一选择,但AssetBundle的使用与管理会伴随着项目、资源复杂度的提升。
首先你会需要用到打包脚本,将资源进行相对应的打包,期间要处理资源的依赖和复用的问题。
之后你需要更改资源加载的方式,需要通过加载AssetBundle然后再读取其中的对应资源进行加载。这个时候根据应用场景,你需要避免AB过大而过度加载占用内存,同时也要防止AB颗粒过小而导致AB数量剧增而增加大幅管理难度。
配合上前面说的资源依赖引用情况,这样打包的时候需要考虑的事情更多了,
虽然你能够使用像是AssetBundleManager/AssetBundleGraph一类的工具来辅助开发,或是制作、使用社区上的一些AssetBundle打包框架来制作资源包。
但这些都不应该从一开始就需要考虑进项目中,减缓开发的速度。
The Addressable Assets System
现在Unity推出了一个更好的解决方案,从早期开发的快速迭代,亦或是中后期项目的资源管理方式快速迁移都能全面覆盖。
而且Unity一直被人诟病的AssetBundle系统,在Addressables的帮助下走入后台,安心为未来的资源管理流水线服务,终于不再需要开发者写痛苦的打包脚手架或者框架了。
The Addressable Assets System有以下几个特点:
- 使用Addressable在开发前期就进入快速开发的阶段,使用任何你喜欢的资源管理技术,你都能快速的切换来Addressable系统中,几乎不需要修改代码。
- 依赖管理:Addressable系统不仅仅会帮你管理、加载你指定的内容,同时它会自动管理并加载好该内容的全部依赖。在所有的依赖加载完成,你的内容彻底可用时,它才会告诉你加载完成。
- 内存管理:Addressable不仅仅能记载资源,同时也能卸载资源。系统自动启用引用计数,并且有一个完善的Profiler帮助你指出潜在的内存问题。
- 内容打包:Addressable系统自动管理了所有复杂的依赖连接,所以即使资源移动了或是重新命名了,系统依然能够高效地找到准确的依赖进行打包。当你需要将打包的资源从本地移到服务器上面,Addressable系统也能轻松做到,几乎不需要任何代价。
快速开始
说了这么多好处,我们怎么样使用这个系统呢?
由于Addressables本身高度自动化的管理方式,使用起来已经非常简单了,但是第一步当然也得先安装起来。
安装
我们需要Unity 2018.2或者以上的版本,在新增的Package Manager(真的是非常方便的包体依赖管理工具,新版本中还加入了了AssetStore和Github Repo的联动)中能找到Addressables包体,点击Install安装即可。
配置
点击Window > Assets Management > Addressable Assets进入配置界面,创建新的Addressable 设置。
到这一步为止,基础的配置就已经完成了
准备
我们知道要使某个资源能被定位(Addressable),得先给这个资源加上一个地址(Addresss),而资源本身存储什么地方,我们暂时先不用关心。
首先我们准备一下需要加载的资源,这边创建一个Cube的Prefab以及Material,同时准备两张不同的贴图并将其中一张(这里Box)设置给Cube的Material。
然后我们需要把这个Cube设置为Addressable,并且赋予其一个Address。
这里我们有两种方式实现:
- 选择Cube的Prefab,在Inspector上勾选Addressable,并且设置其地址。
- 拖动Cube的Prefab在Addressable窗口的特定组内。
在Addressable窗口内能观察获得以下结果
为了更方便理解和使用,我们简单先了解下这个窗口中包含的内容:
菜单:菜单中包含了打包、调试的一些设置,我们会在后面展开延伸。
分组:开发者可以将资源放入不同的分组中,并且针对不同的分组制定打包以及更新策略,具体的内容我们后面再说。
资源:在每个分组组内的就是Addressable的资源了,资源可以是嵌套资源(图集、动作剪辑,文件夹),也可以是普通的资源(贴图、材质、模型、Prefab)。每个资源由以下几部分组成:
Address:用于加载资源的地址标记,非唯一。
Path:资源在当前工程中的位置。
Labels:用于标记资源的特殊性标签,可多选。
这边为了方便加载,我们将Cube的Address(Assets/Dev/Cube.prefab)简化成为"Cube",就完成另外我们的准备工作。
使用
接下来开始尝试加载我们的资源,我们会使用几种方式进行加载:
1、资源引用
新建一个C#文件AddressableExample,输入以下代码:
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public class AddressableExample : MonoBehaviour
{
[SerializeField]
private AssetReference _asset;
private GameObject _instance;
private async void OnEnable()
{
// 加载会触发内部引用计数(ref count)增加1次,手动实例化不会变更引用计数,必须手动计数
// Addressable没办法持有手动实例化后的物件的引用,Unload也不会摧毁对应实例
#region Load by addressables and handle instantiate manually
// AsyncOperationHandle 完成时的回调
// _asset.LoadAssetAsync<GameObject>().Completed += OnLoadedCompleted;
// 使用await/async 的加载方法
// 开始加载,使用await 等待异步操作完成
// var obj = await _asset.LoadAssetAsync<GameObject>().Task;
// _instance = Instantiate(obj);
#endregion
// 使用Addressable直接实例化会增加引用计数,同时内部保存实例
// 当使用ReleaseAsset或ReleaseInstance时,引用计数归零会自动Unload Bundle
// 在不需要手动管理的情况下,推荐使用Addressable系统实例化
#region Load and instantiate by addressables
//_asset.InstantiateAsync().Completed += OnInstantiatedCompleted;
// 使用await/async 的加载方法
_instance = await _asset.InstantiateAsync().Task;
Debug.Log("Instantiated finished");
#endregion
}
private void OnInstantiatedCompleted(AsyncOperationHandle<GameObject> obj)
{
Debug.Log($"Instantiate {obj.Result.name} completed.");
_instance = obj.Result;
}
private void OnLoadedCompleted(AsyncOperationHandle<GameObject> obj)
{
Debug.Log($"Load {obj.Result.name} from async operation.");
_instance = Instantiate(obj.Result);
}
private void OnDisable()
{
// 使用Addressables实例化的实例不能手动删除,否则会破坏ref count的正确性
// Ref Count到达0后会Unload bundle,这时如果依然引用到bundle内的资源的话,他们之间就会失去live-link,出现资源引用错误
// 释放资源异步句柄
// _asset.ReleaseAsset();
// 释放实例,减少ref count,如果是Addressable来Instantiate的实例也会被删除
// 切换场景时,场景上保有的物件如果是addressable生成的话,也会调用release instance
// 如果此时ref count达到0也会触发bundle的unload(自动化管理)
_asset.ReleaseInstance(_instance);
}
}
正式版中直接添加了await的支持,大家可以根据自己的喜好来选择回调还是await的加载方式。
在Inspector中,选择刚才制作的Addressable 资源Cube。
点击运行即可预览直接引用加载、实例化的效果。
2、 根据Address以及Label进行加载
首先在Addressables窗口中新建一个分组,rename成Textures。
将准备的两张贴图做成Addressable资源,放到这个分组下,并且两者都重命名为Cube Texture。
选择项目目录下的Addressables设置文件AddressableAssetsData/AddressableAssetSettings进行一些基本配置。
这边就不一一解释了,挑几个重点讲一下,之后如果有机会的话在详细扩展开。
Send Profiler Events:重点,如果需要进行资源的调试的话,这个一定得打开。
Groups:这边能检视到再Addressables窗口中创建的分组。
Profiles:这边可以理解成一个配置的存储档案,在这个入口的配置有时候会配置好几套(调试用、测试服、正式服等),就可以通过新建、切换不同的Proifle来快速切换几套配置。一般来说,默认的几个入口配置也能覆盖大部分要求,只需要改后面的地址就行了。
Labels:这个就是定义好加载、定位所用到的标签。同一个资源可以有多个标签。
Data Builders:在Addressable窗口菜单中能够选择的在编辑器下的数据构建模式,一般来说附带的这几个已经可以满足大部分要求,你也可以新建适合自己项目的模式。
Fast Mode:加载资源不通过资源包,直接使用AssetDatabase加载。
Virtual Mode:会形成AssetBundle布局,但是不需要打包,加载资源通过ResourcesManager加载,并且可以在RM Profiler中查看包体布局。
Packed Mode:需要额外步骤打包AssetBundle,运行时资源也是在AssetBundle中进行加载。
Assets Group Templates:你可以在这里设定一些自己的常用的模板,新建分组的时候能够快速设置。
Initialization Objects:需要继承了IObjectInitializationDataProvider的ScriptableObject,能够进行一些运行时初始化。
我们需要设置的就是Labels这个选项,我们会添加Skin1、Skins2
这两个 标签。
那么在Addressable的窗口中也能够选择新的标签了,我们将这两个标签分别赋值给两个Cube Texture贴图。
在AddressableExample添加新的方法:
private int _skinIndex = 1;
[ContextMenu("Change Texture")]
public async void ChangeTexture()
{
_skinIndex = _skinIndex == 1 ? 2 : 1;
var textures = await Addressables
// 我们需要加载多个资源文件,同时因为贴图文件不需要实例化,所以这里使用LoadAssetsAsync
// 这里由于增加了Label的选项,key我们得变成List<object>,同时值传入address和label
.LoadAssetsAsync<Texture2D>(new List<object> {"Cube Texture", $"Skins{_skinIndex}"}
// 这里的Callback与AsyncObjectHandle的Complete不一样,这里每加载一次就会调用一次回调
// Addressables.MergeMode是关于资源的融合方式,一共有以下几种:
// 假设我们根据key加载的资源分为[1,2,3][3,4,5]两组,那么
// None和Use First都是返回第一组结果:[1,2,3]
// Union返回组中满足任意一个key的结果:[1,2,3,4,5]
// Intersection返回组中满足所有key的结果:[3]
, null, Addressables.MergeMode.None).Task;
// 将返回的贴图赋值给加载的材质
_instance.GetComponent<Renderer>().material.mainTexture = textures[0];
}
运行后,在脚本的Context Menu中选择Change Texture来查看切换贴图效果。
打包
刚才我们在没有修改数据构建模式的情况下加载资源,其实都是在使用FastMode(AssetDataBase)进行加载,不会有实际的打包发生。而我们要在真机上面使用Addressables的时候,就必须打包测试了。
基础的打包也非常简单,配置好分组以及资源地址后,只需要点击Addressable窗口中的Build=>Build Player Content
既开始进行打包。
打包出来的assetbundle会保存到是什么地方呢?我们选择任意一个分组,然后在Inspector中查看配置。
值得注意的是Content Update Group Schema下面的Static Content选项,这部分就是决定你的资源是否能够热更新。勾选,则发生变动是只能随游戏包更新而更新;不勾选则可以打出资源包来进行增量更新。这部分也是全自动的。这边其他的都很好理解,就不一一解释了。你也可以自己创建新的Schema来扩展打包的功能。
图中的Build Path就是AssetBundle打包的地址。虽然看起来在很可疑的位置(Library)下,但是在打包游戏的时候会自动移到StreamingAssets中的。而Build Path 和Load Path中可指定的地址,就是在Addressables Settings中配置的。一般来说Local的Build和Load都不需要更改,而Remote的Load和Path则根据自己的需求会发生变化(模拟服务器、开发服务器、正式服务器等),所以才需要Profiles来进行快速切换。
同时根据上文,我们的Addressable资源的真实位置也是在这边设定的,但是开发者在使用的时候,不需要考虑究竟当前加载的资源是本地的还是远程的,统统交给Addressables来处理就好。这也是为什么Addressables的加载、实例化或是查询都是异步的原因(避免远程加载时长过久而假死)。
结语
那么Addressables基础的功能我们就先介绍到这边,这么强大的系统当然还有更多可以挖掘的地方,下一篇文章我们就来讲讲Addressables的调试、增量更新、本地模拟服务器、自定义Build Script以及资源系统迁移指南等强大的功能。