Project Details

  • Unity 2019.1.14f1
  • Addressables 1.1.7

AddressableAssetsWebinar

什么是Addressables?

  1. 可以取代Asset Bundle的高阶管理系统
  2. 天涯海角都会追踪到的信标(Address)
  3. 自动化仓储管理
  4. 只加载有关联的资源(系统会处理关联性)
    1. 可以实例化场景,自动关联相关的Asset
  5. 运行阶段优良的内存管理
  6. 除脚本外的资源都可以标记为寻址式资源
  7. Build Script可以让你自己写打包流程(命名规则,打包处理,默认压缩LZ4)

不需要手动去管理AssetBundle,可以通过Addressables系统自动化.

Demo

类似皇室战争的demo, 演示Addressable Asset System用法

如图所示,弓箭手卡牌(按钮),点击后会在场地实例化一个弓箭手.

  • 按钮点击事件绑定了 当前场景中的GameManager对象里的CharacterManager脚本中的SpawnCharacter方法

SpawnCharacter 大量生产角色

普通实例化做法

在SpawnCharacter方法中实例化弓箭手, 普通的做法是:

// Inspector面板上放了Archer Red红色弓箭手的prefab
// 类型是GameObject
public GameObject m_archerObject;

// 同步实例化
// 资源已经在本地端
// 大量加载容易造成卡顿
Instantiate(m_archerObject);

Addressables实例化做法

// AssetReference类型 会指向可寻址资源
public AssetReference m_ArcherObject;

// 异步实例化
// 系统不会等待
// 大量实例化不会卡住系统
m_ArcherObject.InstantiateAsync();

同时需要把Archer Red的prefab勾选可寻址信标.

  • 勾选Addressable后,会在Addressables窗口中出现此资源
  • 也可以直接将prefab拖入Addressables窗口

在Addressables窗口中右键创建一个AssetGroups,这个群组中的所有资源可以视为一个加载包.

在项目面板会生成一个同名的包对象. 可以更改Asset Bundle相关设置

再去GameManager对象的Inspector面板上修改public AssetReference m_ArcherObject;的引用, 因为已经将资源加入到Addressable中,所以这里可以下拉框选到对应的可寻址资源.

Addressables实例化多个对象

  1. 将需要实例化的多个对象加入Addressables中

  1. 在脚本的Inspector面板上选取对应的资源
    1. 放入 弓箭手,法师,战士资源.

CharacterManager脚本代码如下:

public class CharacterManager : MonoBehaviour
{
    // 一次加载多个资源时,用List存放这些资源的AssetReference地址
    public List<AssetReference> m_Characters;
    // 资源是否已经加载完毕
    bool m_AssetsReady = false;
    // 资源总数
    int m_ToLoadCount;
    int m_CharacterIndex = 0;

    // Start is called before the first frame update
    void Start()
    {
        // 计数,需要多少个资源
        m_ToLoadCount = m_Characters.Count;

        foreach (var character in m_Characters)
        {
            // 因为不知道这个资源是不是在本地, 所以要确定已经加载完成
            // 因为是异步加载,注册一个完成后的回调函数
            character.LoadAssetAsync<GameObject>().Completed += OnCharacterAssetLoaded;  
        }
    }

    public void SpawnCharacter(int characterType)
    {
        // 当Asset全部加载完毕后,才允许实例化
        if (m_AssetsReady)
        {
            // 返回半径为5的球体内的一个随机点。
            Vector3 position = Random.insideUnitSphere * 5;
            // 设置xz轴, y轴为0
            position.Set(position.x, 0, position.z);
            // 异步实例化
            m_Characters[characterType].InstantiateAsync(position, Quaternion.identity);
        }
    }

    // 回调函数
    void OnCharacterAssetLoaded(AsyncOperationHandle<GameObject> obj)
    {
        // 加载成功一个, 总数减一
        m_ToLoadCount--;
        // 直到加载全部完成 设置flag 为true
        if (m_ToLoadCount <= 0)
            m_AssetsReady = true;
    }

    // 卸载资产Addressables.Release();资产可能会立即卸载,也可能不会立即卸载,具体取决于现有的依赖项
    private void OnDestroy() //TODO: Should we teach instantiate with game objects and then manually release?
    {
        foreach (var character in m_Characters)
        {
            // 释放操作及其相关资源。
            character.ReleaseAsset();
        }
    }
}

