Unity资源管理和优化调研

Unity资源管理和优化调研

开头先贴参考文献

Unity 打包总结和资源的优化和处理—by fishyu

Unity 性能优化:资源篇 – 心之凌儿的文章 – 知乎

Untiy性能优化:合批篇 – 心之凌儿的文章 – 知乎 

Unity性能优化基础篇——资源规范 – wumiao的文章 – 知乎

Unity 用户手册 (2019.4 LTS)/在 Unity 中操作/资源工作流程/AssetBundle

Unity音频优化实践 – 罗辑的文章 – 知乎

【内存优化】Unity音频资源优化 – 曾志伟的文章 – 知乎


前言

最近在写框架的资源管理器 发现打包和加载这块延伸的部分有很多

搞得自己有点乱 需要考虑的细节还是有很多的

在翻阅了几篇文章过后决定自己总结一下 算是汇总前人的经验

在这里首先明确一下概念 我认为整套流程有三个阶段

  • 资源的预处理
  • 资源的划分打包
  • 资源的运行时加载

这三个阶段串起了整套资源管理的流程

需要注意的是 预处理时指定加载方式的操作

我仍然是归到预处理这一栏里的

运行时加载主要讲一些通用的加载策略


资源的预处理——图片

Unity中的图片处理选项非常多

我们可以先把图片处理分成几个部分

图片导出格式

目前Unity支持的图片格式里我们基本只考虑三种

  • jpg 有损压缩 没有透明通道
  • png 无损压缩 有透明通道
  • tga 不失真压缩 有透明通道

实际使用还是考虑png会好一点

毕竟透明通道这玩意还是有不少图片需要的

图片作为纹理后

当图片导入Unity后 Unity会对图片有很多的纹理处理选项

首先是设置纹理类型形状 这个基本不用强调 根据需要去选就可以

我们主要来看之后的选项

去除Alpha通道

是指一张图片的透明和半透明度

在这里我们首先要对图片进行评估

哪怕你是作为png格式导进来的

你真的有透明需求吗

只要没有透明要求 就去掉Alpha通道

降低纹理分辨率

一张纹理美术导出的大小远远超出了实际需要

就需要对Max Size这一栏做调整

例如一个按钮可能只有128*128

你拿到的却是1024*1024 那就浪费了很多

选择合适的纹理压缩格式

这个图我确实在很多地方都见到了

简单总结一下

要想效果最好不计成本就用RGBA32

PC/主机设备 用RGB16+Dithering

移动端设备 用RGB(ETC1) + Alpha(ETC1)

用ETC还有PVRTC4的理由还有一条

这一类在内存中不需要进行解压 GPU可以直接支持

对于性能不高的设备上表现会比较好一点

不勾选Read/Write Enabled

这一点模型也是一样的 但是模型会有坑(之后会提)

在运行时如果你需要用脚本调用对图片进行修改

那么就要保持对这一项的勾选

勾选后运行时CPU和GPU内存都会有纹理资源 会有额外开销

如果确定自己不需要进行纹理的读写就别勾了

禁用多余的Mipmap

101里其实提到过 我现在还印象深刻

你可以简单理解为 不同视距下加载的纹理精度不同

为此你需要生成多个级别的不同分辨率的纹理图

生成了很多张图 比原本的存储1多了1/3

Mipmap仍然是很有用的

但问题是如果你是个2d游戏 不会有视距纵深的话

MipMap就会变得没有必要了

打包图集

减少DrawCall 提高性能

DC就是CPU通知GPU进行一次渲染的命令

如果DC次数较多 会导致游戏卡顿

可以通过打图集 将小图合并成大图 将本应n次的DC变成一次

一般来说UI的素材基本都是要打包成图集的

因为很多UI单个的大小并没有那么大 不如合并到一张里

有透明要求的也可以专门挑出来打包成图集

这样也比较不会浪费 打包图集的方式就不在这里做介绍了


资源的预处理——模型

Unity其实在文档里就已经提出了很多具体要求了

我在这里稍微简单介绍一下 具体的还是移步文档观看吧

角色建模的性能优化

  • 使用单个带蒙皮的网格渲染器
  • 使用尽可能少的材质
  • 使用尽可能少的骨骼
  • 尽量减少多边形数量
  • 正向和反向动力学保持分离

还有一些和图片类似的处理

例如禁用Reader/Write Enables

但是这有可能会使得Mesh Collider不可用

除此之外也有一些质量优化的选项

  • Mesh Compression:通过使用网格边界和每个组件较低的位深度来压缩网格数据,增加压缩率会降低网格的精度。最好在 Mesh 看起来与未压缩版本没有太大区别的情况下将其调得尽可能高。这对于优化游戏大小很有用
  • Optimize Mesh:确定三角形在网格中列出的顺序以获得更好的 GPU 性能,默认都会勾选
  • Normals:如果网格模型既不是法线贴图也不受实时光照影响,就选用None,这样也能够很好的提升性能表现

