The Addressable Asset System 正式版应用(一)

前言

之前在zhuanlan.zhihu.com/p/46 已经提到过一部分预览版的内容,由于正式版大部分的功能都已经稳定下来,还没看过的同学可以直接看这篇就好了。

最近有点忙,又有点懒,正式版出来后还是拖更了很久抱歉抱歉。

简介

在开始之前,我们也还是先做一个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有以下几个特点:

  1. 使用Addressable在开发前期就进入快速开发的阶段,使用任何你喜欢的资源管理技术,你都能快速的切换来Addressable系统中,几乎不需要修改代码。
  2. 依赖管理:Addressable系统不仅仅会帮你管理、加载你指定的内容,同时它会自动管理并加载好该内容的全部依赖。在所有的依赖加载完成,你的内容彻底可用时,它才会告诉你加载完成。
  3. 内存管理:Addressable不仅仅能记载资源,同时也能卸载资源。系统自动启用引用计数,并且有一个完善的Profiler帮助你指出潜在的内存问题。
  4. 内容打包:Addressable系统自动管理了所有复杂的依赖连接,所以即使资源移动了或是重新命名了,系统依然能够高效地找到准确的依赖进行打包。当你需要将打包的资源从本地移到服务器上面,Addressable系统也能轻松做到,几乎不需要任何代价。

快速开始

说了这么多好处,我们怎么样使用这个系统呢?

由于Addressables本身高度自动化的管理方式,使用起来已经非常简单了,但是第一步当然也得先安装起来。

安装

我们需要Unity 2018.2或者以上的版本,在新增的Package Manager(真的是非常方便的包体依赖管理工具,新版本中还加入了了AssetStore和Github Repo的联动)中能找到Addressables包体,点击Install安装即可。

安装最新版本的Addressable

配置

点击Window > Assets Management > Addressable Assets进入配置界面,创建新的Addressable 设置。

选择Create Addressables Settings
默认的配置界面

到这一步为止,基础的配置就已经完成了

准备

我们知道要使某个资源能被定位(Addressable),得先给这个资源加上一个地址(Addresss),而资源本身存储什么地方,我们暂时先不用关心。

首先我们准备一下需要加载的资源,这边创建一个Cube的Prefab以及Material,同时准备两张不同的贴图并将其中一张(这里Box)设置给Cube的Material。

然后我们需要把这个Cube设置为Addressable,并且赋予其一个Address。

这里我们有两种方式实现:

  1. 选择Cube的Prefab,在Inspector上勾选Addressable,并且设置其地址。
  2. 拖动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。

新建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以及资源系统迁移指南等强大的功能。

发布于 2019-09-02

文章被以下专栏收录