需要注意:

  • 在可寻址资源系统中,因为不知道这个资源是不是在本地, 所以要确定是否已经加载完成,因为是异步加载,注册一个完成后的回调函数
  • 以前需要思考这个包是否下载,去服务器请求,下载之后解包,然后加载. 用了AAS之后,这些步骤由AAS替你完成.

举个例子: 请求加载一个法师模型资源, 但是这个资源 不在本地,需要从远程地址(服务器)去下载,只有下载完成之后加载好才去做实例化的工作.

Addressables窗口

用来放所有被放入Addressables的资产.

  1. 默认以路径作为资产的地址(信标).这是可以更改的.
    1. 呼叫此地址的时候,会找到对应的Asset
  2. 可以设置Labels. 用来分类,区分一些同名资产.

profile 配置文件

默认是Default,可以根据不同的需求增加不同的方案,比如正式发布配置方案,测试配置方案等.

  • Send Profiler Events 勾选后,可以在编辑器下运行时查看所有Asset的加载与卸载情况.
    • 默认不勾选,因为正式发布时不需要,影响性能.

  • 蓝绿色背景 代表 已加载的资产.
  • 绿色条状 代表 当前引用计数.

用Label功能

Label功能可以复数勾选.

  1. 先将需要的资源插入信标(拖入AAS)

  1. 在AddressableAssetSetting中加入Label标签.

  2. 给对应的可寻址资源下拉框选择对应的标签.

  3. 在对应脚本上的AssetLabelReference属性下拉框选择标签

这样这个脚本就会在Start的时候,把所有有Tower标签的资源加载进来.(demo中塔顺序不是对应的,只为演示).

public class TowerManager : MonoBehaviour
{
    // 塔prefabs
    public IList<GameObject> m_Towers;
    // AssetLabelReference Label标签引用对象
    public AssetLabelReference m_TowerLabel;
    // 界面上的塔卡片 按钮
    public Button[] m_TowerCards;

    // Start is called before the first frame update
    void Start()
    {
        // 将所有包(AssetGroups)中的带有这个标签的Asset全部拿出来
        // 每拿取完成一个之后会调用OnResourcesRetrieved
        Addressables.LoadAssetsAsync<GameObject>(m_TowerLabel, null).Completed += OnResourcesRetrieved;
    }
    private void OnResourcesRetrieved(AsyncOperationHandle<IList<GameObject>> obj)
    {
        // 拿取的结果存储在IList<GameObject>中
        // demo中塔顺序不是对应的,只为演示
        m_Towers = obj.Result;

        //Activate the tower cards since their assets are now loaded
        foreach(var towerCard in m_TowerCards)
        {
            // 将卡牌按钮显示可按
            towerCard.interactable = true;
        }
    }
    public void InstantiateTower(int index)
    {
        if(m_Towers != null)
        {
            // 随机放在一些位置
            Vector3 position = Random.insideUnitSphere * 5;
            position.Set(position.x, 0, position.z);
            Instantiate(m_Towers[index], position, Quaternion.identity, null);
        }
    }
}

查询机制

如果我需要一把剑, 但是标有剑的Label有100把,那么这个时候就需要查询功能,不需要全部去加载100把剑.

或者可以用更多的Label来区分, 比如青铜系列, 剑, 单手等等.

因为异步加载的关系, 用Label标签加载的资源可能不是预期的顺序.

AAS提供了一个查询机制(不加载,也不实例化,只是查询对应的可寻址资源清单表).

LoadResourceLocationsAsync(label)

取出来的清单会放入IResourceLocation中,然后根据清单加载哪把剑. 自己使用LoadAssetAsync加载.

本地路径和远端路径

以前的流程是:

  • 本地打包,拿去服务端测,有问题再回本地修复,再去测.

为了简化这种流程,AAS提供了一些模式:

  • 快速模式
    • 第一阶段(研发阶段)本地端资源.
    • 快速模式直接通过资产数据库加载资产,以便快速迭代,无需创建分析或资产包(AB包)。
  • 虚拟模式
    • 也是本地端资源,会虚拟一个服务器端功能
    • 分析布局和依赖关系的内容,而无需创建资产包(AB包)。
  • 打包模式
    • 不会用本地资源,会需要包资源.
    • 使用已构建的资产包(AB包)。

每个群组包都可以设置放在本地路径还是远端路径.

用处说明: 游戏打包之后, 嵌在包里的资源(本地),以及服务器端的资源(远端).
游戏运行时会请求服务器资源下载.

4个路径的说明:

如果群组包本地包(跟随游戏包体一起发布的包),则会放入Local Build Path路径下.

如果是远端包(游戏开始时去请求服务器下载的包),会放入Remote Build Path路径下.

