第1章: Addressables概述与导入

Addressables概述

建议搭配课程

【游戏开发探究】Unity Addressables资源管理方式用起来太爽了,资源打包、加载、热更变得如此轻松(Addressable Asset System | 简称AA)_unity aa-CSDN博客

学习Addressables的前提

  1. 掌握委托事件(C#进阶课程中)

  2. 掌握AssetBundle相关知识点(Lua热更新课程中)

  3. 掌握Resources同步异步加载(Unity基础课程中)

  4. 掌握ScriptableObject(Unity进阶课程中)

  5. 掌握协同程序基本原理(Unity入门课程中)

Addressables是什么

Addressables翻译过来是可寻址的意思,它是可寻址资源管理系统

是Unity从2018.2版本开始

建议用于替代AssetBundle的高阶资源管理系统

在之后的Unity的新版本中,AssetBundle将渐渐被淘汰

但是AssetBundle对于大家来说,还是必备的知识点,因为目前市面上还有很多的项目依旧在使用较老版本的Unity进行开发或者迭代,所以AssetBundle还是一种主流传统的资源管理方式

而Addressables和AssetBundle的主要作用是一样的

  1. 管理资源

  2. 热更新

  3. 减小包的体积

Addressables和AssetBundle的区别

Addressables是基于AssetBundle架构做的高阶流程

你可以认为Addressables将AssetBundle原来需要进行的一些复杂步骤,变的更加自动化,可视化,易管理化。

Addressables的具体优点

  1. 自动化管理AB包打包、发布、加载

  2. 可以更方便的进行本地、远程资源的加载

  3. 系统会自动处理资源关联性

  4. 内存管理更方便

  5. 迭代更方便

本套课程学习大纲

导入Addressables

知识点一 导入Addressables包

  1. 打开Window——>Package Manager窗口

  2. 设置包为Unity Registry

  3. 找到Addressables包 进行安装

知识点二 创建配置相关文件

我们需要先创建配置相关文件后,才能正式使用Addressables。

方法一:打开资源组窗口

  1. Window——>Asset Management——>Addressables——>Groups

  2. 在窗口中点击 Create Addressables Settings按钮 创建配置文件

方法二:在Inspector窗口中为资源勾选Addressable

如果没有创建过配置相关文件,这时会自动创建相关文件

第2章: 资源加载基础

寻址资源设置

知识点一 让资源变为可寻址资源

方法一:选中资源,勾选Inspector窗口中的Addressable

方法二:选中资源,拖入Addressables Groups窗口中

注意:

  1. C#代码无法作为可寻址资源

  2. Resources文件夹下资源如果变为寻址资源,会移入Resources_moved文件夹中

原因:Resources文件夹下资源会最终打包出去,如果变为可寻址资源意味着想通过Addressables进行管理,也没必要再通过Resources去获取这个资源

那么它就没有必要通过Resources方式去加载和打包,所以会自动迁移,避免重复打包,浪费空间

右键选择资源时菜单内容

  • Move Addressables to Group:将该资源放入到现有的另一个组中

  • Move Addressables to New Gourp:使用与当前组相同设置创建一个新租,并将该资源放入该新组中

  • Rmove Addressables:移除资源,该资源会变为不可寻址资源

  • Simplify Addressable Names:简化可寻址资源名,会删除名称中的路径和拓展,简化缩短名称

  • Copy Address to Clipboard:将地址复制到剪贴板

  • Change Address:改名

  • Create New Group:创建新租

知识点二 资源组窗口(Addressables Groups)讲解

这个窗口将是我们进行Addressables 交互的主要窗口。

资源信息(关键)

  1. GroupName\Addressable Name:分组名\可寻址名(可重名,描述资源)

  2. Path:路径(不可重复,资源定位)

  3. Labels:标签(可重复、可用于区分资源种类,例如青铜装备、黄金装备)

创建分组相关

Packed Assets:打包资源分组

Blank(no schema):空白(无架构)

区别:Packed Assets默认自带默认打包加载相关设置信息,Blank没有相关信息需要自己关联

组对于我们来说意义重大,之后在资源打包时,一个组可以作为一个或多个AB包

关于组设置相关信息,之后详细讲解

选中某一组后右键

Remove Group(s):移除组,组中所有资源恢复为不可寻址资源,默认组没有此选项,需切换其他默认组才允许删除。

Simplify Addressable Names:简化可寻址名称,会删除名称中的路径和拓展,简化缩短名称

Set as Default:设置为默认组,当直接勾选资源中的Addressable时,会自动加入该组,非默认组的对象才有这个选项

Inspect Group Setting:快速选中关联的组相关配置文件

Rename:重命名

Create New Group:创建新组

配置概述相关

Manage Profiles:管理配置文件

可以配置打包目标、本地远程的打包加载路径等等信息(之后再详细讲解)

Tools工具相关

Inspect System Settings:检查系统设置

Check for content Update Restrictions:检查内容更新限制

Window:打开Addressables相关窗口

Groups View:分组视图相关

Show Sprite and Subobject Addressable:显示可寻址对象的精灵和子对象,一般想要看到图集资源内内容时可以勾选该选项

Group Hierarchy with Dashes:带破折号的组层次结构

Play Mode Script播放模式脚本(编辑模式下如何运行)

确定在编辑器播放模式下运行游戏时,可寻址系统如何访问可寻址资源

Use Asset Database(fastest):

使用资源数据库(最快的),一般在开发阶段使用,使用此选项时,您不必打包可寻址内容,它会直接使用文件夹中的资源

在实际开发时,可以不使用这种模式,这种模式没有测试的意义

Simulate Groups(advanced):

模拟组(后期),一般在测试阶段使用,分析布局和依赖项的内容,而不创建AB包

通过ResourceManager从资产数据库加载资产,就像通过AB包加载一样

通过引入时间延迟,模拟远程资产绑定的下载速度和本地绑定的文件加载速度

在开发阶段可以使用这个模式来进行资源加载

Use Existing Build(requires built groups):

正儿八经的从AB包加载资源

使用现有AB包(需要构建AB包),一般在最终发布测试阶段使用

从早期内容版本创建的AB包加载资产

在使用此选项之前,必须使用生成脚本(如默认生成脚本)打包资源

远程内容必须托管在用于生成内容的配置文件的RemoteLoadPath上

Build(构建打包相关)

New Build:构建AB包资源(相当于打包资源分组)

Update a Previour Build:更新以前的版本

Clean Build:清空之前的构建资源

知识点三 资源名注意事项

1.资源路径一定不允许相同(后缀不同,名字相同可以)

2.资源名我们可以随意修改

3.之后在加载资源时我们可以使用名字和标签作为双标识加载指定资源

知识点四 资源分组

我们可以按规则将资源进行分组

比如:角色、装备、怪物、UI等等

指定资源加载

知识点一 Addressables中的资源标识类(公共变量)

命名空间:using UnityEngine.AddressableAssets;

  • AssetReference 通用资源标识类 可以用来加载任意类型资源

  • AssetReferenceAtlasedSprite 图集资源标识类

  • AssetReferenceGameObject 游戏对象资源标识类

  • AssetReferenceSprite 精灵图片资源标识类

  • AssetReferenceTexture 贴图资源标识类

  • AssetReferenceTexture2D 2D贴图资源标识类

  • AssetReferenceTexture3D 3D贴图资源标识类

  • AssetReferenceT<> 指定类型标识类

通过不同类型公共标识类对象的申明 我们可以在Inspector窗口中筛选关联的资源对象

不过需要强调的是,在实际项目里面我大概率会更多的使用代码直接根据条件加载资源对象,后面动态加载资源会提到这个方法。

而本节以标识类为例。

知识点二 加载资源

注意:所有Addressables加载相关都使用异步加载

需要引用命名空间:using UnityEngine.ResourceManagement.AsyncOperations;

public AssetReference assetReference;//通用资源标识类,需要外部赋值
//用泛型为资源指定类型
  AsyncOperationHandle<GameObject> handle = assetReference.LoadAssetAsync<GameObject>();

加载成功后使用

  1. 通过事件函数传入的参数判断加载是否成功 并且创建

  2. 通过资源标识类对象判断 并且创建

通过异步加载返回值 对完成进行事件监听

handle.Completed += TestFun;//此回调会在资源加载完毕后执行一次

private void TestFun(AsyncOperationHandle<GameObject> handle)
    {
        //加载成功后 使用加载的资源嘛
        //判断是否加载成功
        if(handle.Status == AsyncOperationStatus.Succeeded)
        {
            Instantiate(handle.Result);
        }
    }


//也可以直接写成匿名函数的格式,一步到位。
assetReference.LoadAssetAsync<GameObject>().Completed += (handle) =>
{
    //使用传入的参数(建议)
    //判断是否加载成功
    if (handle.Status == AsyncOperationStatus.Succeeded)
    {
        GameObject cube = Instantiate(handle.Result);

 
    }

};

知识点三 加载场景

由于场景加载完毕通常是需要载入的,Unity为我们提供了专门的场景加载API,它的completed会在切换到新场景后执行

sceneReference.LoadSceneAsync().Completed += (handle) =>
{
    //初始化场景的一些信息
    print("场景加载结束");
};

看起来这个complete是在新场景才会执行,但是,尽管场景的所有元素都已加载,但场景中的脚本可能尚未完全初始化,所以不要指望用它调用一些新场景的逻辑,最后只用来初始化场景。

LoadSceneAsync里面有一些独特的参数,第三个参数可以开启手动跳转场景,而把调整时机交给completed进行判断,我们会在下文的动态加载资源一课提到。

知识点四 释放资源

释放资源相关API

ReleaseAsset

我们通常会把释放在预制体创建完毕后进行,或者将handle用数组收集起来,等到需要释放的时候在进行处理。

public AssetReference assetReference;//通用资源标识类,释放后会置空
 handle = Addressables.LoadAssetAsync<GameObject>("Cube");//释放后不会置空
handle.Completed += (handle) =>
{
    //判断加载成功
    if (handle.Status == AsyncOperationStatus.Succeeded)
        Instantiate(handle.Result);

    //一定要是 加载完成后 使用完毕后 再去释放
    //不管任何资源 只要释放后 都会影响之前在使用该资源的对象
    Addressables.Release(handle);
};

  1. 释放资源方法后,资源标识类中的资源会置空,但是AsyncOperationHandle类中的对象不为空,即使资源被释放,AsyncOperationHandle 依然可以访问其属性,如 IsDoneStatus 等,但 Result 访问将不再有效,因为资源已被卸载。

  1. 释放资源不会影响场景中被实例化出来的对象,但是会影响使用的资源,导致其消失。使用前两种Play Mode Script可能体现不出来,只有第三种会体现出来(实际打包运行模式)。

知识点五 直接实例化对象

只适用于 想要实例化的 对象 才会直接使用该方法 一般都是GameObject预设体

gameobjcetReference.InstantiateAsync();

知识点六 自定义标识类

自定义类 继承AssetReferenceT<Material>类 即可自定义一个指定类型的标识类

该功能主要用于Unity2020.1之前,因为之前的版本不能直接使用AssetReferenceT泛型字段

知识点七 总结

1.我们可以根据自己的需求选择合适的标识类进行资源加载

2.资源加载和场景加载都是通过异步进行加载

3.需要注意异步加载资源使用时必须保证资源已经被加载成功了,否则会报错

Label标签的作用

知识点一 关于加载资源的方法

上一节我们用标识符进行指定资源加载的方法,其实不会被经常使用

这种模式我们必须在脚本中声明各种标识类来指定加载的资源,不够灵活,做一些小项目没问题

但是在实际商业项目开发中,很多时候加载什么资源都是根据配置文件决定的,往往都是动态加载

请注意的是,Group本身并不允许被拿来根据条件加载对应资源,而是用于配置和分类这些资源的存在。类似当初AB包依赖包的存在

所以我们需要学习根据名字或标签去加载对应的资源,这样我们就可以读表进行加载

知识点二 添加标签

在面板的标签下拉栏就可以直接指定标签,并且可以通过齿轮打开标签管理来添加删除标签。

一个资源可以拥有多个标签。

知识点三 标签的作用

首先需要强调

我们之后学习动态加载资源时

是可以通过名和标签来加载资源的

举例说明 1

游戏装备中有一顶帽子:Hat

但是它可以有不同的品质,比如:红、绿、白、蓝

那么我们可以为这个帽子添加多个材质球资源(或者贴图资源)

这些图都可以叫做:Hat_Mat(或者Hat_Tex)

他们可以同名,我们可以通过标签Label来区分他们

他们的Label可以是:Red、Green、White、Blue

举例说明 2

游戏中我们经常根据设备好坏来选择不同质量的图片或者模型

比如:高清、标清、超清

不同标准下的资源会有所不同,比如模型面数更低、贴图质量更低

但是在不同标准下,这些模型的命名应该是同样的

比如角色1,在高清、标清、超清下它的资源名都是角色1

它的Label可以是:HD、SD、FHD

举例说明 3

游戏中我们经常在逢年过节时更换游戏中模型和UI的显示

比如:中秋节、春节、圣诞节

不同节日时角色或者UI等等资源看起来是不同的

但是在不同节日下,这些资源的命名应该都是同样的规范

比如登录面板,在中秋节、春节、圣诞节时它的资源名都是 登录面板

它的Label可以是:MidAutumn、Spring、Christmas

总结

相同作用的不同资源(模型、贴图、材质、UI等等)

我们可以让他们的资源名相同

通过标签Label区分他们来加载使用

知识点四 通过标签相关特性约束标识类对象

特性[AssetReferenceUILabelRestriction()]

这个特性主要用于我们上一节的指定资源加载中。

用于在编辑器中对 AssetReference类型的公共字段(标识类)施加标签限制。这种特性非常有用,因为它可以帮助确保开发者在赋值给特定 AssetReference 字段时只选择符合指定标签的资源

知识点五 总结

相同作用的不同资源

我们可以让他们的资源名相同

通过标签Label区分他们来用途

用于之后的动态加载

利用名字和标签可以单独动态加载某个资源

也可以利用它们共同决定加载哪个资源

动态加载单个资源

知识点一 通过资源名或标签名动态加载单个资源

命名空间:

UnityEngine.AddressableAssets 和 UnityEngine.ResourceManagement.AsyncOperations

handle = Addressables.LoadAssetAsync<GameObject>("Cube");
handle.Completed += (handle) =>
{
    //判断加载成功
    if (handle.Status == AsyncOperationStatus.Succeeded)
        Instantiate(handle.Result);

    //一定要是 加载完成后 使用完毕后 再去释放
    //不管任何资源 只要释放后 都会影响之前在使用该资源的对象
    Addressables.Release(handle);
};

//Addressables.LoadAssetAsync<GameObject>("Red").Completed += (handle) =>
//{
//    //判断加载成功
//    if (handle.Status == AsyncOperationStatus.Succeeded)
//        Instantiate(handle.Result);
//};

注意:

  1. 如果存在同名或同标签的同类型资源,我们无法确定加载的哪一个,它会自动加载找到的第一个满足条件的对象

  2. 如果存在同名或同标签的不同类型资源,我们可以根据泛型类型来决定加载哪一个

知识点二 释放资源

知识点三 动态加载场景

参数一:场景名

参数二:加载模式 (叠加还是单独,叠加就是两个场景一起显示,单独就是只保留新加载的场景,正常情况为单独)

参数三:场景加载是否激活,如果为false,加载完成后不会直接切换,需要自己使用返回值中的ActivateAsync方法

参数四:场景加载的异步操作优先级

csharp

Addressables.LoadSceneAsync("SampleScene", UnityEngine.SceneManagement.LoadSceneMode.Single, false).Completed += (obj)=> {
    //比如说 手动激活场景
    obj.Result.ActivateAsync().completed += (a) =>
    {
        //然后再去创建场景上的对象的逻辑

        //然后再去隐藏 加载界面的逻辑

        //注意:场景资源也是可以释放的,并不会影响当前已经加载出来的场景,因为场景的本质只是配置文件
        Addressables.Release(obj);
    };
};

注意:需要强调的是,ActivateAsync执行的时候依然不能保证脚本的初始化已经完成,所以,如果你想在这里留下一些调用初始化后脚本的逻辑,可以考虑采用延时执行或者一个过场不会被移除的消息分发机制。

知识点四 总结

1.根据名字或标签加载单个资源相对之前的指定加载资源更加灵活

主要通过Addressables类中的静态方法传入资源名或标签名进行加载

注意:

1-1.如果存在同名或同标签的同类型资源,我们无法确定加载的哪一个,它会自动加载找到的第一个满足条件的对象

1-2.如果存在同名或同标签的不同类型资源,我们可以根据泛型类型来决定加载哪一个

2.释放资源时需要传入之前记录的AsyncOperationHandle对象

注意:一定要保证资源使用完毕过后再释放资源

3.场景异步加载可以自己手动激活加载完成的场景

动态加载多个资源

知识点一 根据资源名或标签名加载多个对象

加载资源

参数一:资源名或标签名

参数二:加载结束后的回调函数

参数三:如果为true表示当资源加载失败时,会自动将已加载的资源和依赖都释放掉;如果为false,需要自己手动来管理释放,默认为false。

AsyncOperationHandle<IList<Object>> handle = Addressables.LoadAssetsAsync<Object>("Red", (obj) =>
{
    //print(obj.name);
});

如果要进行资源释放管理 那么我们需要使用Completed这种回调方式 要方便一些

因为我们得到了返回值对象handle就可以释放资源了

handle.Completed += (handle) =>
{
    foreach (var item in obj.Result)
    {
        //print(item.name);
    }
    //释放资源
    Addressables.Release(obj);
};

我们还是可以通过泛型类型,来筛选资源类型

关于complete和LoadAssetsAsync的参数回调的两种委托区别(重要)

complete:会在确保所有资源加载完毕后执行一次,传入参数类型为AsyncOperationHandle,handle可以获取的信息是比较丰富的,比如加载状态之类的都可以获取。

LoadAssetsAsync的参数回调:每加载一次资源,都会以这个资源为传入参数类型执行一次,即会执行多次,并且执行的时候不确保其他资源加载完毕。但是这种回调更偏向于对每个资源本身的单独调用,是无法直接靠参数获取全局的加载信息的。

知识点二 根据多种信息加载对象

参数一:想要加载资源的条件列表(资源名、Lable名)

参数二:每个加载资源结束后会调用的函数,会把加载到的资源传入该函数中

参数三:可寻址的合并模式,用于合并请求结果的选项。

如果键(Cube,Red)映射到结果([1,2,3],[1,3,4]),数字代表不同的资源

None:不发生合并,将使用第一组结果 结果为[1,2,3]

UseFirst:应用第一组结果 结果为[1,2,3]

Union:合并所有结果 结果为[1,2,3,4]

Intersection:使用相交结果 结果为[1,3]

如果不填写,默认参数为None。

参数四:如果为true表示当资源加载失败时,会自动将已加载的资源和依赖都释放掉

如果为false,需要自己手动来管理释放

List<string> strs = new List<string>() { "Cube", "HD" };
Addressables.LoadAssetsAsync<Object>(strs, (obj) => {
    print(obj.name);
}, Addressables.MergeMode.Intersection);

注意:我们还是可以通过泛型类型,来筛选资源类型

总结

1.可以根据 资源名或标签名+资源类型 来加载所有满足条件的对象

2.可以根据 资源名+标签名+资源类型+合并模式 来加载指定的单个或者多个对象

Addressables管理类(有缺陷)

导入

在学习了 Addressables体系后,我们自然的想到可以为其专门写一个管理类,正如我们上文提到的,我们的思路就是多一个字段用以管理我们已经加载的handle,以此获得资源信息和进行调用。

但是需要注意的是,本管理类是初步版本,有一些瑕疵会在下文解决,问题有:

  1. 由于handle用泛型的原因,dic只能用IEnumerator暂时替代,这导致了Clear方法无法使用真正的ab包释放api。

  2. 没有实现Addressables自带的资源加载和释放计数制度

代码

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class AddressablesMgr
{
    private static AddressablesMgr instance = new AddressablesMgr();
    public static AddressablesMgr Instance => instance;

    //有一个容器 帮助我们存储 异步加载的返回值
    public Dictionary<string, IEnumerator> resDic = new Dictionary<string, IEnumerator>();

 

    //异步加载单个资源的方法
    public void LoadAssetAsync<T>(string name, Action<AsyncOperationHandle<T>> callBack)
    {
        //由于存在同名 不同类型资源的区分加载
        //所以我们通过名字和类型拼接作为 key
        string keyName = name + "_" + typeof(T).Name;
        AsyncOperationHandle<T> handle;
        //如果已经加载过该资源
        if (resDic.ContainsKey(keyName))
        {
            //获取异步加载返回的操作内容
            handle = (AsyncOperationHandle<T>)resDic[keyName];

            //判断 这个异步加载是否结束
            if(handle.IsDone)
            {
                //如果成功 就不需要异步了 直接相当于同步调用了 这个委托函数 传入对应的返回值
                callBack(handle);
            }
            //还没有加载完成
            else
            {
                //如果这个时候 还没有异步加载完成 那么我们只需要 告诉它 完成时做什么就行了
                handle.Completed += (obj) => {
                    if (obj.Status == AsyncOperationStatus.Succeeded)
                        callBack(obj);
                };
            }
            return;
        }
        
        //如果没有加载过该资源
        //直接进行异步加载 并且记录
        handle = Addressables.LoadAssetAsync<T>(name);
        handle.Completed += (obj)=> {
            if (obj.Status == AsyncOperationStatus.Succeeded)
                callBack(obj);
            else
            {
                Debug.LogWarning(keyName + "资源加载失败");
                if(resDic.ContainsKey(keyName))
                    resDic.Remove(keyName);
            }
        };
        resDic.Add(keyName, handle);
    }

    //释放资源的方法 
    public void Release<T>(string name)
    {
        //由于存在同名 不同类型资源的区分加载
        //所以我们通过名字和类型拼接作为 key
        string keyName = name + "_" + typeof(T).Name;
        if(resDic.ContainsKey(keyName))
        {
            //取出对象 移除资源 并且从字典里面移除
            AsyncOperationHandle<T> handle = (AsyncOperationHandle<T>)resDic[keyName];
            Addressables.Release(handle);
            resDic.Remove(keyName);
        }
    }

    //异步加载多个资源 或者 加载指定资源
    public void LoadAssetAsync<T>(Addressables.MergeMode mode, Action<T> callBack, params string[] keys)
    {
        //1.构建一个keyName  之后用于存入到字典中
        List<string> list = new List<string>(keys);
        string keyName = "";
        foreach (string key in list)
            keyName += key + "_";
        keyName += typeof(T).Name;
        //2.判断是否存在已经加载过的内容 
        //存在做什么
        AsyncOperationHandle<IList<T>> handle;
        if (resDic.ContainsKey(keyName))
        {
            handle = (AsyncOperationHandle<IList<T>>)resDic[keyName];
            //异步加载是否结束
            if(handle.IsDone)
            {
                foreach (T item in handle.Result)
                    callBack(item);
            }
            else
            {
                handle.Completed += (obj) =>
                {
                    //加载成功才调用外部传入的委托函数
                    if(obj.Status == AsyncOperationStatus.Succeeded)
                    {
                        foreach (T item in handle.Result)
                            callBack(item);
                    }
                };
            }
            return;
        }
        //不存在做什么
        handle = Addressables.LoadAssetsAsync<T>(list, callBack, mode);
        handle.Completed += (obj) =>
        {
            if(obj.Status == AsyncOperationStatus.Failed)
            {
                Debug.LogError("资源加载失败" + keyName);
                if (resDic.ContainsKey(keyName))
                    resDic.Remove(keyName);
            }
        };
        resDic.Add(keyName, handle);
    }

//这里可以实现一个可以获取handle的回调参数的方法,相比上面的方法外界可以控制的内容更多。
//但是调用一次需要填写的内容也更多。
    public void LoadAssetAsync<T>(Addressables.MergeMode mode, Action<AsyncOperationHandle<IList<T>>> callBack, params string[] keys)
    {

    }

    public void Releas<T>(params string[] keys)
    {
        //1.构建一个keyName  之后用于存入到字典中
        List<string> list = new List<string>(keys);
        string keyName = "";
        foreach (string key in list)
            keyName += key + "_";
        keyName += typeof(T).Name;
        
        if(resDic.ContainsKey(keyName))
        {
            //取出字典里面的对象
            AsyncOperationHandle<IList<T>> handle = (AsyncOperationHandle<IList<T>>)resDic[keyName];
            Addressables.Release(handle);
            resDic.Remove(keyName);
        }
    }


    //清空资源
    public void Clear()
    {
        resDic.Clear();
        AssetBundle.UnloadAllAssetBundles(true);
        Resources.UnloadUnusedAssets();
        GC.Collect();
    }
}

第3章: 配置相关

Profile 概述窗口配置

知识点一 概述配置用来干什么?

主要用于配置Addressable打包(构建)加载AB包时使用的一些变量

这些变量定义了

  1. 在哪里保存打包(构建)的AB包

  2. 运行时在哪里加载AB包

您可以添加自定义变量,以便在打包加载时使用

我们之后在设置 组中打包和加载路径相关时,都是使用这里面的变量

知识点二 打开Profiles窗口

方法一:Window > Asset Management > Addressables > Profiles

方法二:在AddressableAssetSettings中打开

方法三:在Addressables Groups窗口中打开

知识点三 Profiles窗口参数相关

知识点四 Profiles变量语法

所有的变量类型都是string字符串类型

你可以在其中填写一些固定的路径或值来决定路径

还可以使用两个语法指示符让原本的静态属性变成动态属性

[]:方括号,可以使用它包裹变量,在打包构建时会计算方括号包围的内容

比如

使用自己的变量[BuildTarget]

使用别的脚本中变量[UnityEditor.EditorUserBuildSettings.activeBuildTarget]

在打包构建时,会使用方括号内变量对应的字符串拼接到目录中

{}:大括号,可以使用它包裹变量,在运行时会计算大括号包围的内容

比如

使用别的脚本中变量{UnityEngine.AddressableAssets.Addressables.RuntimePath}

注意:方括号和大括号中使用的变量一定是静态变量或者属性。名称、类型、命名空间必须匹配

比如在运行时 UnityEditor编辑器命名空间下的内容是不能使用的

AddressableAssetSettings配置

知识点一 配置文件有哪些

我们在导入Addressables包之后 创建的那些就是配置文件

AddressableAssetsData文件夹下的内容都是本质为ScriptableObject的数据配置文件

我们可以在工程中对Addressables相关内容进行设置

他们会影响我们的打包方式等等相关内容

-AddressableAssetsData(可寻址资源数据)

  • AssetGroups(资源组)

当我们创建一个组就会多一些相关数据配置文件:

-AssetGroupTemplates(资源组模板,主要是对资源组的一些默认设置)

  • Packed Assets:打包资源数据配置

-DataBuilders(数据生成器,这些内容决定了在不同模式下,资源打包和使用的方式)

  • BuildScriptFastMode:构建脚本快速模式

  • BuildScriptPackedMode:构建脚本打包模式

  • BuildScriptPackedPlayMode:构建脚本打包播放模式

  • BuildScriptVirtualMode:构建脚本虚拟模式

-AddressableAssetSettings(可寻址资源设置)

-DefaultObject(默认对象)

知识点二 AddressableAssetSettings 参数讲解

AddressabAssetSettings翻译过来的意思就是 可寻址资源设置

该配置文件可以设置一些可寻址资源的一些公共设置

总结

AddressableAssetSettings 可寻址资源设置

就是对可寻址相关功能进行设置

对于我们来说较为重要的是

  1. 概述配置——决定了路径配置相关

  2. 诊断相关——决定了调试相关

  3. 目录相关——决定目录后缀等内容

  4. 内容更新相关——决定了更新相关方案

  5. 构建和编辑器模式脚本相关——决定了测试方案

  6. 资源组模板——决定了创建组时的配置模板

Packed Assets 打包资源配置

知识点一 打包资产配置的作用

Packed Assets 也是生成的一个配置文件。

Packed Assets 翻译过来的意思是 打包资产(资源)

它的作用是确定如何处理组中的资源

比如:你可以指定生成AB包的位置和包压缩相关的等等设置

每个组都有属于自己的Packed Assets可以单独设置,AssetGroupTemplates下自带一个默认的设置,其设置会被其他新创建出来的AssetGroupTemplates默认采用。

知识点二 打包资产参数讲解

知识点三 创建自定义的配置

在Project窗口右键或者点击+号

Create(创建)——>Addressables(可寻址)——>Group Templates(组模板)——>Blank Group Template(空白组模板)

总结

对于我们来说就是 关于组的设置参数

我们必须要掌握的就是可以为每一个组单独设置打包和加载的路径

远程加载还是本地加载,都在组中进行设置

Addressables Hosting 可寻址托管窗口

知识点一 Addressables Hosting可寻址托管窗口的作用

一般资源服务器需要将其搭建为http服务器

这样才能进行资源的上传和下载

而Unity为了简化本地测试的这一过程

提供了快捷搭建http服务器的工具

Addressables Hosting 窗口

通过它我们可以将我们的本机模拟为一台远端服务器来进行远端发布加载测试

可以帮助我们快速的进行远程打包下载的相关测试

简单理解就是把本机作为一台资源服务器

知识点二 打开可寻址托管窗口

方法一:Window > Asset Management > Addressables > Hosting

方法二:Addressables Groups窗口中 > Tools > Window > Hosting Services

知识点三 可寻址托管窗口参数

知识点四 注意事项

Addressable Hosting窗口创建的本地服务器有时候会失效

下载第三方工具 让本机变为一个http服务器 模拟远端加载

总结

如果我们要在本地模拟远端加载

  1. 使用Addressable Hosting窗口 创建本地托管将本机模拟为远端服务器

  2. 使用第三方的一些快捷搭建http服务器的工具 将本机作为http服务器 模拟为远端服务器

第4章: 资源打包(发布)加载

第1节: 资源打包加载(重要)

资源打包加载理论

知识点一 资源打包是指什么?

资源打包是指,将可寻址资源打包到AssetBundle资源绑定包中

以前我们学习AssetBundle时需要我们自己写代码或者通过工具进行打包

现在Addressable将这个过程自动化了

可寻址资源中资源一般以组为单位进行打包

最终的AB包数量,都是基于资源组决定的

打包好的AB包,我们可以自定义如何使用它们

  1. 发布游戏时所有AB包作为原始资源一起打包出去(这样做的目的仅仅是单机游戏为了减小游戏包体)

  2. 将AB包放到远端服务器,发布游戏时只打包必备资源(这样做的目的是不仅可以减小包体,还可以进行热更新)

知识点二 资源打包的模式

之前在讲解Packed Assets配置相关时

有一个打包模式的字段Bundle Mode

我们有三种打包的方式

  • Pack Together:创建包含组中所有资产的单个包

  • Pack Separately:为组中的每个类型的资源创建一个包。如精灵图片中的精灵图片被包装在一起

  • Pack Together by Label:为共享相同标签组合的资产创建一个包

一般情况下,我们都使用第一种模式,按组来进行打包,所以我们在整理资源时要尽量合理

知识点三 资源打包的注意事项

  1. 场景资源始终有自己单独的包

当一个可寻址包中有场景资源和普通资源时,场景资源和其它资源始终会被分开打包

也就是说,该组会生成两个包,一个场景的,一个其它资源的

  1. 资源依赖的注意事项

如果资源a和资源b都使用了资源c,但是资源a和b是可寻址资源但不在一个组中,而c不是可寻址资源

那么这时资源c分别会被打进a和b的包,相当于这时的c就被重复利用了,浪费了空间

较好的解决方案是:将c也作为可寻址资源, a-A包 b-B包 c-C包

这时c不会被打进A、B包,A、B包只会依赖于C,而Addressable会自动帮助我们处理依赖问题

注意:在使用图集时,尤为要注意这个问题,当不同包中内容使用同一个图集中图片时,

建议大家将图集也作为可寻址资源,并且有专门的一个图集包

让其它使用图集中图片的对象会对该图集包产生依赖

  1. 合理安排可寻址资源分组

同类型,同作用放一起

比如:角色组、怪物组、武器组、衣服组、登录UI组、装备UI组、音效组、可变贴图组、图集组等等

  1. 关于包的数量(分组的数量)

要根据实际情况来对资源进行布局

包(分组)过多、包(分组)过大都不太好

要根据自己的实际情况进行组的安排

资源打包加载(本地和远端)

导入

需要强调的是,远端和本地资源是可以并存的,但是目录只需要一个,通常我们会放在远端以方便未来的更新。

知识点一 编辑器中资源加载的几种方式

Use Asset Database(fastest):

使用资源数据库(最快的)

不用打AB包,直接本地加载资源,主要用于开发功能时

Simulate Groups(advanced):

模拟组(后期)

不用打AB包

通过ResourceManager从资产数据库加载资产,就像通过AB包加载一样

通过引入时间延迟,模拟远程AB包的下载速度和本地AB包加载速度

在开发阶段可以使用这个模式来进行资源加载

Use Existing Build(requires built groups):

正儿八经的加载AB包资源

必须打AB包后使用

会从AB包中加载资源

知识点二 本地资源发布

本地发布

所有组的加载和发布都选择本地路径(新版已经规定必须全选远端或者本地了)

LocalBuildPath-打包路径

LocalLoadPath-加载路径

注意:使用默认设置,当发布应用程序时,会自动帮我们将AB包放入StreamingAssets文件夹中

如果修改了路径,我们需要自己将内容放入StreamingAssets文件夹中

所以不建议大家修改 默认的本地构建和加载路径 ,因为如果你修改了 就需要自己手动的去把AB包相关内容移动到StreamingAssets文件夹中

知识点三 模拟远端发布资源

第一步:将本机模拟为一台资源服务器,通过Unity自带工具或者第三方工具

第二步:设置远端LoadPath路径http://目标ip:目标Port,buildPath如果是本机模拟可以保持默认,会给你把AB包打到ServerData文件夹,否则也设置相同,但是要给予本机上传目标服务器资源的权限

第三部:打包,反正最终要保证目录和远端资源都在指定的远端路径文件夹内。

知识点四 实际上的远端发布资源

在知识点三的基础上

  1. 在远端的电脑上搭建Http服务器

  2. 将打包出来的资源上传到对应服务器上

总结

一个项目中的资源到底是本地还是远端,根据实际情况而定

  1. 对于需要热更新的网络游戏。默认基础资源作为本地资源,大部分资源作为远端资源

  1. 对于不需要热更新的单机游戏,所有的资源都是本地资源

具体采用的打包策略根据实际情况来定

第2节: 资源更新(重要)

资源更新

知识点一 资源更新指的是什么?

当项目正式发布后,对于远程加载的资源

我们可以通过改变资源服务器上的AB包内容来达到更新游戏的目的

Addressables会自动帮助我们判断哪些资源更新了,并加载最新的内容

知识点二 内容更新限制参数回顾

在组设置中有一个内容更新限制的设置

Content Update Restriction

  1. Can Change Post Release:

可以改变发行后内容,该模式不移动任何资源,如果包中的任何资源发生了更改,则重新构建整个包

  1. Cannot Change Post Release:

无法改变发布后内容,如果包中任何资源已经改变,

则[检查内容更新限制]工具会将其移动到为更新创建的新组中。

在进行更新构建时,从这个新组创建的AssetBundles中的资产将覆盖现有包中的版本。

下面我们会详细介绍这两个的区别

知识点三 整包更新

组设置为 Can Change Post Release

整包更新指,某一个分组的资源发生变化后

我们需要将其整体进行打包

这种方式比较适用于大范围资源更新时使用

坏处是,玩家需要下载的内容较大,比较耗时耗流量

注意:Unity自带的 资源服务器模拟工具有问题 有的时候明明开启了服务 但是加载不成功

这里建议使用第三方工具来搭建我们的模拟本地资源服务器

实际操作

当我们完成对新版本的项目的AB包处理后,更新不要直接构建新包,而是在Groups界面点击Build—>Update a Previous Build

随后会让我们选择文件,我们选择项目文件Assets\AddressableAssetsData\Windows下的addressables_content_state.bin文件后即可进行整包更新。

知识点四 局部更新

组设置为 Cannot Change Post Release

s局部更新指,当组中有资源发生变化时

我们可以单为发生变化的内容生成AB包

之后使用该资源时,Addressables会自动加载最新的内容

它相对整包更新来说,更节约时间和流量

实际操作

我们照常先完成对新版本的项目的AB包处理。

随后打开Groups->Tools->Check for Content Update Restrictions,同样选择addressables_content_state.bin文件,随后会弹出一个Unity自动帮我们分辨的新组窗口,点击确定后你会发现新的组包,这就是Unity帮我们挑出来的新内容被分组了。

随后和整包操作一样,直接点击Update a Previous Build即可。

注意

不用担心已经发布文件后,开发端的某一组的更新类型可不可以改变,实际上是没问题的,因为日志已经记录了我们的更新类型,即使改变了客户端也可以知道并进行操作。

总结

远程资源包时整包更新还是局部更新

取决于Content Update Restriction内容更新限制参数

可以根据自己项目的实际情

第5章: 其它重要内容(重要)

第1节: 资源加载相关 补充知识

根据资源定位信息加载资源

知识点一 回顾学过的加载可寻址资源的方式

  1. 通过标识类进行加载(指定资源加载)

  2. 通过资源名或标签名加载单个资源(动态加载)

Addressables.LoadAssetAsync<GameObject>("Cube")

  1. 通过资源名或标签名或两者组合加载多个资源(动态加载)

Addressables.LoadAssetsAsync<GameObject>(new List<string>() { "Cube", "SD" }, (obj) => { }, Addressables.MergeMode.Intersection);

知识点二 加载资源时Addressables帮助我们做了哪些事情?

  1. 查找指定键的资源位置

  2. 收集依赖项列表

  3. 下载所需的所有远程AB包

  4. 将AB包加载到内存中

  5. 设置Result资源对象的值

  6. 更新Status状态变量参数并且调用完成事件Completed

如果加载成功Status状态为成功,并且可以从Result中得到内容

如果加载失败除了Status状态为失败外

如果我们启用了 Log Runtime Exceptions选项 会在Console窗口打印信息

而我们这节课,下面使用的API,可以做到只先执行前两步,而为我们提高更多的信息。

比如我们可以获取到目标资源的资源名和资源路径等信息,进行选择后,再进行真正的下载。

知识点三 根据名字或者标签获取 资源定位信息 加载资源

参数一:资源名或者标签名

参数二:资源类型

AsyncOperationHandle<IList<IResourceLocation>> handle = Addressables.LoadResourceLocationsAsync("Cube", typeof(GameObject));
handle.Completed += (obj) =>
{
    if(obj.Status == AsyncOperationStatus.Succeeded)
    {
        foreach (var item in obj.Result)
        {
            //我们可以利用定位信息 再去加载资源
            //print(item.PrimaryKey);
            Addressables.LoadAssetAsync<GameObject>(item).Completed += (obj) =>
            {
                Instantiate(obj.Result);
            };
        }
    }
    else
    {
        Addressables.Release(handle);
    }
};

知识点四 根据名字标签组合信息获取 资源定位信息 加载资源

参数一:资源名和标签名的组合

参数二:合并模式

参数三:资源类型

csharp

AsyncOperationHandle<IList<IResourceLocation>> handle2 = Addressables.LoadResourceLocationsAsync(new List<string>() { "Cube", "Sphere", "SD" }, Addressables.MergeMode.Union, typeof(Object));
handle2.Completed += (obj) => { 
    if(obj.Status == AsyncOperationStatus.Succeeded)
    {
        //资源定位信息加载成功
        foreach (var item in obj.Result)
        {
            //使用定位信息来加载资源
            //我们可以利用定位信息 再去加载资源
            print("******");
            print(item.PrimaryKey);
            print(item.InternalId);
            print(item.ResourceType.Name);

            Addressables.LoadAssetAsync<Object>(item).Completed += (obj) =>
            {
                //Instantiate(obj.Result);
            };
        }
    }
    else
    {
        Addressables.Release(handle);
    }
};

知识点五 根据资源定位信息加载资源的注意事项

  1. 资源信息当中提供了一些额外信息

  • PrimaryKey:资源主键(资源名)

  • InternalId:资源内部ID(资源路径)

  • ResourceType:资源类型(Type可以获取资源类型名)

我们可以利用这些信息处理一些特殊需求

比如加载多个不同类型资源时 可以通过他们进行判断再分别进行处理

  1. 根据资源定位信息加载资源并不会加大我们加载开销只是分部完成加载了而已,按照情况选择具体使用哪种方式即可

异步加载的几种使用方式

导入

我们目前广泛使用的异步加载主要是使用事件监听Completed 来实现,但是实际上Addressables为我们提供了其他的异步加载方法:

它继承于IEnumerator所以可以使用协程,它还含有Task变量,所以它也可以执行async和await 的写法。

知识点一 回顾目前动态异步加载的使用方式

handle = Addressables.LoadAssetAsync<GameObject>("Cube");
//通过事件监听的方式 结束时使用资源
handle.Completed += (obj) =>
{
    if (handle.Status == AsyncOperationStatus.Succeeded)
    {
        print("事件创建对象");
        Instantiate(obj.Result);
    }

};

知识点二 3种使用异步加载资源的方式

1.事件监听(目前学习过的)

2.协同程序

3.异步函数(async和await )

知识点三 通过协程使用异步加载

StartCoroutine(LoadAsset());
IEnumerator LoadAsset()
{
    handle = Addressables.LoadAssetAsync<GameObject>("Cube");
    //一定是没有加载成功 再去 yield return
    if(!handle.IsDone)
        yield return handle;

    //加载成功 那么久可以使用了
    if (handle.Status == AsyncOperationStatus.Succeeded)
    {
        print("协同程序创建对象");
        Instantiate(handle.Result);
    }
    else
        Addressables.Release(handle);
}

知识点四 通过异步函数async和await加载

注意:WebGL平台不支持异步函数语法

单任务等待

csharp

Load();
async void Load()
{
    handle = Addressables.LoadAssetAsync<GameObject>("Cube");
    
    //单任务等待
    await handle.Task;

}
多任务等待

csharp

Load();
async void Load()
{
    handle = Addressables.LoadAssetAsync<GameObject>("Cube");

    AsyncOperationHandle<GameObject> handle2 = Addressables.LoadAssetAsync<GameObject>("Sphere");
    

    //多任务等待
    await Task.WhenAll(handle.Task, handle2.Task);

    print("异步函数的形式加载的资源");
    Instantiate(handle.Result);
    Instantiate(handle2.Result);
}

关于AsyncOperationHandle 知识点

导入

本节主要补充一些AsyncOperationHandle 里面没有提到的比较有用的写法。

知识点一 获取加载进度

startCoroutine(LoadAsset());

IEnumerator LoadAsset()
    {
        AsyncOperationHandle<GameObject> handle = Addressables.LoadAssetAsync<GameObject>("Cube");

        //if (!handle.IsDone)
        //    yield return handle;

        //注意:如果该资源相关的AB包 已经加载过了 那么 只会打印0
        while (!handle.IsDone)
        {
            DownloadStatus info = handle.GetDownloadStatus();
            //进度
            print(info.Percent);
            //字节加载进度 代表 AB包 加载了多少
            //当前下载了多少内容 /  总体有多少内容 单位是字节数
            print(info.DownloadedBytes + "/" + info.TotalBytes);
            yield return 0;
        }

        if (handle.Status == AsyncOperationStatus.Succeeded)
        {
            Instantiate(handle.Result);
        }
        else
            Addressables.Release(handle);
    }

知识点二 强制同步加载资源

//如果执行了WaitForCompletion 那么会卡主主线程 一定要当资源加载结束后
//才会继续往下执行
print("1");
handle.WaitForCompletion();
print("2");
print(handle.Result.name);

注意:

Unity2020.1版本或者之前,执行该句代码不仅会等待该资源

他会等待所有没有加载完成的异步加载加载完后才会继续往下执行

Unity2020.2版本或以上版本,在加载已经下载的资源时性能影响会好一些

所以,总体来说已经不建议大家使用这种方式加载资源,还不如过场的时候用进度条掩盖加载过程。

知识点三 无类型句柄转换

AsyncOperationHandle<Texture2D> handle = Addressables.LoadAssetAsync<Texture2D>("Cube");
//有类型转无类型的,会自动转化
AsyncOperationHandle temp = handle;
//把无类型句柄 转换为 有类型的泛型对象
handle = temp.Convert<Texture2D>();

上文的写法恰好可以解决我们之前管理类的协程无法转化handel的问题。

所以,我们将修改后的管理类写在下小节

修改后的管理类

sing System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

public class AddressablesMgr
{
    private static AddressablesMgr instance = new AddressablesMgr();
    public static AddressablesMgr Instance => instance;

    //有一个容器 帮助我们存储 异步加载的返回值
    public Dictionary<string, AsyncOperationHandle> resDic = new Dictionary<string, AsyncOperationHandle>();

    private AddressablesMgr() { }

    //异步加载资源的方法
    public void LoadAssetAsync<T>(string name, Action<AsyncOperationHandle<T>> callBack)
    {
        //由于存在同名 不同类型资源的区分加载
        //所以我们通过名字和类型拼接作为 key
        string keyName = name + "_" + typeof(T).Name;
        AsyncOperationHandle<T> handle;
        //如果已经加载过该资源
        if (resDic.ContainsKey(keyName))
        {
            //获取异步加载返回的操作内容
            handle = resDic[keyName].Convert<T>();

            //判断 这个异步加载是否结束
            if(handle.IsDone)
            {
                //如果成功 就不需要异步了 直接相当于同步调用了 这个委托函数 传入对应的返回值
                callBack(handle);
            }
            //还没有加载完成
            else
            {
                //如果这个时候 还没有异步加载完成 那么我们只需要 告诉它 完成时做什么就行了
                handle.Completed += (obj) => {
                    if (obj.Status == AsyncOperationStatus.Succeeded)
                        callBack(obj);
                };
            }
            return;
        }
        
        //如果没有加载过该资源
        //直接进行异步加载 并且记录
        handle = Addressables.LoadAssetAsync<T>(name);
        handle.Completed += (obj)=> {
            if (obj.Status == AsyncOperationStatus.Succeeded)
                callBack(obj);
            else
            {
                Debug.LogWarning(keyName + "资源加载失败");
                if(resDic.ContainsKey(keyName))
                    resDic.Remove(keyName);
            }
        };
        resDic.Add(keyName, handle);
    }

    //释放资源的方法 
    public void Release<T>(string name)
    {
        //由于存在同名 不同类型资源的区分加载
        //所以我们通过名字和类型拼接作为 key
        string keyName = name + "_" + typeof(T).Name;
        if(resDic.ContainsKey(keyName))
        {
            //取出对象 移除资源 并且从字典里面移除
            AsyncOperationHandle<T> handle = resDic[keyName].Convert<T>();
            Addressables.Release(handle);
            resDic.Remove(keyName);
        }
    }

    //异步加载多个资源 或者 加载指定资源
    public void LoadAssetAsync<T>(Addressables.MergeMode mode, Action<T> callBack, params string[] keys)
    {
        //1.构建一个keyName  之后用于存入到字典中
        List<string> list = new List<string>(keys);
        string keyName = "";
        foreach (string key in list)
            keyName += key + "_";
        keyName += typeof(T).Name;
        //2.判断是否存在已经加载过的内容 
        //存在做什么
        AsyncOperationHandle<IList<T>> handle;
        if (resDic.ContainsKey(keyName))
        {
            handle = resDic[keyName].Convert<IList<T>>();
            //异步加载是否结束
            if(handle.IsDone)
            {
                foreach (T item in handle.Result)
                    callBack(item);
            }
            else
            {
                handle.Completed += (obj) =>
                {
                    //加载成功才调用外部传入的委托函数
                    if(obj.Status == AsyncOperationStatus.Succeeded)
                    {
                        foreach (T item in handle.Result)
                            callBack(item);
                    }
                };
            }
            return;
        }
        //不存在做什么
        handle = Addressables.LoadAssetsAsync<T>(list, callBack, mode);
        handle.Completed += (obj) =>
        {
            if(obj.Status == AsyncOperationStatus.Failed)
            {
                Debug.LogError("资源加载失败" + keyName);
                if (resDic.ContainsKey(keyName))
                    resDic.Remove(keyName);
            }
        };
        resDic.Add(keyName, handle);
    }

    public void LoadAssetAsync<T>(Addressables.MergeMode mode, Action<AsyncOperationHandle<IList<T>>> callBack, params string[] keys)
    {

    }

    public void Releas<T>(params string[] keys)
    {
        //1.构建一个keyName  之后用于存入到字典中
        List<string> list = new List<string>(keys);
        string keyName = "";
        foreach (string key in list)
            keyName += key + "_";
        keyName += typeof(T).Name;
        
        if(resDic.ContainsKey(keyName))
        {
            //取出字典里面的对象
            AsyncOperationHandle<IList<T>> handle = resDic[keyName].Convert<IList<T>>();
            Addressables.Release(handle);
            resDic.Remove(keyName);
        }
    }


    //清空资源
    public void Clear()
    {
        foreach (var item in resDic.Values)
        {
            Addressables.Release(item);
        }
        resDic.Clear();
        AssetBundle.UnloadAllAssetBundles(true);
        Resources.UnloadUnusedAssets();
        GC.Collect();
    }
}

但是还残留一个不完美的地方,那就是它并没有实现引用计数机制,这也是我们下一节课需要讲解的内容。

自定义更新目录和下载AB包

知识点一 目录的作用

目录文件的本质是Json文件和一个Hash文件

其中记录的主要内容有

Json文件中记录的是:

  1. 加载AB包、图集、资源、场景、实例化对象所用的脚本(会通过反射去加载他们来使用)

  2. AB包中所有资源类型对应的类(会通过反射去加载他们来使用)

  3. AB包对应路径

  4. 资源的path名

等等

Hash文件中记录的是:

目录文件对应hash码(每一个文件都有一个唯一码,用来判断文件是否变化)

更新时本地的文件hash码会和远端目录的hash码进行对比

如果发现不一样就会更新目录文件

当我们使用远端发布内容时,在资源服务器也会有一个目录文件

Addressables会在运行时自动管理目录

如果远端目录发生变化了(他会通过hash文件里面存储的数据判断是否是新目录)

它会自动下载新版本并将其加载到内存中

知识点二 手动更新目录

  1. 如果要手动更新目录 需要先在设置中关闭自动更新,我们需要去AddressableAssetSettings配置文件勾选Disable Catalog Update On Startup选项关闭自动更新。

  2. 自动检查所有目录是否有更新,并更新目录API

Addressables.UpdateCatalogs().Completed += (obj) =>
{
    Addressables.Release(obj);
};

3.获取目录列表,再更新目录

// 参数 bool 决定加载结束后 会不会自动释放异步加载的句柄
 Addressables.CheckForCatalogUpdates(true).Completed += (obj) =>
 {
     //如果列表里面的内容大于0 证明有可以更新的目录
     if(obj.Result.Count > 0)
     {
         //根据目录列表更新目录
         Addressables.UpdateCatalogs(obj.Result, true).Completed += (handle) =>
         {
             //如果更新完毕 记得释放资源
             //Addressables.Release(handle);
             //Addressables.Release(obj);
         };
     }
 };

知识点三 预加载包

下面代码就是在大部分手游预加载包界面常常会执行的流程:

值得一提的是,GetDownloadSizeAsync方法仅会获取本地未缓存的AB包。

//建议通过协程来加载
StartCoroutine(LoadAsset());
IEnumerator LoadAsset()
{
    //1.首先获取下载包的大小
    //可以传资源名、标签名、或者两者的组合
    AsyncOperationHandle<long> handleSize = Addressables.GetDownloadSizeAsync(new List<string>() { "Cube", "Sphere", "SD" });
    yield return handleSize;
    //2.预加载
    if(handleSize.Result > 0)
    {
        //这样就可以异步加载 所有依赖的AB包相关内容了
        AsyncOperationHandle handle = Addressables.DownloadDependenciesAsync(new List<string>() { "Cube", "Sphere", "SD" }, Addressables.MergeMode.Union);
        while(!handle.IsDone)
        {
            //3.加载进度
            DownloadStatus info = handle.GetDownloadStatus();
            print(info.Percent);
            print(info.DownloadedBytes + "/" + info.TotalBytes);
            yield return 0;
        }

        Addressables.Release(handle);
    }

}

总结

一般我们会在

刚进入游戏时 或者 切换场景时 显示一个Loading界面

我们可以在此时提前加载包,这样之后在使用资源就不会出现明显的异步加载延迟感

目录更新 我们一般都会放在进入游戏开始游戏之前

引用计数规则

知识点一 什么是引用计数规则?

当我们通过加载使用可寻址资源时

Addressables会在内部帮助我们进行引用计数

使用资源时,引用计数+1

释放资源时,引用计数-1

当可寻址资源的引用为0时,就可以卸载它了

为了避免内存泄露(不需要使用的内容残留在内存中)

我们要保证加载资源和卸载资源是配对使用的

注意:释放的资源不一定立即从内存中卸载

在卸载资源所属的AB包之前,不会释放资源使用的内存

(比如自己所在的AB包 被别人使用时,这时AB包不会被卸载,所以自己还在内存中)

我们可以使用Resources.UnloadUnusedAssets卸载资源(建议在切换场景时调用)

AB包也有自己的引用计数,其实这个引用计数本质上就是当前相同handle的存在数量(Addressables把它也视为可寻址资源)

从AB包中加载资源时,引用计数+1

从AB包中卸载资源时,引用计数-1

当AB包引用计数为0时,意味着不再使用了,这时整个AB包会从内存中卸载

总结:Addressables内部会通过引用计数帮助我们管理内存

我们只需要保证 加载和卸载资源配对使用即可

通俗的来说:所以所谓的计数,可以认为就是场上引用某个ab包的handle数量,当AB包关联的handle全都不存在的时候,Untiy会自动卸载这个包本身。

知识点二 举例说明引用计数

我们创建两个一样的资源,但是注意他们必须用不同的handle句柄。

然后一个一个的释放他们的资源句柄

当释放第一遍的时候,两个资源没有变化。

但是释放第二遍的时候,你会发现纹理丢失。

因为虽然预制体可以独立于场景,但是预制体中的纹理本质上也被打包到了所在组里,而所在的组(AB包)已经被判断引用计数为0,被卸载了。

由此我们可以观察到潜在的引用计数机制,尽管我们无法直接靠变量获取。

注意:使用第三种模式加载资源(从AB包中加载)

知识点三 回顾之前写的资源管理器

我们之前写的资源管理器

通过自己管理异步加载的返回句柄会让系统自带的引用计数功能不起作用

因为我们不停的在复用一个句柄

所以下一节,我们可以完成一个最终版本的管理类

Addressables管理类(最终版)

引入

此管理类在上一个管理类的基础上,完善了引用计数机制和dic的句柄value。

但是实际上还有一些可以定制化的内容:你完全可以假设handle是全局唯一的,而不希望使用计数机制

毕竟引用计数规则的目的我的理解就是为了确保当相同包的handle被大量创建的情况下,有关的AB包会在已经不被引用的时候及时自动释放内存。目的是无用的AB包及时使删除,而不是handle的存在数量本身。

但是在管理类的字典的管理下,我们已经可以时刻遍历和掌控大部分的句柄了,我们释放某个AB包所引用的全部句柄的时候,这个AB包自然会正确的触发删除,并不影响引用计数规则的设计初衷。

反而为单个handle加上引用计数,会导致想释放句柄的时候反而得多放几次。

代码


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;

//可寻址资源 信息
public class AddressablesInfo
{
    //记录 异步操作句柄
    public AsyncOperationHandle handle;
    //记录 引用计数
    public uint count;

    public AddressablesInfo(AsyncOperationHandle handle)
    {
        this.handle = handle;
        count += 1;
    }
}

public class AddressablesMgr
{
    private static AddressablesMgr instance = new AddressablesMgr();
    public static AddressablesMgr Instance => instance;

    //有一个容器 帮助我们存储 异步加载的返回值
    public Dictionary<string, AddressablesInfo> resDic = new Dictionary<string, AddressablesInfo>();

    private AddressablesMgr() { }

    //异步加载资源的方法
    public void LoadAssetAsync<T>(string name, Action<AsyncOperationHandle<T>> callBack)
    {
        //由于存在同名 不同类型资源的区分加载
        //所以我们通过名字和类型拼接作为 key
        string keyName = name + "_" + typeof(T).Name;
        AsyncOperationHandle<T> handle;
        //如果已经加载过该资源
        if (resDic.ContainsKey(keyName))
        {
            //获取异步加载返回的操作内容
            handle = resDic[keyName].handle.Convert<T>();
            //要使用资源了 那么引用计数+1
            resDic[keyName].count += 1;
            //判断 这个异步加载是否结束
            if(handle.IsDone)
            {
                //如果成功 就不需要异步了 直接相当于同步调用了 这个委托函数 传入对应的返回值
                callBack(handle);
            }
            //还没有加载完成
            else
            {
                //如果这个时候 还没有异步加载完成 那么我们只需要 告诉它 完成时做什么就行了
                handle.Completed += (obj) => {
                    if (obj.Status == AsyncOperationStatus.Succeeded)
                        callBack(obj);
                };
            }
            return;
        }
        
        //如果没有加载过该资源
        //直接进行异步加载 并且记录
        handle = Addressables.LoadAssetAsync<T>(name);
        handle.Completed += (obj)=> {
            if (obj.Status == AsyncOperationStatus.Succeeded)
                callBack(obj);
            else
            {
                Debug.LogWarning(keyName + "资源加载失败");
                if(resDic.ContainsKey(keyName))
                    resDic.Remove(keyName);
            }
        };
        AddressablesInfo info = new AddressablesInfo(handle);
        resDic.Add(keyName, info);
    }

    //释放资源的方法 
    public void Release<T>(string name)
    {
        //由于存在同名 不同类型资源的区分加载
        //所以我们通过名字和类型拼接作为 key
        string keyName = name + "_" + typeof(T).Name;
        if(resDic.ContainsKey(keyName))
        {
            //释放时 引用计数-1
            resDic[keyName].count -= 1;
            //如果引用计数为0  才真正的释放
            if(resDic[keyName].count == 0)
            {
                //取出对象 移除资源 并且从字典里面移除
                AsyncOperationHandle<T> handle = resDic[keyName].handle.Convert<T>();
                Addressables.Release(handle);
                resDic.Remove(keyName);
            }
        }
    }

    //异步加载多个资源 或者 加载指定资源
    public void LoadAssetAsync<T>(Addressables.MergeMode mode, Action<T> callBack, params string[] keys)
    {
        //1.构建一个keyName  之后用于存入到字典中
        List<string> list = new List<string>(keys);
        string keyName = "";
        foreach (string key in list)
            keyName += key + "_";
        keyName += typeof(T).Name;
        //2.判断是否存在已经加载过的内容 
        //存在做什么
        AsyncOperationHandle<IList<T>> handle;
        if (resDic.ContainsKey(keyName))
        {
            handle = resDic[keyName].handle.Convert<IList<T>>();
            //要使用资源了 那么引用计数+1
            resDic[keyName].count += 1;
            //异步加载是否结束
            if (handle.IsDone)
            {
                foreach (T item in handle.Result)
                    callBack(item);
            }
            else
            {
                handle.Completed += (obj) =>
                {
                    //加载成功才调用外部传入的委托函数
                    if(obj.Status == AsyncOperationStatus.Succeeded)
                    {
                        foreach (T item in handle.Result)
                            callBack(item);
                    }
                };
            }
            return;
        }
        //不存在做什么
        handle = Addressables.LoadAssetsAsync<T>(list, callBack, mode);
        handle.Completed += (obj) =>
        {
            if(obj.Status == AsyncOperationStatus.Failed)
            {
                Debug.LogError("资源加载失败" + keyName);
                if (resDic.ContainsKey(keyName))
                    resDic.Remove(keyName);
            }
        };
        AddressablesInfo info = new AddressablesInfo(handle);
        resDic.Add(keyName, info);
    }

    public void LoadAssetAsync<T>(Addressables.MergeMode mode, Action<AsyncOperationHandle<IList<T>>> callBack, params string[] keys)
    {

    }

    public void Release<T>(params string[] keys)
    {
        //1.构建一个keyName  之后用于存入到字典中
        List<string> list = new List<string>(keys);
        string keyName = "";
        foreach (string key in list)
            keyName += key + "_";
        keyName += typeof(T).Name;
        
        if(resDic.ContainsKey(keyName))
        {
            resDic[keyName].count -= 1;
            if(resDic[keyName].count == 0)
            {
                //取出字典里面的对象
                AsyncOperationHandle<IList<T>> handle = resDic[keyName].handle.Convert<IList<T>>();
                Addressables.Release(handle);
                resDic.Remove(keyName);
            }
            
        }
    }

    //清空资源
    public void Clear()
    {
        foreach (var item in resDic.Values)
        {
            Addressables.Release(item.handle);
        }
        resDic.Clear();
        AssetBundle.UnloadAllAssetBundles(true);
        Resources.UnloadUnusedAssets();
        GC.Collect();
    }
}

第2节: 窗口相关

事件查看窗口(Addresssables Event Viewer)

知识点一 事件查看窗口用来做什么?

使用可寻址事件查看窗口可以监视可寻址资源的资源内存管理

该窗口

  1. 显示应用程序何时加载和卸载资源

  2. 显示所有可寻址系统操作的引用计数

  3. 显示应用程序帧率和分配的内存总量近似图

我们可以通过它来检查可寻址资源对性能的影响

并检查没有释放的资源

知识点二 打开事件查看窗口

注意:使用事件查看窗口的前提要打开AddressablesAssetSettings配置文件中的事件发送开关Send Profiler Events

窗口有两种打开方式

  1. Window > Asset Management > Addressables > Event Viewer

  2. Addressabeles Groups > Window > Event Viewer

知识点三 事件查看窗口使用

左上角:

Clear Event:清楚所有记录的帧,会清空窗口中所有内容

Unhide All Hidden Events:显示你隐藏的所有事件内容(当我们右键一个内容隐藏后才会显示该选项)

右上键:

Frame:显示当前所在帧数

左按钮和右按钮:在记录的帧中前后切换查看信息

Current:选中当前帧

中央部分:

FPS:应用的帧率

MonoHeap:正在使用的托管堆内存量

Event Counts:事件计数,某一帧中发生的可寻址事件的数量

Instantiation Counts:实例化计数,某一帧中Addressables.InstantiateAsync的调用数量

线性图标:显示统计的什么时候加载释放资源的信息

Event 相关:显示当前帧中发生的可寻址操作的事件

总结

事件查看窗口对于我们来说很有用

我们可以通过它来排查内存泄露相关的信息

比如场景中对象都被移除了,但是事件查看窗口中还有AB引用相关的信息,那证明存在内存泄露

可以排查加载和释放是否没有配对使用

分析窗口

知识点一 分析窗口有什么作用?

分析窗口是一种收集项目可寻址布局信息的工具

它是一种信息工具,可以让我们对可寻址文件布局做出更明智的决定

知识点二 打开分析窗口

两种打开方式

  1. Window > Asset Management > Addressables > Analyze

  2. Addressabeles Groups > Window > Analyze

知识点三 使用分析窗口

上方的三个按钮

  1. Analyze Selected Rules:分析选定的规则

  2. Clear Selected Rules:清除选定规则

  3. Fix Selected Rules:修复选定规则

下方的内容

Analyze Rules:分析规则

-Fixable Rules:可修复的规则(提供了分析和修复两种功能的规则出现在这里)

--Check Duplicate Bundle Dependencies:检查重复的AB包依赖项

主要检测处理的问题:

比如资源a和b,都使用了材质c,a和b是可寻址资源,c不是可寻址资源

a,b分别在两个AB包中,那么这时两个AB包中都会有资源c,这时就可以通过该规则排查出该问题

那么这时我们可以选择自己重新处理后打包,也可以选择修复功能

建议使用自己处理问题,因为某些特殊情况它也会认为有问题

比如,一个FBX中有多个网格信息a,b,这时我们分别把网格a放入包A,网格b放入包B

它也会认为A和B有重复资源,但其实他们并没有重复

Unfixable Rules:不可修复的规则(对于只有分析功能,没有修复功能的规则在这里出现)

-Check Resources to Addressable Duplicate Dependencies:检查可寻址重复依赖项的资源

主要检测的问题:

同时出现在可寻址资源和应用程序构建的资源中

比如一个资源A,它是可寻址资源

但是它同时在Resources、StreamingAssets等特殊文件夹中,最终会被打包出去

-Check Scene to Addressable Duplicate Dependencies:检查场景到可寻址重复依赖项

主要检测的问题:

同时出现在可寻址资源和某一个场景中

比如一个资源A,它是可寻址资源但是它有直接出现在某一个场景中

这时你需要自己根据需求进行处理

-Bundle Layout Preview:AB包布局预览

1.当我们选中一个规则后,可以点击上方的 分析选定规则按钮 进行分析

分析完成后,会在下方看到对应的信息

2.我们也可以点击清除选定的规则可以清除上一次信息

3.对于提供了修复操作的规则,我们可以点击修复选定规则,来修复问题

补充

我们也可以自己定义分析规则

但是这种高级方式我们不是特别常用

你可以参考官方文档:

https://docs.unity.cn/Packages/com.unity.addressables@1.18/manual/AnalyzeTool.html

Extending Analyze拓展分析相关的内容

了解即可

总结

分析窗口对于我们来说也很有用

当我们打包后,我们可以通过分析窗口工具

分析AB包中的资源分布是否合理

根据分析结果自己处理一些潜在问题

构建布局报告

知识点一 构建布局报告有什么作用?

构建布局报告提供了有关可寻址资源的构建打包的详细信息和统计信息

包括

  1. AB包的描述

  2. 每个资源和AB包的大小

  3. 解析作为依赖项隐式包含在AB包中的不可寻址资源

  4. AB包的依赖关系

我们可以通过查看报告文件获取这些信息

知识点二 如何查看构建布局报告?

  1. 启用调试构建布局功能

Edit > Preferences > Addressables

启用Debug Build Layout

  1. 只要我们构建打包可寻址资源后

就可以在Library/com.unity.addressables/文件夹中找到buildlayout.txt文件

知识点三 构建布局报告的内容

内容中主要包含:

  1. 摘要信息(包括AB包数量、大小等等)

  2. 每组相关信息(哪些资源,几个包,包大小等等)

  3. 依赖相关信息

第6章: 总结

常见问题总结

知识点一 用多包策略还是大包策略?

1.AB包太多(太小)的问题

1-1:每个包都会有内存开销,如果加载过多的包可能会带来更多的内存消耗

1-2:并发下载的问题,如果包小而多,以为着下载包时可能需要更多的时间

1-3:目录文件会因为过多的包而变大,因为它要记录更多的包信息

1-4:复用资源的可能性更大,比如多个包使用同样一个资源,但是该资源不是寻址资源,那么在每个包中都会有该资源

2.AB包太少(太大)的问题

1-1:过大的包如果下载失败,下次会重新下载,因为使用UnityWebRequest下载包时不会恢复失败的下载

比如100MB的包,下了50MB,玩家中断下载了,下次又得重新下

1-2:能单独加载,但是不能单独卸载,更大的包意味着包中有更多资源,比如加载了1个大包中100个资源

但是现在用完了99个,还剩一个再用,即使99都卸载了,但是由于引用计数这个大包也不会卸载

就会造成内存的浪费

所以没有最好的策略,只有根据自己的项目需求合理安排分组打包

要根据资源的使用情况来合理设置资源分组,在分组时权衡好各方面的问题

知识点二 哪种压缩方式更好?

AB包的压缩方式:不压缩、LZ4、LZMA

一般情况LZ4用于本地资源,LZMA用于远端资源

主要原因是LZMA的压缩内容更小,更节约下载时间和流量

注意:压缩不会影响加载内存的大小,只会影响包体大小,下载时间等

但是也要根据实际情况

比如:

1.不压缩:包体并不大的单机游戏,使用不压缩最好,没有包体大小的压力,加载也是最快的,因为不用解压

2.LZ4:它是基于块的压缩,所以提供了加载文件的能力,加载资源时不用全加载AB包,只加载使用的内容,相对LZMA来说更节约内存

3.LZMA:不建议用它在本地内容中,因为它虽然包最小,但是加载最慢,用它只是为了节约下载时间和极限压缩包体大小

综合来说,也没有最优的方式,还是要根据实际情况来选择

个人认为LZ4压缩方式,是相对比较优秀的一种方式

知识点三 减小目录文件大小

当我们想要极限压缩包体大小时,可能希望优化目录文件的大小

1.压缩本地目录

AddressableAssetSettings > Catalog > Compress Local Catalog

2.禁用内置场景和资源

默认Addressables提供了从Resources等内置资源文件夹中加载资源以及加载内置场景(是之前从Resources过渡到AB包时Unity提供的一种过渡选择,目前已经意义不大)

如果你不通过Addressables加载他们,可以禁用,这样目录文件就不会包含其中信息

但是我们就只能使用老方法加载同名

个人建议取消,因为一般我们不会通过Addressables去加载非寻址资源

知识点四 注意事项

1.关于AB包最大的限制,老版本不支持大于4G的包,虽然新版本中已经没有这个限制

但是为了兼容性,还是建议大小控制在4G以下

2.活用可寻址资源上的Groups View中的两个功能

Show Sprite and Subobject Addresses:当窗口中内容特别多时,禁用它可以提升窗口加载的性能

Group Hierarchy with Dashes:启用带破折号的层级结构

可以让我们在内容特别多时以层级结构查看分组信息

Addressables 总结

学习的主要内容

  • Addressables导入

  • 资源加载基础

  • 配置相关

  • 资源发布加载

  • 资源加载补充

  • 窗口相关

  • 常见问题总结

常见的资源加载方式对比

  1. Resources

特点:应用程序发布后不能动态修改、本地

  1. AssetBundle

特点:减小包体大小、热更新

  1. Addressable

特点:基于AssetBundle,帮助我们管理AssetBundle

选择建议

Resources比较适用于做小游戏,单机游戏

AssetBundle和Addressables适合商业游戏

具体AssetBundle和Addressables怎么选

主要看你的团队和公司

如果是老项目或者迭代项目那么用以前写好的AssetBundle管理器即可

如果是新项目可以尝试使用Addressables,他让我们的使用更加方便