cover_image

第四期西山居技术沙龙精彩回顾,运行时资源管理(Unity)!

西山居技术
2017年08月04日 12:51

第四期西山居技术沙龙

2017/8/3 19:00-19:45

主题:运行时资源管理(Unity)

讲师:李翔威

图片讲师李翔威

图片主持人与观众

温馨提醒:没能参加第四期西山居技术沙龙的小伙伴,可以连接内网,点击【阅读原文】,查看直播视频。




上期回顾

上周和大家分享了Unity资源配置和资源配置工具,Unity资源配置在资源管理中处于基础地位,影响资源的增长速率以及量级。

通过合理的资源配置,可以承载更多数量的资源,丰富游戏内容。再之后介绍的场景解耦,提供一个资源处理思路,通过把场景资源拆分成一个个资源,我们可以动态管理这些资源。

图片

查看上期沙龙点击:Unity资源配置,项目中的资源管理




本期分享

今天主要分享运行时的资源管理,并探讨如何妥善地管理资源以实现内存与性能兼顾。从资源介绍开始,分析加载接口设计与对象池设计,然后讨论资源内容分级,最后分享一款轻量级内存Profile工具。

内存与程序稳定性

iPhone 6& iPhone 6P只有1G的内存,而这两个机型在iOS平台上的市场份额超过40%。这两个机型如果使用了超量的内存,游戏会闪退,这是极差的游戏体验。

你可以想象一下,在进行激烈战斗的时候,加载了更多的特效和模型,游戏突然闪退了。或许游戏有一套不错的断线重连机制,你还能回到战场。但基本上说你很难获得这场战斗的胜利,这是非常差的游戏体验。

同时,在iPhone 6S以上的机型有2G的内存可以使用,只要性能没有问题,完全可以承载更多的内容(资源)。在制作了过量资源的情况下,如何妥善地管理资源是一个较大的挑战。一个项目一百多号人参与制作,如何协调工作、规整制作内容是一个头疼的问题。

合理的资源管理方案兼顾性能与内存,提供一个稳定流畅的游戏环境。

Unity资源介绍

在做资源管理之前,首先我们要对资源有足够的了解,这样可以方便展开之后的工作。Unity官方已经有一篇非常精彩的文章来介绍Unity资源《Assets, Objects and serialization》

An Asset is a file on disk, stored in the Assets folder of a Unity project. For example, texture files, material files and FBX files are all Assets. Some Assets contain data in formats native to Unity, such as materials. Other Assets need to be processed into native formats, such as FBX files.

A UnityEngine.Object, or Object with a capitalized ‘O’, is a set of serialized data collectively describing a specific instance of a resource. This can be any type of resource which the Unity Engine uses, such as a mesh, a sprite, an AudioClip or an AnimationClip. All Objects are subclasses of the UnityEngine.Object base class.

Asset是指在Assets目录下的所有文件,在工程里面每个Asset会有一个对应的Meta文件,Meta文件用于描述Asset在工程里面的格式。上周讲的贴图配置也是通过修改Meta文件来达成。一个Assets包含一个或多个Object,这里的Obejct可以直接包含数据,也可以引用了其他Asset文件下的Object。

GameObject是一个特殊类型的Obejct,通常我们通过把一系列Assets组装成Prefab(GameObject)来制作资源,Unity通过依赖关系加载所有资源。在加载一个GameObject之后,我们通常需要实例化GameObject。

大部分Asset资源是共用的,实例化过程Unity并不会复制这些共用资源,而是复制那些可修改的不复用数据,比如MonoBehaviour上的数据。当然我们也可以直接加载Asset资源来使用,比如直接加载一张贴图,放在一个面板上展示。通过依赖加载的贴图和直接加载的贴图是同一份贴图,Unity内部帮我们解决了资源重复的问题,可以放心使用。

Resources

The Assets and Objects in all folders named “Resources” are combined into a single serialized file when a project is built.

Resources目录下所有的资源,都会被打包且可以通过Resources接口加载,加载路径为Resources目录的相对路径。支持同步与异步两个加载接口,支持单对象的UnloadAsset,还有一个清理引用计数为零的资源接口。

