1. 程式人生 > >Unity3d-MMO遊戲優化經驗分享沙龍總結

Unity3d-MMO遊戲優化經驗分享沙龍總結

昨天去上海蔘加了UWA公司的張鑫和張強進行了一場關於MMO遊戲開發和效能優化的沙龍,活動連結為:UWA優化日上海站|傳統MMO手遊效能該如何突圍?。雖然第二場場景分塊載入部分的內容沒有預期中對目前的專案那麼有幫助,再加上最後因為趕火車沒有聽完Q&A的環節就離開了(然而還是沒有趕上火車,此中悲苦就不提了。。。),有些疑問沒有提出來討論,略有遺憾,但對於我這樣一個剛剛接觸Unity的新兵來說,收穫頗豐。因此這裡做一個簡單的記錄和整理,只包含現場討論我印象比較深的一些點,與講座內容的順序沒有直接對應關係,更加詳細、條目性的內容可以參考官方放出的正式文件。

0. 關於UWA

張鑫博士介紹了UWA成立這一年多時間內做過的事情,我還是比較讚賞有組織在做這件事情的,可以說是Unity引擎和使用者之間除了官網文件和官網技術支援之外的一座橋樑。雖然說他們目前的存在價值和盈利方式依賴於Unity引擎和這樣一個生態圈,但是對於程式設計師來說,它們比單純的引擎部門接近遊戲產品,和做產品的比又更純粹,沒那麼功利,介於兩者之間。如果有合適的機會我還是挺想做一做這樣的事情的,哈哈~

目前來說,能有這樣的一群人幫忙提供一些技術上的分享,一些坑可以拿出專門的時間和精力去踩和分析,對於沒有引擎組的小團隊來說是非常好的事情。

張鑫博士提到他們提供熱更新方案,會後單獨問了一下還是隻支援Android的版本,Lua方案的熱更新還沒有人力去做。希望後面可以有人力來做一些這方面的踩坑和效能優化工作……

1. UI系統中的Mesh重建

在UI系統的優化中,著重提到了Mesh重建的過程,這通常是一個消耗最大的部分,因此也是優化需要關注的重點。在NGUI中,這一過程發生在Panel的LateUpdate函式中——由於NGUI是外掛的形式,因此這一過程是在C#中的,每次需要改變渲染網格時都會銷燬記憶體中的mesh進行重建操作。而對於UGUI來說,這一過程是在Cavas的BuildBatch中進行的,已經被封裝成了Native的實現,因此在UGUI中這部分沒有額外的堆記憶體分配

,這也是UWA官網把UGUI在執行時分配的堆記憶體大小標準定義為小於2M,而NGUI的建議標準是小於20M的核心原因。

那麼,如何降低UI系統中Mesh重建的次數呢?主要從以下兩點出發:

  • 當ui發生改變,比如有元件的新增、刪除、遮擋關係改變等事情發生的時候會需要重建Mesh,而當ui的transform發生改變的時候,如果不影響原本的遮擋關係,是不會導致Mesh重建的過程的
  • UGUI中Mesh的重建的基本單位是Canvas

上述兩點分別對應兩個優化思路:

  • 儘量減少Mesh重建發生的次數;
  • 當一定需要Mesh重建的時候,儘量減少Mesh重建影響的範圍;

針對第一點,除了不要進行不必要的ui修改操作之外,對於需要頻繁切換的複雜介面

,張鑫博士給出的建議是:

"不要使用例項化/銷燬操作來實現介面的切換,會有很大的額外開銷,對於active/deactive操作來說也會有mesh重建的過程。推薦通過將其座標移出視窗之外,或者通過Camera的Layer機制來隱藏介面這兩種方式具有最好的效能效果。"

一個使用案例是揹包介面,有大量的item,建立過程消耗也會很大,如果記憶體允許的話,使用上述方法會有最小的效能開銷。

而儘量減少Mesh重建影響的範圍則對應一個基本原則——動靜分離,即把不需要動態改變和需要頻繁地動態改變的元件分離出來。按照上述的第二點,可以使用canvas進行動靜分離。就是說比如某個元件下面掛的東西是一個會動態變化的部分,那就把它獨立成一個canvas。比如仍然以揹包介面為例,滑動內容會導致介面的變化,在滑動過程中每幀都會有Mesh的重建操作。把滾動的部分做成單獨的Canvas,這樣背景圖片和標題等部分就不會被每幀重建了。

