華文網

Epic Games工程師分享:如何在移動平臺上做UE4的UI優化?

隨著技術的不斷升級,高性能的引擎逐漸受到越來越多研發商的青睞,

UE4就是其中之一。在上周Unreal Open Day 2017活動會上,大宇就宣佈旗下經典IP《仙劍奇俠傳》、《軒轅劍》的續作,即《仙劍奇俠傳7》和《軒轅劍7》將採用虛幻4引擎開發的消息。

而從另一方面,用虛幻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基本上不會有什麼瓶頸了。

遊戲執行緒優化

接下來就開始介紹具體的優化方案,第一步是遊戲執行緒優化,這是一個小的事例,這個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基本上不會有什麼瓶頸了。