如果是TA的话 可能还会了解LOD

一种类似于纹理的Mipmap的处理

但关于这块我确实也不太了解 暂时不做介绍


资源的预处理——音频

音频这块其实作为开发者平时可能不大会去在意

但实际上音频这块 不小心的话也很杀空间和内存

我们先提一个比较常见的解决方案——Wwise

Wwise是一个很强大的音频中间件

主要给音频制作者和开发人员提供桥梁

但我要说的是这个音频引擎也给音频做了不少优化

无论是体积还是内存还是运行时CPU 可以去了解一下

这里还是回归Unity自带的部分

音频格式的选择

比较常用的格式

wav 几乎无损

mp3 失真小的有损压缩

ogg 压缩比高

aiff和wav比较类似 大小都会比较大

实际使用过程中比较常见的方案如下

音效 语音会采用ogg

BGM这种对音质有一定要求的会采用mp3

unity对音频的加载和压缩格式

音频的加载方式如下

  • Decompress on Load – 解压完整的数据进内存
  • Compressed in Memory – 加载进内存,使用的时候解压。用CPU性能换一部分内存
  • Streaming – 完全不加载进内存,使用时从存储介质中串流。最省内存,消耗最多CPU

音频的压缩格式如下

  • PCM :提供高品质但牺牲文件大小最适合使用在很短的音效上
  • ADPCM: 这种格式适用于大量音效上如脚步爆破和武器,它比PCM小3.5倍但CPU使用率远低于Vorbis/MP3
  • Vorbis/MP3: 比PCM小但是品质比PCM低,比ADPCM消耗更多CPU。但大多数情况下我们还是应该使用这种格式,这个选择还多了个Quality可以调节质量改变文件大小 (Quality测试1和100对内存影响并不大)

比较推荐的方案

时间短的音效使用Decompress on Load,压缩格式使用Ogg Vorbis

时间较长的音效使用Compressed in Memory,压缩格式使用ADPCM

音乐(如背景音)使用Streaming,压缩格式使用PCM

关于Unity中的声道

Unity中没有双声道 或者说双声道没有意义

因为两个声道都是从一个点出发

所以使用了双声道的音源 可以把Force to Mono给勾上

同时发声的限制

Unity同时发声的数量是有限制的 默认是32个

但可以在Max Real Voices这栏进行修改

Virtual Voice会在后台播放 实际是听不到的

每个声音都可以设置优先级0最高 256最低

如果Real Voice小于上限 Virtual Voice就会根据优先u级补足为RealVoice

如果Virtual Voice大于上限了 就会根据优先级被停止

比较合理的方案:就是啥也不改

如果有需要的话 可以根据设备性能往上加

非必须的音效勾选Load in Background

可以减少场景加载的时间

场景不会等该音频加载好后才开始

合理加载音频数据

如果Preload Audio Data这一项被勾选了

加载音频的时候就会把声音信息加载进内存

不勾选手动控制可以节约一些开销

audioClip.LoadAudioData();
audioClip.UnloadAudioData();

禁用音频组件而不是使用静音

静音 = 仍然在播放

开销仍然是存在的 所以真的不用了 还是禁用掉吧


其它资源的预处理

讲完了图片 模型 音乐

其实已经把资源里最大的三块给讲完了

剩下的部分会比较零碎一点 所以集中在这里解释

动作片段的预处理

剔除动作里面那些位移和缩放差别非常小的帧

可以通过脚本自动化来实现

特效材质贴图动态加载

会很经常出现特效的材质贴图复用的情况

这个时候可以把这些材质贴图单独拿出来打个包

加载的时候动态加载就可以了


资源的划分打包

这一段主要是针对AB包来说的

如果你去解压手游的资源文件 会发现里面单个AB包的大小都不会太大

最大的 也不会超过4~5mb

回忆一下 为什么用Resource一把梭是不合适的

因为所有东西都打包到一个包里 然后直接加载到内存里

是很浪费的一件事情 因为里面的东西并不是都要同时加载的

在体量小的游戏里 这样做的问题不是很明显

但一旦管理的资源多了 Resource一把梭就会很有问题

所以AB包单个包大小都不会太大 为了防止一次性加载过多的资产到内存中

那么如何把手头上的资源按照合适的大小划分到一个接一个的AB包中

就是一个很折磨人的问题了 所以预处理阶段是很有必要的

因为其中很多的压缩手段都能有助于减少包体大小

AB包的压缩方式

  • Compression 不压缩
  • LZMA 压缩到最小 解压比较慢 用一个要解压全部
  • LZ4 比较平衡 用什么解压什么 不会全部解压 内存占用低

AB包的依赖

创建一个材质球 作为立方体的材质

我们并没有给材质球赋值包名 但它已经被包含进立方体相同的包了