這裡需要著重注意的是一些會頻繁改變的動態部分,比較容易遺忘,比如聊天中的動態表情,角色座標資訊的顯示等部分,在開發過程中需要著重注意。

這裡我的一個疑問是如果劃分過細,過多的Canvas是否會導致UGUI有額外開銷?比如是否不同Canvas上的介面元素即使使用Batch後的圖,也無法合併Draw Call?這是在開發中需要注意權衡的一個點。

之前有討論過動靜分離的概念,但是進行分離的基本規則和原理並不是很清楚,張鑫博士的沙龍從效能分析的具體資料出發,結合具體函式的作用,講解得清晰透徹。
另外有幾點關於ui優化的筆記:

  • NGUI中UITexture不會被合併,建議使用UISprite;(我們專案目前使用UGUI,因此NGUI的不是很熟悉,不知道UGUI中有沒有無法合併的控制元件?)
  • UI應該儘量避免重疊,尤其是看上去不重疊但是實際上有半透區域存在重疊,會導致Unity不進行合併操作,增加Draww Call數量。
  • 在Unity 5.2之後的版本中,ui控制元件的z值不為0的情況下不會進行合併。(不知道這是為了解決遮擋或者什麼問題而修改的特性,還是Bug,在製作的時候注意一下,尤其是做角色的3D血條有深度更改需求的情況下。)
  • 揹包介面的優化中提到了PixelPerfect的設定,這是一個對齊畫素的效果可以讓字型等顯示的效果更好,比如在揹包內容滾動的時候,每幀都會有比較大的消耗在這上面,可以考慮關閉掉
  • UI中Mesh需要的頂點數量如果可以控制在1000以內,Mesh合併的效率就會比較好。過多的頂點數量在全部是靜態的情況下問題不大,但是一旦涉及到動態介面就會造成卡頓。
  • UI部分的Draw Call建議控制在20~30之間,是一個比較理想的情況。

2. 血條優化案例

UWA做了一個血條效能分析的Demo,也是和UI有關。

通常MMO遊戲中血條會比較多,比如一些PVP或者PVE玩法中,血條會有幾十個甚至上百個,他們會隨著怪物的死亡等消失,又會隨著新的怪物產生而重新出現。這裡需要注意的點有如下幾個:

  • 通常比較直接的思路是建立一個緩衝池,用完丟回去,需要的時候先衝池子裡拿。這裡依然會有一些效能問題,因為放回緩衝池的操作和重新放回場景中的操作會有SetParent的過程,在UGUI中這一過程會導致控制元件進行一系列的初始化操作,造成卡頓等問題。UWA的建議是使用移除視窗之外的操作來代替SetParent操作,可以提升效能。
  • 在掉血跳字等字型的部分美術同學喜歡使用Outline等效果,一次Outline對於一個字來說會多繪製上下左右4遍,因此對於內容可控的部分建議直接使用 靜態字型
  • 對於頻繁出現又消失的戰鬥提示資訊等部分,可以使用.text = ”“的賦值操作,即把文字的內容賦值為空的方式來代替Active和Deactive。
  • 如果血條非常多的情況下,可以考慮拆分成多個Canvas,會有一些意想不到的優化效果。UWA做測試觀察到現象是每個Canvas的效能消耗與其中的血條數量不是成正比的,而是一種超線性的關係,這可以理解,在每一控制元件都可能發生變化的情況下,需要重建整個Canvas,那這是一個n*n的關係。(當然說是n^2的關係也不準確,因為每幀改變1個和改變n個都只會重建一次,但是從統計概率上來說,數量越多,每幀需要進行Mesh重建的概率就越大,重建消耗也是越大的,因此是超線性的。)這裡還是一個需要進行Draw Call和重建消耗的折中考慮的點。

3. 多執行緒渲染

多執行緒渲染是我在網易的時候跟過的一個大坑。。。當然不是我做的開發,而是我們專案比較早在使用引擎組做的這一功能,從整合過程到做相容性測試遇到過很多問題,比如某些裝置上莫名其妙的Crash。

