Addressable资源管理

Addressable资源管理

1)Addressable资源管理
​2)Addressable热更新问题
3)不合理旧图集拆分成新的小图集
4)XLua中在Lua和C#传递自定义值类型
5)Toggle的onValueChanged如何正确移除某个匿名的监听


这是第216篇UWA技术知识分享的推送。今天我们继续为大家精选了若干和开发、优化相关的问题,建议阅读时间10分钟,认真读完必有收获。

UWA 问答社区:answer.uwa4d.com
UWA QQ群2:793972859(原群已满员)

Addressable

Q1:大家用Addressable管理资源,对于AssetsGroups的Bundle Mode:选用Pack Together好,还是选用Pack Separately好?大家有什么好的建议吗?

A:Pack Together是最简单的管理方式,但是适合打包后比较小的资源集,比如少量通用的Shader可以打包在一起。但是往往还是Pack Separately用的比较多,个人觉得Separately更容易管理。

其实具体如何选择要看我们需要打包的资源如何分类。假设一个游戏的资源分出以下类型:玩家、怪物、NPC、地图、特效、音乐和通用资源等,那么从功能和管理的角度考虑,来对这些资源进行目录管理。

RemoteAssets
        |__Players  (Group)
        |    |__Player01   (拖入Group)
        |    |    |__Animations (作为SubAsset,不可修改,会被打包进Player01)
        |    |    |__Materials
        |    |    |__Textures
        |    |    |__Models
        |    |    |__Prefabs
        |    |__Player02
        |__Monsters
        |    |__Common
        |    |__Monster001
        |    |__Monster002
        |__Maps
             |__Map001

不同Group内则以更新为单位来考虑,比如版本2需要新加Monster003,版本4需要添Map002。自然应该按这些小分类做Bundle打包。

于是可以以大分类做Group,选择Pack Separately,以小分类的目录拖进Group作为AssetEntry,这样基本就能管理好了。并且一般每个Bundle的大小也能控制在合理范围内。

但是分开打包可能因为各个包内资源引用到其他共用资源,造成重复打包。因此整体尺寸会大于Together。于是我们可以在某个Group内建立一个Common的子目录,将这个Group内可能被共用的部分提出来打包。

另外一部分资源比如UI,可以考虑归入一个Group,然后选择按Label打包。按UI使用的界面和更新批次建立Label,我们这个步骤是结合AssetGraph利用文件名自动生成Label来设置Group的。

一些通用资源可以单独建立Group,然后选择Pack Together。但是我们依然是建立一个叫Common的Group,然后还是选择Separately。

这个组里面添加各个资源目录:

Common
    |__ Fonts
    |__ Shaders
    |__ Materials

大致思路是这样,但是还会要结合各个项目的实际需求,功能分割,更新和运营要求来做设计。

Q2:目前比较奇怪的现象是用Pack Separately打出的包要比Pack Together的大很多。

另外对于Addressable我们会有针对性地设置它的Group分组,比如按照功能、按照资源类型进行分组,如果所有资源都是Pack Together模式,那么Addressable会对应生成Group个数的Bundle文件,然后我们利用Addressable的Analyze工具,进行冗余资源分析,自动分析出的Group资源,我们还是用Separately模式在分析一次?检验下是否还有冗余吗?

另外还有个想法:把所有资源Group进行Separately打包,然后再用Analyze分析工具进行冗余检查,和先用Together模式进行打包再分析冗余,这两种操作方式,分析出的结果会是一样的吗?

A:其实我们基本不用Analyze工具,我们尽量作到所有资源的具体去向(打到那个具体包)都能清晰地管理和掌握,最多结合UWA的AssetBundle分析工具来找出冗余,然后手动来做移动和管理。只要每添加一个资源都能严格按照既定的规范来管理,目录分割清晰。导出和生成预制体的流程使用工具自动化,那么还是很好管理的。

感谢黄程@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/5f3557d29424416784ef1c01


Addressable

