随着技术的不断升级,高性能的引擎逐渐受到越来越多研发商的青睐,UE4就是其中之一。在上周Unreal Open Day 2017活动会上,大宇就宣布旗下经典IP《仙剑奇侠传》、《轩辕剑》的续作,即《仙剑奇侠传7》和《轩辕剑7》将采用虚幻4引擎开发的消息。
而从另一方面,用虚幻4引擎制作游戏也需要注意一些问题。此前,葡萄君曾在《》一文中有所提及,近日,同样在Unreal Open Day 2017活动上,Epic Games的开发者支持工程师郭春飚以“如何在移动平台上做UE4的UI优化?”为主题,从四个方面对整个优化过程进行了描述。
以下为演讲实录:
大家好,我是Epic Games的开发者支持工程师郭春飚,今天给大家介绍的是在移动平台上面做UE4的UI优化,因为我们之前一直接到国内开发者的一些抱怨,他们觉得UI在手机上面开了以后性能下降的很快,今天就专门给大家介绍一下怎么用UE4在UI上面做优化,这是今天要讲的内容,首先会演示一个案例,接下来介绍怎么做优化,一块是游戏线程优化,一个是渲染线程优化,最后是编程技巧,先做案例介绍。
案例介绍
这是我们的一个演示工程,这个工程大概是我们做的测试工程,是在手机上面演示的,我们测试的机器是小米4C,同时开启了Mobile HDR。
谈性能之前先看一下性能指标,不要使用Stat.Slate会影响开发者做性能分析,可以使用stat dumpave num查看LOG,性能指标可以做Slate Tick - STAT_SlateTickTime游戏线程:Vertex Buffer;Slate Render - STAT_SlateRenderingRTTime渲染线程:UI渲染到Back Buffer;Widget Render - FWidgetRenderer_DrawWindow渲染线程:UI RTT / Retainer Box。
这是小米4C的性能数据,一开始是FPS是36,右边的列表是我们的优化开关,大家看到这个优化效果,一开始游戏线程11毫秒,渲染线程是8毫秒,用了Invalidation Box以后,游戏线程就减少到了1毫秒,这时候FPS提升不大,因为在手机上面UI的瓶颈更多是GDO,然后如果打开了Retainer Box以后,我们的渲染线程大概能减低3毫秒,这个时候FPS提高将近10每帧。
游戏线程优化
接下来就开始介绍具体的优化方案,第一步是游戏线程优化,这是一个小的事例,这个UI上面有两个贴图和一个文本框,Invalidation Box,每帧操作Grid Panel遍历所有的Child Widgets,Image1, Text1, Image2分别计算Draw Elements,Grid Panel将Image1和Image2的Draw Elements合并,最后Grid Panel返回2个Draw Elements进行渲染,如果是像这样一个复杂的控键数,这个开销也是比较大的。
我们Invalidation Box缓存Draw Elements (Vertex Buffer),用Invalidation Box封装Grid Panel。
这个有一点需要注意的是一个Volatile的概念,如果标志成Volatile的Widget每帧都会重新计算,一些属性的Widget Binding会使得Widget变成Volatile,Check Box放在Invalidation Box下会不起作用,需要设置成Volatile,建议自定义User Widget,用Button实现对应功能。
这就是引擎提供这样一个工具,叫InvalidationDebugging,开发者可以使用Slate.InvalidationDebugging找出Volatile,另外可以使用Slate.AlwaysInvalidate测试是否会突然卡顿。
有一个注意的是Invalidation Box自身会被标志成Volatile,一些重复使用的子控件建议不要Invalidation Box, 会有额外计算,Invalidation Box放在Retainer Box的下层。
接下来要讲一下可见性,除了可否可见以外还有是否可以接收点击测试,HitTestInvisible 可见、当前控件不可点击、所有子控件不可点击,SelfHitTestInvisible 可见、当前控件不可点击、不影响子控件,Hidden 不可见、占用布局空间,Collapsed 不可见、不占用布局空间。
如果大量的Visible会导致点击响应太慢,这个也会消耗很大的性能,Button设置成Visible,其它Widgets可以设置成Self Hit Test Invisible或Hit Test Invisible,Collapsed不占用布局空间, 略优于Hidden,Show/Collapse要优于AddToViewport/RemoveFromViewport。
这里还要讲一个是Widget Binding,某些属性上Widget Binding会导致对应Widget被放入Volatile List,这些属性发生变化,表示对应的控件需要重新计算Vertex Buffer,所以我们尽量避免这个Widget Binding。另外还有一点是Widget Binding会每帧Tick执行,这一点也会带来比较大的性能开销,所以手机上面建议使用C++ Event设置Widget属性。
目前UE4的UI开发对于C++是很好的,右边的编辑器里面进行了UI界面,不建议把复杂的逻辑放在蓝图Tick中执行,在C++中声明变量, 引擎会自动绑定编辑器中的Widget。
渲染线程优化
接下来介绍一下渲染线程优化,渲染线程首先介绍一个合并批次,我们在左图看到的是UI的有些可以合并批次,有些不可以合并批次,像不合并批次Canvas Panel、合并批次Grid Panel、Uniform Grid Panel、Vertical Box、Horizontal Box。
另外对于UI方面,我们可以使用Stat Slate查看批次,Num Batches,尽量使用可以批次的UI容器,但不用刻意追求合并批次。通过Sprite实现合并贴图功能。
接下来介绍一下UE4怎么合并贴图,这是我们合并贴图和贴完以后的情况,这是像素填充率,这里是背包界面的前5个Draw Call,后4个Draw Call的渲染面积很大,已经接近第一个背景图,可以看到UI的像素填充率非常高,这个时候我有接近5倍的面积,这个时候也有将近约5倍的Pixel Shader的执行次数,所以我们要提高像素填充率。
Retainer Box,将UI渲染到Render Target,再将Render Target 渲染到屏幕,另外引擎处理了点击响应区域的映射,鼠标点击区域引擎已经自动在屏幕上面映射了相应的测试。
Widget Render:将UI渲染到Render Target,Slate Render: 使用缓存的Render Target渲染Back Buffer,每隔3帧一个循环进行Retainer Box的更新,将1帧的UI渲染工作量分配到3帧去处理。
性能对比方面,关闭Retainer Box 7.7ms+0ms,开启Retainer Box是1.5ms+3.2ms,FPS提升由38到48。
Retainer Box 会占用额外的显存,因此建议仅在主界面上使用;Retainer Box区域尽量小,提高渲染效率、降低显存使用;Retainer Box会为每个User Widget实例创建一个Render Target, 因此重复使用的User Widget不要使用Retainer Box;游戏线程的Tick也会相应的隔几帧执行一次;持续表示的效果可以从Retainer Box中分离出来,但需要注意像素填充率;也可以从特效设计的方面解决;Invalidation Box放置在Retainer Box上方没有意义;推荐一个Retainer Box下跟一个Invalidation Box的方式;Retainer Box可以上材质效果。
另外需要注意的是,每隔3帧更新一次Retainer Box A,在第0帧更新;每隔5帧更新一次Retainer Box B,在第2帧更新;每隔15帧这两个Retainer Box就会同时更新,这样帧数变得不太稳定,导致帧数下降比较多,Phase Count的设置要全局考虑,避免重叠而导致帧数不稳定,所以必须做很好的控制。
Invalidation Box我们是每帧更新一次,但是我们很多时候可以做到根据事件触发,比如说背包穿戴了一个装备、卸下一个装备,按钮发生变化等等,这个时候可以根据事件更新,甚至不用每几帧更新一次,这样的话可能我们的UI交互不是很频繁,它的提升可能还是比较大的。
这就是我们的一个演示,如果打开了事件驱动的Retainer Box时,可以看到RTT的时间从3毫秒降低到0,最后可以看到我们这样一个复杂的界面,我们的游戏线程只花了1毫秒,渲染线程也花了1点多毫秒在小米4C上,而UE4是一个多线程渲染的,所以可能时间大概有11毫秒左右,当然事件驱动的Retainer Box刚才也说过了,对于频繁使用的UI不建议使用,所以可能最后需要看的是我们有多少频繁交互的事件,当然对于低端机的话带来很大的性能提升,如果我们有UI特效,可能在这个上面这种事件驱动没有办法更新,所以我们比较适合推荐这种方式在低端手机开启,首先关闭了UI特效。
这也是开发者比较关心的功能,左图有简单的材质,右图可以自动关闭材质和切换到低材质,这样可以兼顾高端机的效果和低端机的性能,DYNAMIC_MULTICAST的框架,这样程序可以变得更容易维护,开发也比较简单。
编程技巧
最后介绍UI方面的编程技巧,当然蓝图的话其实在大多数情况下性能都是没有问题的,但是如果我们要在低端机上面需要追求很好性能的话,其中有计算量比较大的逻辑,我们是不建议放在蓝图里面做,因为毕竟中间有很多的分装,建议可以把一些计算量比较复杂的逻辑下放在C++里面做,运行效率比蓝图高,更灵活,很多C++接口并未开放成蓝图接口,除了UI动画,其它代码都能用C++实现。
对于UI开发,我们建议开发者有Widget Manager,可以在蓝图中,也可以在C++中,就是管理所有User Widget,Brush、Font等资源也可以在Widget Manager中统一管理,这样的项目比较好管理,特别是UI比较多的时候。
接下来介绍一个怎么在UE4当中释放贴图内存,某些UI的贴图较大,这个时候应用程序希望可以在关闭UI后,释放对应贴图,这个时候要做一些简单的扩展,将UI贴图控件自定义成弱引用,管本这个UI空间以后这个内存就会释放掉。
UE4因为用GC回收内存,开发者并不是马上知道哪一块内存马上释放了,这个时候可以看到贴图还有哪些地方在引用,保证引用技术都是零,这个时候后面的GC可以释放它,可能一些图片被不知名的地方还在引用着。
这里还有一个小技巧3DRTT,这个小技巧并不需要每帧Tick,只要和动画频率大致同步就可以,所以我们要把每帧去渲染的两个选项关闭,同时这个蓝图我们设置成0.03秒Tick一次,产生在蓝图当中Tick这样的RTT,另外还有一个小细节就是Render Target的尺寸不要太大,会影响显存和渲染效率。
最后总结一下今天的技术点还有优先级,因为有些项目已经在开发中或者已经在后期,这个时候遇到UI导致的性能问题可以根据这个优先级做测试,前面讲到这些比较重要,包括下面合并批次容器,只要把这些设计好,我们移动项目的UI基本上不会有什么瓶颈了。