如果把材质球移到和立方体不同的包中

只加载立方体的包和立方体自身 立方体的材质就会丢失

要么把材质球和立方体放在相同的包里 要么就还要加载材质球所在的包

也就是说两个包之间存在依赖 就要都加载

基于AB包特点的划分方案

对于压缩方式的选择

如果预处理已经有压缩的 同时又不需要依赖的

例如音效 或者 文本文件 都可以考虑完全不压缩

这样加载起来速度是最快的

其它的都统一选择LZ4这个方式 会比较平衡一点

对于资源的具体划分 Unity文档里也给出了建议https://docs.unity.cn/cn/2019.4/Manual/AssetBundles-Preparing.html

  • 将频繁更新的对象与很少更改的对象拆分到不同的 AssetBundle 中
  • 将可能同时加载的对象分到一组。例如模型及其纹理和动画
  • 如果发现多个 AssetBundle 中的多个对象依赖于另一个完全不同的 AssetBundle 中的单个资源,请将依赖项移动到单独的 AssetBundle。如果多个 AssetBundle 引用其他 AssetBundle 中的同一组资源,一种有价值的做法可能是将这些依赖项拉入一个共享 AssetBundle 来减少重复。
  • 如果不可能同时加载两组对象(例如标清资源和高清资源),请确保它们位于各自的 AssetBundle 中。
  • 如果一个 AssetBundle 中只有不到 50% 的资源经常同时加载,请考虑拆分该捆绑包
  • 考虑将多个小型的(少于 5 到 10 个资源)但经常同时加载内容的 AssetBundle 组合在一起
  • 如果一组对象只是同一对象的不同版本,请考虑使用 AssetBundle 变体

资源的运行时加载

//如果采用的是 LZMA 压缩方式,将在加载时解压缩 AssetBundle
//LZ4 压缩包则会以压缩状态加载
AssetBundle.LoadFromMemoryAsync

//从本地存储中加载未压缩的捆绑包时,此 API 非常高效
//如果未压缩或采用了LZ4,LoadFromFile 将直接从磁盘加载捆绑包
//使用此方法加载LZMA将先解压再将其加载到内存中
AssetBundle.LoadFromFile

//网络加载
UnityWebRequest.GetAssetBundle
DownloadHandlerAssetBundle.GetContent(UnityWebRequest)

如果一个包依赖的包非常多 还需要利用主包的清单来获取依赖信息

挨个加载 缺点也很明显 不能知道具体是谁依赖哪几个包

只能得到一个包里所有物体依赖的包的和

何时调用 何时卸载?

首先要明确 需要的时候去加载 用完去释放

这个过程如果全手动的话 我感觉是很笨的

资源的加载和释放 应该跟随的是实体对象的生命周期

例如说我有一个UI对象 他关联了若干的图像资源

那么这个UI对象加载的时候 他关联的资源对象也要跟着加载

当这个UI对象确定真的要被销毁 注意是确定真的要被销毁的时候

才去卸载它所关联的这些资源

引用计数法

资源的复用是很频繁的

如果一个资源在被调用的时候 被其他资源卸载了

那就会出现很严重的问题了

所以我们就得考虑更为合理的方式来确定资源的卸载

引用计数就是比较常用的一种方式

这里我写了一个很粗糙的范例 实际还要再改改

但用来介绍引用计数已经够了

public interface IReferenceCounter
{
    int refCount { get; }

    void Gain();

    void Release();

    void OnRefZero();
}

public class AssetObject : IReferenceCounter
{
    public string assetName;
    public AssetBundle ab;

    public int refCount { get; private set; }

    public AssetObject(string assetPath)
    {
        refCount = 0;
        assetName = assetPath;
        ab = AssetBundle.LoadFromFile(assetPath);
    }

    public T GetResource<T>(string name) where T : Object
    {
        Gain();
        return ab.LoadAsset<T>(name);
    }

    public void Gain()
    {
        refCount++;
    }

    public void Release()
    {
        refCount--;
        //这里可以做个延迟卸载的操作
        if (refCount == 0)
        {
            OnRefZero();
        }
    }

    public void OnRefZero()
    {
        ab.Unload(true);
    }

简单来说

当一个对象引用这个AB包 AB包的计数就会加一

当对象被销毁 对应的被引用的AB包的计数就会减一

当减一的时候判断计数是否为0 是的话就要卸载该AB包了

当然在卸载的时候我们仍然要谨慎一点

假如我们有一个面板 引用了一个资源

这个资源在这个时候只被这个UI面板引用

那我作为一个手贱的玩家 一直疯狂的开关这个面板

那么只要引用计数为0的时候就会去卸载它 这样其实是很没有必要的

所以可以考虑在引用计数为0的时候 设计一个缓冲期

并不是立刻卸载 而是缓冲期过的时候 再去卸载它 就会避免这种问题

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注