Q1:Addressable打热更新资源的时候会生成一个新的资源在原来的目录下,和旧的资源在一个目录,那我是每次都要把这整个文件夹都上传到CDN上吗?有没有什么办法能把每次需要热更新的资源单独提取出来放在一个额外版本文件夹呢?

A:主要有以下几点:

  1. 只要传新的AssetBundle和Catalog。
  2. 如果不包含Hash,那么AssetBundle文件名一样的情况下会覆盖旧资源(可能存在CDN缓存问题)。
  3. 可以每次出更新包修改RemoteBuildPath,指向不同目录。
  4. 继承一个BuildScriptPackedMode自己去实现一个Build脚本。

Addressables本身不对更新资源做版本号管理的,这个需要自己做。

Q2:麻烦问下,如果自己添加了版本控制,Addressable可以做到回退版本吗?例如已经使用版本2的Catalogs完成了更新,此时需要回退回版本1,用客户端版本2的Catalogs和服务端版本1的对比,可以再更回去吗?

A:没有试过,理论上可以,因为Addressable没有版本号概念,只对比Hash,不同就认为有更新,至于新旧,它不管。

Q3:组设置里,有个Use Asset Bundle Crc.,如果勾选这个,那客户端的文件CRC校验失败(例如被修改器之类的改了),Addressable会怎么处理呢?看描述说,这个是本地文件和远程文件都起作用的,这里对于Load Path分别是本地和远程的组来说,有什么不同吗?

A:CRC检测失败,自然这个资源的载入就失败了,Addressable会提示错误。这个过程一般来说相当于用记录在Catalog内的CRC对下载的Bundle做校验。这个和本地远程应该没多大关系,就算放在本地的包,其实也可以通过更新来做替换使用的。这个时候也是可以做CRC检验的。更细节的部分我也没有具体关注过,你也可以读一下源代码,看看具体怎么实现的。

感谢黄程@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/5f3263c576e4793c5394796a


Texture

Q:我们这边有这样的一个问题:用的TexturePacker打出的图集有大量的无效区域(很久的老图集了)有没有什么办法可以将这些图集拆分一下(需要考虑材质的分割和重新引用)。

A1:如果还有TexturePacker的工程,那么将ForceSquared的勾去掉,这样就不会强制正方形贴图,会根据实际使用的尺寸进行缩减了。

如果没有原工程,那么可以解析导出的.tpsheet文件,都是文本形式的。比如:

不同版本可能格式不同,但是都是可以解析的。然后做个小工具就可以分解并且重组了。粗暴点直接修改也行。一般只要Sprite名字不变,那么使用TexturePacker的SDK应该不需要考虑材质的分割和重新引用。

感谢黄程@UWA问答社区提供了回答

A2:TP打图集后期想拆分很难了,包括Sprite改名字都麻烦。如果你们能保证每个Sprite的名字都是唯一的,可以写脚本批量替换,否则就放弃。

感谢Walker@UWA问答社区提供了回答

A3:感觉是有不太有自动化的方法,推荐是手写脚本批量替换,我倒觉得Sprite名不定要唯一。

假设图集名字为a,新图集为b。

  1. 先遍历工程,找出对图集a引用的资源A等等。
  2. 将A引用的Sprite替换为b中对应的Sprite。
  3. 都替换完成后,再重复1,确定a没有被引用。

还有1种情况是Atlas - Sprite对在代码中被使用,而非直接引用。那可能要定向查询配表信息/字符串常量了。

感谢cloud@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/5f2959e81501c93067a9ac2a


Script

Q:XLua在传递值类型的时候,可通过GCOptimizeAttribute来优化GC表现,通过AdditionalPropertiesAttribute来声明在C#和Lua之间做值传递时,当作字段来传递的属性(Property)。此外,对例如 UnityEngine.Vector3 的类型,Lua一侧在一些情况下可直接用Table传递,如:

local v = {x=1, y=2, z=3} + Vector3(4, 5, 6)

得到的V是个分量为5、7、9的Vector3。这个行为在生成Wrapper代码,或者以反射模式运行的时候,都是成立的。