張鑫博士說他們從他們的經驗來看,Unity 5.3版本之後多執行緒渲染的功能已經是一個比較穩定的版本了,現在已經有正在運營的專案在開啟多執行緒渲染。因此整體上還是可以比較放心地開啟的。多執行緒渲染對於PostEffect的提升效果很大,他們做了一個測試可以讓CPU消耗從平均20ms降低到平均2ms,也有專案開始之後出現頓卡。

與之前瞭解的一樣,開啟多執行緒渲染之後的效能提升效果根本上和是CPU瓶頸還是GPU瓶頸有關,不同裝置效果不同,不同遊戲的瓶頸也不同,因此要各個專案自己測試來看,因此UWA官方的說法是——

”推薦各個專案嘗試開啟。“

這部分提到如果在觀察Profile面板時發現WaitingForJob很高,就說明CPU在等子執行緒,就已經有效能問題了,當出現PutGeometryJobFence的時候,效能問題就已經很嚴重了。

4. 動態陰影的技術方案

MMO開發中角色陰影已經成為了一個標配,UWA經過測試,Unity官方的Build-in ShadowMap的效能還是最好的,推薦使用,只是效果相對差,而且在Mobile裝置上無法支援原生的軟影(存疑,需要自己測試一下效果)。

而Projector的實現方式比較適合只有一個角色需要陰影的情況,推薦了兩款外掛:

  • Fast Shadow Projector 只支援靜態物體;
  • Fast Shadow Receivor 可以將接受陰影的物體拆分出需要陰影的獨立的mesh,提升渲染速度,可以支援柱子這樣非平面的物體。

陰影這塊是我們專案目前要研究的重點內容之一,我們在考慮全部動態陰影的方案來部分替代烘焙的LightMap。這部分市面上的產品只觀察到韓國的兩款Unreal的手遊這麼來做,之前在網易內部使用Forward Lighting一直都是烘焙的方案。這塊如果有朋友瞭解什麼資訊還希望不吝賜教~

5. 資源的同步和非同步載入

這是比較有意思的一塊內容,之前的思路一直都是大部分情況下統一使用非同步載入,只有在明確知道資源非常小,而且不重要的模組使用同步載入的方案,比如一個只有幾個面的dummy model之類的。

UWA給出的建議是:在Loading介面中,如果不需要表現平滑的載入進度和載入介面的話,可以使用同步載入,其他的過程中使用非同步載入。原因是——

”我們觀察到,在對於同一個資源,同步載入比非同步載入所花費的時間要少很多。非同步載入會把任務拆分成比較小的粒度到每幀執行,但是在裝置上每幀33ms的時間中,往往用不了這麼久非同步載入任務就執行完畢了,比如16ms甚至更少的時間,這就導致了無謂的等待。”

如果覺得載入時間過長,而對於載入過程中的玩家體驗不需要過多關注的專案可以參考這一思路。這的確是我之前的經驗所沒有包含的部分。

另外,在Unity中,對於非同步載入來說,也可能會造成頓卡,因為不是所有的過程都是非同步的。IO部分可以做到完全非同步,但是記憶體中的初始化的部分過程可能仍然是同步的,比如一張10241024大小的貼圖非同步載入,通常裝置上都會可以感受到卡頓,因此建議大於這個尺寸的貼圖統一進行預載入*。

與此相關的還有一個Unity的小知識。Unity預設每幀給2ms的時間讓CPU拷貝記憶體東西到GPU中,比如貼圖、網格頂點等,預設的Buffer是4M大小,因此這裡也會影響資源載入到最終渲染到螢幕上的時間。這兩個引數是可以調整的,具體介面參考官方文件。另外預設4M正好是一張32位的10241024大小的貼圖大小,如果使用了20482048或者更大的貼圖格式,這個Buffer會增大為對應的大小,並且不會在縮小回來。因此建議資源中的最大尺寸可以給一個定義,儘量不要出現只有偶爾幾張貼圖使用非常大的尺寸的情況。

6. 動畫模組的優化

除了常規的降低骨骼數量和動畫曲線數量之外,Unity 5.2之後提供了一個culling Group的功能,用在模型位置一直繫結一個球的方式做碰撞體來優化判定,不在視錐範圍的的物體不進行Animator的Update。這一功能主要針對指令碼邏輯的Update,Animator沒有提供單獨的介面根據距離來控制Update頻路,一個可行的思路是自己重寫其Update介面,然後傳入更高的Delta Time來模擬降頻的功能。不過以我之前用Havok的降頻功能來做效能對比的話,除非角色數量非常高,否則這部分骨骼骨骼的更新的優化空間不是非常大,不過Unity這塊具體的資料要進行測試才知道。