这里UnloadAsset不能卸载GameObject和Component对象,而且是强制卸载,如果外面对象还持有引用资源就会丢失。UnloadUnusedAssets接口则开销较大,可能会引起游戏卡顿。

图片

AssetBundles

An AssetBundle is an archive file containing platform specific Assets (Models, Textures, Prefabs, Audio clips, and even entire Scenes) that can be loaded at runtime.

通常我们推荐使用AssetBundle来加载资源,使用AssetBundle可以以更小的包来管理和更新资源,同时还可以加快游戏启动时间。更深入的内容可以看看Unity官方的文章《The Resources folder》。

链接:

https://unity3d.com/cn/learn/tutorials/temas/best-practices/resources-folder

加载AssetBundle需要我们自己去维护依赖关系,对比起Resources来说会更加麻烦。通常在开发的时候使用Resources加载,而在发布版本使用AssetBundle。这里需要实现自己的加载器来满足两套资源的切换。

资源管理器

  • 统一Resources&AssetBundle加载

  • 类似的加载接口设计,包括同步与异步

  • 强引用计数管理,Load与Unload匹配

  • 支持按优先级加载资源

  • 支持配置系统开销,后代异步加载开销

对外实现为静态接口,正常情况下支持Editor运行时与非运行时,运行时不管在PC还是手机都支持Resources与AssetBundle无缝切换。

所有的加载路径参数统一为Resources目录相对路径,这里要求在同一目录最好不要有同名文件(扩展名不一样)。按类型匹配资源是较烦琐的工作,而且对于Object基类加载,很难匹配到正确的资源。

异步接口定义一个自己Request类返回,除了原有的ResourceRequest数据,这里新增一个打断属性。当不再持有这个对象的时候设置打断属性的值来中断加载。同时这里还可以配置回调接口,这样不需要每次更新去查询状态。资源管理器在异步加载完资源后,执行回调接口。

异步加载增加优先级参数,优先加载高优先级的对象,可以有更好的游戏体验。同时有些可能被打断的资源请求因为低优先级直接结束,也可以减少程序开销,提高游戏性能。比较好的一个设计是同时发起多个异步请求,但限制异步请求数量。

然后还要关注异步加载的开销,如果异步加载占用太多的主线程时间,那带来的游戏体验危害可能高于直接使用同步加载。Unity可以通过配置Application.backgroundLoadingPriority值来约束开销。如果要求游戏跑30帧的话,建议配置为Normal即可,在过场景的情况下,可以配置成High来加快加载速度。

  • ThreadPriority.Low - 2ms;

  • ThreadPriority.BelowNormal - 4ms;

  • ThreadPriority.Normal - 10ms;

  • ThreadPriority.High - 50ms.

由于实现了自己Request,所以这里也要实现自己的时间片管理器。实例化对象与回调接口的开销都是不可预期的,我们配置一个每帧最大执行时间做平滑。

最后讨论下资源卸载策略,实时卸载资源导致资源反复加载开销大影响游戏体验,通常会缓存一定数量的资源来改善体验。对于非GameObject和Component的Asset资源,可以UnloadAsset直接卸载。

剩下的GameObject,通过取消资源管理器对对象的持有再调用Resources.UnloadUnusedAssets来卸载。由于我们使用了强引用计数管理,所以在清理的时候通过对引用计数的判断可以正确的清理资源。特别对于使用AssetBundle加载资源的情况,错误的管理可能会导致资源重复加载,浪费内存。

资源对象池

资源加载器负责加载、卸载资源,同时缓存资源,这里的资源对象池特指GameObject资源池。GameObject资源通常带有自己的数据,在加载的时候需要实例化一份以便使用。

实例化GameObject是一个开销较大的操作,同时也会带来较高的GC Alloc内存分配。资源对象池就是一个GameObject对象池用于缓存实例化的GameObject对象。