但是我自定义的Struct类型,声明了上述Attribute,也生成了代码(包括CopyByValues等),类似行为只有生成Wrapper才能正确执行,在反射模式下传递的值都是0。请问为什么?

Struct代码:

public struct SB
{
    private int intField;
    private float floatField;
    private long longField;
    public static SB operator +(SB a, SB b)
    {
        return new SB
        {
            IntField = a.IntField + b.IntField,
            FloatField = a.FloatField + b.FloatField,
            LongField = a.LongField + b.LongField,
        };
    }
    public int IntField
    {
        get => intField;
        set => intField = value;
    }
    public float FloatField
    {
        get => floatField;
        set => floatField = value;
    }
    public long LongField
    {
        get => longField;
        set => longField = value;
    }
    public override string ToString()
    {
        return $"int: {intField}, float: {floatField}, long: {longField}";
    }
    public static SB Create(int intField, float floatField, long longField)
    {
        return new SB
        {
            IntField = intField,
            FloatField = floatField,
            LongField = longField,
        };
    }
}

Lua侧:

local SB = CS.SB
    local sb1 = SB.Create(1, 2.0, 3)
    local sb2 = SB.Create(2, 3.0, 4)
    Logger.LogWarningSafe(sb1 + sb2)
    Logger.LogWarningSafe(sb1 + {IntField=1, FloatField=2, LongField=3})
    Logger.LogWarningSafe({IntField=1, FloatField=2, LongField=3} + sb1)

用反射模式运行的结果:

生成Wrapper代码后的运行结果:

后者运行结果正确。所以,使用AdditionalProperties,是否还需要做什么额外工作才能使得上述代码在反射模式和Wrapper模式下都能正确运行呢?

环境:Unity 2018.4.18f1,以及大约是2019年较早时候的xlua-framework。

A:你看一下AdditionalPropertiesAttribute的被引用情况就知道了,只在Generator里用到,反射模式下根本不判断这个的。

Vector3可以,是因为X、Y、Z都是Field。在反射方式下,只会用Field方式去尝试匹配(见ObjectCaster.cs 672行)。

感谢snuc@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/5f28e814d6897a051537151c


Script

Q:如下述测试代码,OnDisable内的无法正确移除Test(), 其结果是多次OnEnable后 onValueChanged时调用了很多次Test();

public void OnEnable()
    {
        toggle.onValueChanged.AddListener(_ => Test());
    }

    public void OnDisable()
    {
        toggle.onValueChanged.RemoveListener(_ => Test());
    }

    public void Test()
    {

    }

A:RemoveListener的时候_ => Test()作为匿名函数是单独的实例,和Add的时候不是同一个,自然无法移除了。

public void OnEnable()    {

        toggle.onValueChanged.AddListener(this.Test);
    }

    public void OnDisable()    {
        toggle.onValueChanged.RemoveListener(this.Test);
    }

    public void Test()    {

    }

试试:

private Data _data;

private delegate void OnValueChangedDelegate;

private OnValueChangedDelegate onValueChanged;


public void OnEnable()    {
    this.onValueChanged = ()=>{
        OnValueChange_FileDelete(this.toggle, this._data);
    };
    toggle.onValueChanged.AddListener(this.onValueChanged);
}

public void OnDisable()    {
    toggle.onValueChanged.RemoveListener(this.onValueChanged);
}

public void OnValueChange_FileDelete(Toggle toggle, Data _data)    {

}

感谢黄程@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/5f34f8799424416784ef1bea

封面图来源于网络


今天的分享就到这里。当然,生有涯而知无涯。在漫漫的开发周期中,您看到的这些问题也许都只是冰山一角,我们早已在UWA问答网站上准备了更多的技术话题等你一起来探索和分享。欢迎热爱进步的你加入,也许你的方法恰能解别人的燃眉之急;而他山之“石”,也能攻你之“玉”。

官网:www.uwa4d.com
官方技术博客:blog.uwa4d.com
官方问答社区:answer.uwa4d.com
UWA学堂:edu.uwa4d.com
官方技术QQ群:793972859(原群已满员)