Animator中有一個Optimize Game Objets的選項,可以降低Update的消耗,UWA建議使用。這是因為預設情況下每根骨骼都是一個GameObject,每幀骨骼更新之後會需要修改它們的Transform。

"當開啟這個選項,匯入的角色中的遊戲物件transform hierarchy將會被移除,而且以Avatar and Animator元件替代。
角色的SkinnedMeshRenderer將會直接使用Mecanim內部骨骼,因此我們能擺脫所有用於描述骨頭的Transform。
這個選項將提升動畫角色的效能,推薦最終產品開啟這個選項。優化模式下,面板網格模型的抽取也是多執行緒的。
當開啟了這個選項,使用者能在ModelImporter inspector中指定“Extra Transforms to Expose”的列表。例如,如果你想附加一把劍道右手,這是一個掛載點。暴露的transform在遊戲物件的hierarchy中是平行的,不管它在骨架檢視中的深度"

對於Animator的Active和Deactive的操作有很大的效能消耗,這在之前鬥魚上的直播中已經提到過了,這裡還是像ui一樣,建議將角色移出視窗之外的方式來進行快取,或者只把元件Active和Deactive,來提升效能。

7. 其他的小tips

除了上述的一些問題之外,還有一些比較了零碎的筆記,不進行贅述,只記錄如下:

  • 渲染面數建議控制在10w面一下,這是目前經過大量測試價效比比較高的一個點,5w-10w面測試看下來差別不是很大,Draw Call數量建議控制在200-300以內,比較好的情況是在100以下。
  • 粒子系統通常需要載入的資源很多,但是初始化過程比較消耗CPU,因為通常一個粒子系統中的Component很多,建議進行預載入。
  • 粒子系統中的PreWarn選項會在後臺進行一次完整週期的模擬,因此使用可能會有卡頓。
  • Skined Mesh引擎是不會進行合併的,MeshBaker外掛可以減低Draw Call數量,對於動態的物體也可以,但是會影響裁剪,而且動態新增和刪除很慢,通常用於ARPG遊戲中的優化,MMO較為少用。
  • 注意建立Mesh和Material的拷貝過程,比如修改一個Material的引數,會建立一個新的Material物件,頻繁地執行這樣的操作會有洩漏出現,推薦使用DynamicMaterial,快取然後只修改這一個動態材質的方法。

8. 大世界場景拆分和動態載入

這一部分是張強同學做的講座,基於地形的方式實現了的大世界動態載入功能。其實這部分本來是我期望去聽和討論的部分,以為我們專案正好在進行這塊的技術預研,但是我們不是使用地形,而是基於靜態Mesh,另外視角我們更傾向於平視而非2.5D,因此這部分對於我們的幫助沒有想象中的大。

我個人覺得這部分的一個問題是整個工程是基於一個Demo性質的實現,而非正式的專案,因為時間關係沒有在後面進行深入的交流,因此也不清楚目前的實現是否在正式的專案中應用了。一些應用方面的疑問其實講座正文中沒有講到:

  • 結合到美術製作,地塊的拆分建議遵循的原則是什麼?比如多少個螢幕範圍劃分為一塊比較合理?
  • 如果使用lightmap的方案,兩個地塊交接處是否會有問題,比如交界處左側有一座山,它的投影可能會在另外一個拆分後的地塊上,如果拆分後再Bake,是否有解決方案可以處理這樣的問題?
  • 如果使用平視視角,目前有沒有什麼比較好的解決方案?LOD的話有哪些注意事項?

這部分可以直接參考官方給出的PPT,我做的筆記不太多,這裡只放了一些沒有來及提出的問題,幸好加了兩位主持人的微信,回頭整理好問題再一併請教,有答覆了再修改本文。

9. 總結

這次上海之行,一天時間往返上海杭州,只為了這兩場講座。從收穫來說,雖然和預期稍有不同,但是還是很值得的。感謝張鑫博士和張強同學兩位主持人的分享,你們辛苦啦也感謝UWA公司組織這樣免費的技術沙龍,祝願貴公司越來越好~

2016年11月26日於杭州家中