资源对象池在使用上要注意GameObject对象的复用,开始的时候加载一个预制体是一个干净的数据。外部逻辑会修改GameObject上的数据、添加组件,之后这个对象会入池。

设计上如果一个对象需要使用对象池的复用功能,逻辑需要保证这个GameObject是可复用的,这并不是一件容易的事情。把状态还原重置本身就有一定的开销,如果实例化一个对象的成本低于重置数据的开销,那不需要对象池每次实例化即可。

同时在对象入池的时候,还需要做一项让对象不可见的工作,销毁一个对象(对象入池)在这里的行为应该保持一致。有两个常见的做法一个是SetActive(false),还有一个做法是把对象移除摄像机。

对于transform特别多的对象修改坐标的开销较大,对于组件较多的对象修改激活状态的开销可能会更大,需要执行OnEnable。这里提供了三种入池行为,分别是InActive、InVisible以及Destroy用于处理上面讨论的情况。

对于加载资源,然后实例化这个常见的对象加载操作。资源对象池封装实现自己的一个Spawn接口,表示生成一个对象,然后对应的一个Despawn接口用于销毁对象。

这里还提供了异步的SpawnAsync接口用于异步加载以及错帧实例化使游戏体验更加流畅。对于Spawn接口提供带初始坐标的实例化接口与Instantiate保持一致,提供初始坐标避免多次修改坐标减少资源加载开销。

最后讨论下资源池的缓存策略,通常资源池里面存在两种情况的资源。一种是外部还存在着相同的对象在使用,另一种则是所有的对象都在资源池。

对于所有对象都在资源池的对象,可以认为是不使用资源根据时间淘汰。对于外部存在引用的情况,增加其权重值但还是会按时间来淘汰。存在部分类型资源会有较多的实例而部分资源只有一两个实例,这里做资源池总上限的约束而不做单类型数量约束。

在激烈的战斗场景下对象数量会远远高于平时,过小的资源池上限会导致卡顿,过大的资源池上限会导致内存过高。这里增加一个资源池下限,当资源池对象数高于这个数目的时候执行清理操作,然后配置一个较高的资源池上限不用当心一直占用过高的内存,得到一个性能与内存兼顾的结果。

资源内容分级

当资源的使用超标时,也可以通过简单地调整一些参数来开关这些对内存有较大影响的对象。 

观察iPhone机型内存时,你可以发现其内存有着较大的跨越。在2G机型可以承载游戏内容的情况下,1G机型承载不了这么多的内容,所以这里对资源内容进行分级。

机型内存
iPhone 5 – 6P1 GB LPDDR2/LPDDR3 DRAM
iPhone 6S – 72 GB LPDDR4 DRAM
iPhone 7P3 GB LPDDR4 DRAM

常见的分级内容

  • 屏幕后期效果

  • 高低材质

  • 贴图大小减半

图片

如果通过上面的OnRenderImage实现屏幕后期效果,这里的source和destination贴图都是Unity申请的,并与分辨率直接挂钩。在1080P的分辨率情况下,会消耗掉50M左右的内存。所以一个比较好的做法是在低内存机型上关掉这个效果。

高级的材质使用更多的顶点数据与贴图,比如法线贴图、通道贴图。低级材质使用更少的贴图,通过高低材质的切换可以达到减少贴图使用量减少内存开销。

Mesh也是同理,如果不需要法线则、不需要有法线的顶点数据,缩减贴图大小也是一个不错的方法,不过保存两份贴图会使包文件变大。

配置资源管理参数

前面我们为了得到一个较好的性能做了较多的资源缓存工作,针对不同的内存配置不同的参数达到优化内存的目的。内存不够带来的体验是游戏直接闪退,所以这里认为游戏稳定性的优先级高于游戏卡顿。

这里主要配置Assets资源缓存数量,资源池的上限与下限,还有一些资源清理时间间隔的参数配置。通常经过一系列的压力测试可以得到一个安全配置参数,这些数据可以方便地修改。后期如果增长导致内存不够,则可以通过修改配置参数来达到稳定游戏的目的。

简洁的内存管理机制

