Unity中资源的加载方式
- 直接引用,把引用的物体直接通过Inspector面板拖入:
public RectTransform SkinsContainer;
[SerializeField]
private SpriteRenderer m_render;
这种方式使用的局限性很大,而且随着项目规模的增加,会出现各种未知的错误.
- 把Asset放在 Resources 文件夹,然后使用特定的API进行加载:
Resources.Load("abc.txt");
在刚使用Unity进行项目开发的时候,使用最多的便是这种方式,随着进一步学习,意识到这种方式的弊端也是很大的:
- Resources 文件夹下的内容都会被打包进App的安装包,资源越多,安装包的体积会越大
- Resources 文件夹下的内容不能进行热更新,只能重新打包整个游戏发布
- 对Asset文件的命名要求严格,不能进行随意的变更,因为代码中加载资源时使用的名称都是写死的
-
AssetBundle: 基于前两种方式的弊端,如果要对资源进行热更新,AssetBundle几乎是唯一的选择.
其实AssetBundle最初设计出来是让非开发人员来使用的,因此很多的API设计并不是很友好,而且随着项目规模的增加,管理难度也会相应增加.首先需要根据平台和实际需要来设计打包脚本
要处理包体的依赖和复用关系,自己来编写一套资源管理系统
总的来说,系统对开发者暴露了太多的打包细节,这从一个模块的设计上来说是不合理的,项目不应该有时间浪费在打包的逻辑设计上.
为什么要使用Addressable?
其实Unity官方很早就通过论坛和反馈意识到了这个问题,2018年2月的时候推出了Addressable第一个preview版本,直到2019年7月确定发布了第一个正式版.
用一句话来概括Addressables: 对AssetBundle更高级的封装,把打包细节隐藏掉,使开发者不必在打包逻辑上花费时间.
- 可以快速集成到项目中,不需要额外的管理成本
- 自动管理了包体的依赖和复用关系
- 使用引用计数来管理内存,提供了完整的加载和卸载资源的接口,而且配套 Profiler 工具来帮助解决可能有的内存问题
- 对资源的命名不敏感,只要指定了资源的 ResourceLocator ,即使资源进行了移动或者重命名,也能够准确地定位到
- 不关心资源的存放位置,资源存放在服务器或者打包进包体都可以,但开发者使用的接口都是一致的,这也是Addressables使用异步接口的原因
- 基于上一条,可以方便地进行资源包存放位置的更换
Addressable是官方主推的打包工具,而且也集成进了其主推的ECS开发体系,因此有必要学习和了解这一套工具.
Addressable的导入和配置说明
笔者使用的编辑器版本是2018.4.12f1,能支持的Addressables最高版本是1.16.1.
安装方式: 通过Package Manager进行安装.
安装后按照 Window > Asset Management > Addressables > Groups 打开界面,第一次使用的时候会提示创建Settings:
这里只跟大家解释几个重要的配置,其余的配置可以查看官方文档: Addressable官方文档
1. AddressableAssetSettings
这是整个Addressables系统的设置界面,要注意的就是图中标出的选项一定要勾选.可以在这个界面进行服务器环境的快速切换以及进入资源组管理界面.
2. Profiles
在这个界面可以进行服务器环境的配置,一般我们至少需要两套开发环境: Debug 以及 Release
- LocalBuildPath: 创建到Library目录,会在打包时候自动拷贝进StreamingAssets,不要改动
- LocalLoadPath: 本地资源包的加载路径,默认是StreamingAssets文件夹,不要改动
- RemoteBuildPath: 服务器分包打包完毕后的存放路径,一般不需要改动,每个平台会对应存放到各自平台的文件夹下
- RemoteLoadPath: 服务器分包的加载路径,我们需要把RemoteBuildPath下生产的包体和catalog文件一起放到该路径下,应用在运行时会从该路径进行下载和更新
3. AssetGroups
资源需要进行分组管理,每个组都有自己对应的AssetgroupScheme来标记自己属于本地包还是服务器包.
我们先来看第一张图,Assetgroup内每一项都是一个单独的资源项:AddressableAssetEntry,它有几个重要的属性:
- GroupName: 所属的组名称
- AddressableName: 在Addressable系统中的 ResourceLocator , 与资源的真实名称无关,系统通过该项进行资源查找,可以在Inpector面板修改,也可以在该界面右键修改
- AssetPath: 在编辑器中的路径
- Labels: 额外辅助的 ResourceLocator ,可以通过与AddressableName的配合来加载指定的一类资源,比如图中可以通过该项加载第几套皮肤
怎样才能把一个资源变为一个AddressableAssetEntry呢?
- 选中资源,直接拖动到上面的界面
- 选中资源,在Inspector面板中勾选上Addressable选项
第二张图中显示的便是对应Group的AssetgroupScheme,除了可以在这里进行包体存放位置的配置,还可以进行压缩格式等选项的配置.要注意的是下面的 Update Restriction 选项:
- Can Change Post Release: 字面意思,应用发布后可以进行更改,其实准确的意思是发布后会经常更新的包,也可以称之为 "动态包";
- Cannot Change Post Release: 应用发布后不能进行更改,准确的意思是发布后不会经常更新的包,也称之为 "静态包";
这里会有个误区,认为静态包是随着包体走的,或者静态包是不能进行资源热更新的,这种理解是错误的.
静态包与动态包与包体存放的位置没有关系, 而且都是可以进行热更新的,只是更新的策略不一样,下面会详细讲述.
静态包用来存放项目中不会变动或者不常变动的资源,包体一般会比较大,而动态包时需要经常更新的,因此用来存放经常改动的资源.
4. PlayModeScript
Unity提供了三种打包模式:
- Use Asset DataBase(Fast Mode): 这种模式下,它能快速的启动游戏,不需要构建等步骤,直接从AssetDatabase数据库加载资源.可用于快速开发迭代,没有任何的分析和相关的asset bundle的创建.
- Simulate Groups (Advanced Mode): 在这种模式下模拟资源从bundle加载的情形, 可以通过 Window > Asset Management > Addressables > Event Viewer 来查看Bundle的加载与卸载情况.⽤来判断当前的加载策略是否正确,资源分组⽅式是否最佳,可以在发布之前找到最合适的资源分组策略.
- Use Existing Build (Requires built groups): 真正的打包模式,在该模式下需要手动触发Build选项来进行打包.
Addressable的基本使用
上面讲解了一些理论知识,接下来让我们开心的撸一些代码吧!
[SerializeField]
private AssetReference _prefabAsset;
我们仍然可以用上面的方式在Inspector界面拖入一个AddressableAssetEntry资源.
Unity自带了很多特定的AssetReference类:
- AssetReferenceSprite
- AssetReferenceTexture2D
- AssetReferenceGameObject
- ...
通过Inspector中的下拉框,只会显示指定类型的AddressableAssetEntry资源.
1. AddressableAssetEntry资源的加载
有三种加载后的回调形式可以使用,我更倾向于使用第三种的 Async-Await 语法糖形式.
示例中只是对一个Asset资源进行了加载,其实还可以对一个Scene进行加载.
_prefabAsset.LoadAssetAsync().Completed += AssetLoadCompleted;
private void AssetLoadCompleted(AsyncOperationHandle obj)
{
//加载完毕 根据加载状态进行实例化 可以同步实例化
if (obj.Status == AsyncOperationStatus.Succeeded)
{
_instance = Instantiate(obj.Result);
}
else
{
Debug.LogWarning("AssetLoadFail!");
}
}
_prefabAsset.LoadAssetAsync().Completed += handle => Instantiate(handle.Result);
private async void LoadAssetReferenceAsyncAwait()
{
var obj = await _prefabAsset.LoadAssetAsync().Task;
_instance = Instantiate(obj);
}
2. AddressableAssetEntry资源的实例化
与加载相对应,实例化后的回调也有三种方式:
_prefabAsset.InstantiateAsync().Completed += AssetInstantiateCompleted;
private void AssetInstantiateCompleted(AsyncOperationHandle obj)
{
_instance = obj.Result;
}
_prefabAsset.InstantiateAsync().Completed += handle => _instance = handle.Result;
private async void InstantiateAssetReferenceAsyncAwait()
{
_instance = await Addressables.InstantiateAsync(_assetPath).Task;
}
3. Labels的使用
通过AddressableName和Label的配合,我们就可以加载一个更具体的资源:
比如对于一个角色换肤,我们可以把皮肤的AddressableName都设置为 HeroSkin,每套皮肤有自己的标签, 如果我们要使用 Skin2 的皮肤:
// 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]
private async void ChangeSkin(List keys)
{
if (keys == null)
{
keys = new List {"HeroSkin", "Skin2"};
}
var materials = await Addressables.LoadAssetsAsync(keys, SkinLoadCompleted, Addressables.MergeMode.Intersection).Task;
_instance.GetComponent().material = materials[0];
}
可以通过 ResourceLocator 的组合来加载到指定的资源, 下面的换装系统就是基于这种方式来实现的.
Addressables的打包与资源更新
设置好每个资源组的打包策略后,让我们来开始正经的打包吧!
1. 项目发布前的打包
- 首先确定PlayModeScript设置的打包模式是Use Existing Build (Requires built groups);
-
清空一下以前的打包缓冲:
-
开始全量打包:
- 找到我们设置的RemoteBuildPath路径,一般是 ServerData/[BuildTarget]/{Version} 文件夹下,
把生成的文件拷贝到服务器,服务器也要按版本进行存放,同时要修改RemoteLoadPath与服务器的存放位置一致, 这就完成了资源的布局. - 开始应用的打包,这一步直接打包或者使用自动化打包脚本来配置都可以.
2. 资源的更新
项目上线后,如果有资源的修改(新增, 删除, 替换),使用Addressable系统可以快速的实现热更新.
- 确保资源修改完成,资源组策略配置完毕
- 清理一下打包缓存
- 查看有哪些内容进行了变化:
选择指定平台的addressables_content_state.bin文件.
该文件存储的是全量打包后所有静态包的映射关系,是资源热更新的基础,每次全量打包后该文件都会被覆盖,因此每次全量打包之前需要保存该文件.
进行比对后会会把修改的所有静态包资源单独抽取为一个组:
-
开始增量打包:
- 在RemoteBuildPath路径下,找到生成的资源包,拷贝到服务器的指定位置,这样就可以在不升级App版本的情况下进行资源的热更新.
3. 项目的迭代打包
上面的两步指明了应用第一个版本发布后的资源热更新流程,那么当App要发布第二版的时候,是怎么样的打包流程呢?
有两种不同的场景需要考虑;
新的版本更新没有进行资源的变动,只是进行了代码的修改,那么不用再进行资源打包,直接进行 1.5 的流程, 如果该版本后续有资源需要热更,按照2.1->2.5的步骤进行升级.
新版本有资源变动,我们需要进行新的全量打包.全量打包前要对该分支打上标签,因为一旦进行全量打包, 上一个版本的 addressables_content_state.bin 文件就会被覆盖,因此为了方便测试和后续的问题修改,有必要保存该文件.然后开始1.1->1.5的流程,资源更新继续2.1->2.5的流程,这样就完成了一个循环迭代.
4. 静态包与动态包的更新策略的不同
两种包体有着不同的更新策略,一句话概括为:如果有了资源更新,动态包会进行整包替换,静态包是增量更新,用新的资源替代被修改的资源,被替代的资源会永久留在用户的应用内,虽然永远不会再被使用.用官方的一个示例来阐述两者的不同.
在开发者的角度,有三个资源组:
- 本地静态包,安装在用户应用内
- 远程静态包,可能在用户的应用缓存内,也可能只在服务器上还未下载到应用内
- 远程动态宝,同样在服务器上,也可能在用户的应用缓存内
如果我们对 AssetA,AssetL,AssetX进行了修改,并且使用 Check for Content Update Restrictions 来查看包变化:
在开发者编辑器上,新的资源组布局如图所示.
让我们切换一下视角,来到用户的角度:
该静态包已经在用户的应用内,此处的AssetA已经不会再引用,变成了包体内的 dead data;
如果用户手机内还没有该包,那么当使用到AssetM或者AssetN的时候,会从服务器下载该包,痛上面的本地静态包一样,该包中的AssetL不会再被使用,成为了 dead data.
该动态包如果已经存在了用户缓存中,那么会从缓存中移除,从服务器下载整个新包,如果没有在缓存中,则直接从服务器下载新包,AssetX不会产生dead data.
经过修改后的静态包中资源,我们可以称之为新的AssetA和新的AssetL,将会被应用引用.
关于这个示例,如果还有些疑惑,可以在官方文档查看更详细的流程: 官方示例
测试的时候最好用真机测试,那么就需要一个服务器,我这里是直接把自己的Mac电脑当成了服务器,不会搭服务器的小伙伴看这里:Mac上快速搭建Apache服务器
基于Addressable的换装实现
关于换装,上面代码示例中的Label使用提供了初始的思路,我们来设计一下项目中怎么来实现换装页面;
- 换装页面: UI层
- 换装资源: 资源层
UI层与资源层之间不要进行直接的交互,会造成很差的扩展性,因此需要一个数据层来桥接两端:
- 换装DataTable: 数据层
我们使用一个csv或者txt文件来保存UI层要使用的数据,该文件作为一个 TextAsset 使用Addressable管理,在展示换装页面的时候,先读取该配置文件,然后根据配置文件生成UI,这样就可以修改该文件来达到皮肤的新增与删除以及皮肤的排序.
示例的表格做的比较简单,只记录每种资源的Label字段,还可以有新的字段,如果每种服装按钮上的文本也可以在这里进行配置.
把该文件使用Addressable管理.
private async void LoadSkinsTable()
{
string skinsKey = "SkinsTable";
var skins = await Addressables.LoadAssetAsync(skinsKey).Task;
_skinsLabel = skins.text.Split(new string[] { "\r\n" }, StringSplitOptions.None).ToList();
_skinsLabel.RemoveAt(0);
//根据数据创建皮肤按钮
foreach (var skinName in _skinsLabel)
{
var skinButton = await Addressables.InstantiateAsync("SkinButton", SkinsContainer).Task;
skinButton.GetComponentInChildren().text = skinName;
skinButton.GetComponent
这样可以根据配置表来动态的生成换肤按钮,从而达到换肤资源的热更新.
结语
Addressables是Unity极力主推的资源管理工具,因此有必要早日在项目中使用.
不过其异步编程的方式需要各位筒子逐渐接受,在使用的过程中发现了问题,那就一起开心的来解决吧!
待继续研究的问题:
内存的管理机制
便捷的Editor脚本使用
Event Viewer与Analyze的使用
如果大家想更进一步的了解Addressable的知识,我这里有三枚锦囊,赠与各位;
锦囊1: 简单直接
锦囊2: 通俗易懂
锦囊3: 有点厉害