本地测试的话,本地打包路径和本地加载路径是相同的.

远端打包的话,路径是在一个文件夹下,而加载的路径是 根据http的一个IP和port去加载.

利用AAS附带的虚拟HTTP服务器模拟

开启虚拟资源服务器

根据地址选取一个,记住名称,在后面配置远程路径时需要用到, 再添加一个Hosting Services服务来开启端口.

点击Enable Services按钮开启服务,会得到一个端口号. 名称也会需要用到.

然后开始配置AddressableAssetSetting里的远程路径设置:

  • Profiles 配置方案新建一个, 改名为开发时的配置方案名称.
  • [PrivateIpAddress_4]改为[PrivateIpAddress]和之前Hosting Services设置对应.

设置对应的包为远程包

首先,更改profile为新建的deve方案,然后选择AessetGroup里的Tower包更改远程构建路径和加载路径. 可以预览的看到.

测试

Play Mode Script改为Packed Paly Mode, 点击Build->Build Play Contant. 这样就完成了打包.

点击播放就可以正常运行,包在对应的目录中.
\HostedData\StandaloneWindows64\towers_assets_all_ee248a6bacba1b7989b2b4d9c590083b.bundle

以上就是远端加载流程. 只要把包放到服务器上,把本地设置改掉就可以真实测试了.

安装包和加载包设置(渐进式递增更新)

不用重新下载整个包. 从App Store下载最小的APP包. 第一次启动加载资料包.

利用Label设计换皮与各种功能包

切换到QualityLevelsDemo场景.

将材质放入Addressables, 并且信标(地址)可以相同,设置不同的Label.

名称相同比如: 帽子,可以分类为A角色的帽子,B角色的帽子, 他们的名称可以相同,用不同的Label就可以区分他们.

public class TextureController : MonoBehaviour
{
    // 渲染材质
    public Renderer m_ReferencedMaterial;

    public void SwitchToHighDef()
    {    
        // 加载对应名称和Label的资源
        LoadTexture("ArcherSkin", "Skin2");
    }

    void LoadTexture(string key, string label)
    {
        Addressables.LoadAssetsAsync<Texture2D>(new List<object> { key, label }, null, Addressables.MergeMode.Intersection).Completed
            += TextureLoaded;
    }

    void TextureLoaded(AsyncOperationHandle<IList<Texture2D>> obj)
    {
        m_ReferencedMaterial.material.mainTexture = obj.Result[0];
    }
}

运行之后,点击就会成一个穿红色衣服的角色.

修改纹理之后,不想要重新打包的做法

静态的本地包可以增量式打包.

将材质包组设置为静态内容.

  • 打包一个可执行文件. 用Unity的build生成exe.

  • 在一些情况下,你想修改包体,而不像重新打包(一个包很大,重新打包很麻烦)
  • 现在可以通过ASS增量式打包(具体规则参考文档)

  • 修改了Skin2的材质
  • 打包流程就需要改变

  • 点击 Prepare For Content Update
  • 相关说明文档
  • 选择原本对应的设置,ASS会提示变动的文件,让你选择是否要分离出一个更新包.
  • 之后分离出一个更新包(非静态的) Content Update组

  • 最后再打成远端包,选择Build For Content Update,选择原本对应的设置.

  • 玩家手中的exe执行档就会从远端下载这个Content Update组里的更新包,完成替换.

远程包如果有修改,不管多大都需要重新打一个完整的. 比如包A(资源1,2,3),修改3的话,资源12也需要重新打进去.
本地包(静态内容,随包发布),如果修改了资源,可以增量式打一个修改的小包,通过远程包的形式替换掉本地包的内容.

场景的异步加载

将场景放入Addressables可以自动关联所有需要的资源.

可以根据信标(地址)用Addressables.LoadSceneAsync方式异步加载场景.

区别于资源,场景要用Addressables.LoadSceneAsync方法加载

public class SceneManager : MonoBehaviour
{
    public string m_SceneAddressToLoad;

    public void LoadGameplayScene()
    {
        // 加载传入String场景名称,异步加载场景
        Addressables.LoadSceneAsync(m_SceneAddressToLoad, UnityEngine.SceneManagement.LoadSceneMode.Single).Completed += OnSceneLoaded;
    }

    void OnSceneLoaded(AsyncOperationHandle<SceneInstance> obj)
    {
        //Addressables.UnloadSceneAsync(new SceneInstance());
        // LOGIC THAT KICKSTARTS THE GAMEPLAY
    }
}