想要解决内存闪退,了解闪退时的内存使用情况是很有必要的。在游戏运行过程中,我们可以记录内存使用情况。同时可以对资源类型进行分类,了解细节。

Unity的Profile工具虽然非常方便,功能也足够强悍。但是没有数据落地,而且采样占用额外的内存。这里自己实现了一个简易的内存Profile工具,支持数据落地方便对比,同时不占用过多的额外内存。

在了解到闪退时的内存情况后,我们可以很容易了解到什么样的情况会导致内存不够用,哪些地方的内存使用超标,是否有优化的余地,极限情况下最低的内存使用量。

内存Profile工具

图片

通过Resources.FindObjectsOfTypeAll获取当前所有的对象,通过Profiler.GetRuntimeMemorySize计算每个Object的内存大小,通过Object.name可以获取对象的名字。这些信息可以实现一个简洁的内存Profile工具,对比起Unity提供的Profile工具,自己实现工具可以比较方便做一些数据落地以及自动采样过程。

同时这个Profile工具还和自己实现的资源管理器进行了整合,可以记录当前的Assets数量、GameObject数量、缓存数量以及引用计数为零的对象数量。这些额外的数据有较大的参考价值,可以直接记录,以方便后续的对比以及观察数据趋势。

同时Profile工具还支持导出资源列表,之前提到我们的加载接口是通过路径加载做强引用计数管理。这里可以输出每个资源的路径与引用计数,可以定位资源泄露,排查资源残留的情况。

数据以文本的形式记录,支持自动采集上报,之后可以对这些数据做图形化显示、分析。每次跑游戏都可以得到一份数据报告,对比数据报告可以对客户端内存使用趋势有一定的认识。避免出现因内存不够导致游戏闪退的情况。

图片

上图是内存Profile工具在PC上采样的结果,这里对数据进行了分类,按类型和使用场景分类。通过这些数据可以得出一些指标,比如贴图不能超过50M,Mesh不能超过20M。

不同场景下的资源使用情况是不同的,这里做的工作就是把50M分配给各个模块。这样做的一个好处是这事能找到一个人负责,同时这个人又是对这个模块最熟悉的。

兼顾内存与性能

  • Android 高内存,低CPU,低I/O

  • iOS 低内存,高CPU,高I/O

资源加载是一项非常慢的操作,如果所有的资源都实时释放,那下次加载资源带来的卡顿也会带来较差的游戏体验。由于iPhone机型内存少,加载快,可以做实时释放策略。对于Android机型内存多,加载慢,可以做预加载策略。

同时还可以采用带权重的资源缓存策略,资源缓存是由资源的最后使用时间和加载时间得到的一个权重,优先释放加载快、不经常使用的资源,这样可以在内存和性能上得到一个较好的照顾。同时在iOS上会有内存的Warning警告,当触发警告时可以做强制性清理,避免游戏闪退。



未完继续

图片

如果你有一肚子干货,还有表达欲望!

请火速联系小编!小编水陆空全力支持你!

联系邮箱:fulei1#kingsoft.com

注:发送邮件时将#换成@

下期沙龙预告

地点:1楼展厅

时间:8/10(下周四)19:00-19:45

主题:Mono性能优化(Unity)

讲师:李翔威

13年进入西山居做DX11引擎

14年开始参与剑侠世界手游项目

17年再次参与剑网3项目

图片



图片


Brainstorm

脑洞时间




问题:地球上有多少个点,使得从该点出发向南走一英里,向东走一英里,再向北走一英里之后恰好回到了起点?



在文章结尾留下答案

点赞前3名将获得鲜榨果汁咖啡或者1积分

可自己选择奖品类型,奖品在一周后送出

图片

往期精彩回顾:

① 第一期:刘宇,演讲就是讲好故事!

② 第二期:顾露和刘马良,Lua性能优化方案!

③ 第三期:李翔威,Unity资源配置,项目中的资源管理!

点击阅读原文,查看直播视频


图片

-长按关注西山居技术-


继续滑动看下一个
西山居技术
向上滑动看下一个