Unity MMORPG游戲項目優化


文章轉載於https://www.gameres.com/812928.html

 

在優化Unity游戲時,我們一般從四個方面:CPU、GPU、內存、工程配置等入手,它們都可能是影響游戲性能瓶頸的關鍵。

CPU

我們平常游戲的很多性能瓶頸都在CPU。例如:MONO內存分配帶來CPU開銷,當Mono內存從50M、60M、70M,一直增大到100M,這些內存分配都相當於CPU的開銷。當在Update函數中存在比較復雜的邏輯時,很容易出現每一幀都觸發內存分配,如圖01所示。

圖 01


雖然截圖中一幀里的GC Alloc只有0.6KB,但是當游戲運行很長時間后,累計數量是相當高的,這就讓每一幀都存在GC Alloc帶來的CPU開銷。

處理客戶端與服務器通信的數據包時,會存在序列化與反序列化,如果實現方式不合理時,會帶來多余的內存分配。一般很多項目都現在使用Protobuff,如果是自行設計的數據包格式,就要考慮如何控制序列化與反序列化的內存分配。

靜態數據表如果使用Json、xml等格式時,同時解析邏輯與數據結構設計不良,在初始化數據表時容易由於過大的內存分配而撐大MONO堆內存。所以要在項目設計時找到最優化的方式來實現功能需求與性能需求。

String是一個很常用的引用類型對象。當代碼里存在字符串拼接、直接或間接調用ToString()函數時,會生成字符串的副本,也就產生了內存分配。例如:調用Object.name屬性,即使每次返回值是固定的,依然是不同的String對象,因為這里每次返回都是一個對象拷貝。所以建議可以通過把這類字符串預先緩存,或者在打包時生成一個名字的列表作為靜態數據,提供給運行時的邏輯直接讀取。

部分Unity內置API在被調用時,都是返回對象拷貝。例如:Getcomponents、Sprite.Vertices、Input.Touches等。從設計角度是考慮代碼安全性,防止外部直接去修改真正的對象數據。所以,這些屬性返回值要做緩存。或者通過其他API來實現需求從而規避掉這個問題。請注意,Getcomponent只會在編輯器環境下存在內存開銷,真機上不存在,大家在Profiling時不要被誤導。

通常Debug.Log一類的日志函數應該只存在Debug階段,但是很多時候這些函數沒有屏蔽。如果它們出現在調用次數較多的邏輯中,就帶來額外的CPU開銷。同樣Warning和Log存在相同的情況。雖然日常在console或真機Log里常見,但是經常沒有被處理。建議對待Warning也要找到它的觸發原因並解決,防止在Release中出現。Log函數不會因為打包為release版本就會自動屏蔽,需要使用宏定義來屏蔽。

閉包與匿名函數盡可能不要使用。閉包中調用外部變量,需要創建一個臨時class對象來包含外部變量並且傳給閉包函數,從而帶來內存開銷。匿名函數在作為一個函數的參數傳入時,也存在內存分配。il2cpp中如果使用匿名函數當參數,不要用預聲明的函數。

ParticleSystem API在Unity 2017.2之前的版本中,Stop和Simulate內部實現使用了閉包。粒子系統的一些API,例如:Start、Stop、Pause、Clear、Simulate在調用它們時會遞歸調用當前粒子節點下面的所有子級節點,並會觸發GetComponent,這帶來了一定的CPU開銷。如果需要調這幾個方法的時候,函數參數withChildren可以設為false,不觸發遍歷子節點。在粒子對象初始化時,預存子節點,在需要時直接根據緩存的子節點列表分別調用它們的Start。

Camera.main的調用是存在開銷的,可以把Object.FindObjectWithTag(“MainCamera”)緩存下來來代替。調用射線檢測函數時應該使用那些不存在開銷的函數,例如Physics.RaycastNonAlloc。

當Canvas重建時,會引起材質的重新創建、排序、Mesh重建,這都會帶來CPU的開銷。當Canvas內容非常復雜的時候,每次重建很可能會帶來比較明顯的卡頓。UGUI里面的Mask會使用StencilBuffer,蒙版內的元素是沒法和外面的元素做合批,即便在圖集與材質都是相同的。這時可以用RectMask2D來實現蒙版,可以稍微降低一些開銷。Canvas上的GraphicRaycaster選項,在不需要有交互時可以不勾選。而Layout組件會涉及到節點的遍歷操作,都有內存與CPU的開銷,如果能不用就不用它,或者自行硬編碼實現簡單的自動布局。

Canvas都建議做動靜分離,頻繁改動的元素和固定不變的元素分開到不同的Canvas。需要注意Canvas數量,數量多少根據UI的復雜程度、動靜分離的Canvas個數進行測試,評估多少個Canvas是合理的。目前發現Unity2017.3中,出現過當Canvas數量達到十幾個或更多時,帶來的開銷反而比不分拆時還大。

UI元素存在半透並很多元素進行疊加,就導致OverDraw消耗比較大。可以通過減少疊加層數、縮小Sprite的空白區域等方式來控制。

當Canvas 處於Worldspace或者Screen Space時,Canvas存在Event Camera或者Render Camera屬性,需要掛接Camera。此處若為None,運行時每幀都會有十幾次訪問它,底層默認返回Camera.main。所以預先關聯Camera對象。

圖集的分類方式直接影響到UI的合批效率。除了幾個通用圖集外,其它圖集按UI模塊類型區分,一個或多個UI公用一套圖集。圖集的面積利用率要做到最高,避免圖集存在太多空白區域。而圖標是分散還是合並到圖集上,要看項目實際情況,並沒有固定的規則。

UI背景圖不要出現NPOT尺寸,如果要用NPOT,嘗試多個NPOT圖合並為POT尺寸,或者美術對NPOT圖拉伸為POT,在Unity中還原為原始尺寸。

通常靜態合批通過給場景上的物體勾上Static實現,但是有時會因為導致包體太大,改為運行時調用staticBatchingUtility.Combine進行物件合並。但是運行時手動靜態合批會有不小的CPU開銷,同時Mesh可讀寫選項也開啟,在內存中邊存在雙份的Mesh數據,同時合並后模型也是一份新Mesh數據。建議可以用第三方插件Mesh Baker來進行靜態合批。同時,各個模型的材質也要針對靜態合批來制作,畢竟相同材質的模型才可以合並。

圖 02


動態合批對於大部分有Lightmap的模型是無效的,還存在900左右頂點的合批限制。在Unity 2017.3支持32bit Mesh index buffers,可以合並Mesh時支持更多的頂點,可以在FBX選項內Index Format打開或者運行時設置Mesh.indexFormat。

骨骼蒙皮計算一般使用CPU Skinning,雖然引擎也是支持GPU skinning的,但需要注意性能瓶頸在CPU端還是GPU端。如果GPU端是性能瓶頸時,盲目打開GPU skinning,會變成一種負優化。當角色模型的骨骼數超過100根、150根時,某些身體部位的骨骼動畫,可以用BlendShapes代替。當某一部位骨骼動畫不播放時,可以把這個部位的Animator組件關掉。Animation Instancing也是一個可以優化大量角色動畫性能的手段。

物理系統中,MeshCollider的使用在場景比較復雜龐大時,Bake的性能比較差。可以通過配合射線檢測和自定義高度圖數據控制角色高度。

GPU

頂點數量的控制,首先要從美術方面,控制模型的合理面數。有的建築物被遮擋了一部分,被遮擋部分可以減面甚至把這一塊摳掉留空。避免場景中出現大量小物體組合出一個更大的物件,設計之初就對零散物體合並材質、貼圖、Mesh。場景地圖也可以分區塊制作、加載管理,同時配合LODGroup使用。還可以通過第三方插件Mesh Baker LOD輔助進行。

圖 03


紋理的尺寸會影響上傳紋理時帶寬的使用,也就是上傳耗時比較高。通常3D模型的紋理,都會把打開Mipmap,可以提高紋理采樣的質量,降低命中耗時,提升IO速度。同時紋理過濾模式的選擇,對於UI紋理使用Bilinear足矣,Trilinear配合打開Mipmap后的插值計算,效果更好。

當一個角色帶有一對翅膀,設置Mesh.alpha進行隱藏或顯示,翅膀在Alpha=0時,依然被渲染。而顯示全屏UI時,它擋住了后面的主場景,但由於場景Camera未關閉使得場景依然被渲染,如果此時UI里還顯示角色模型,積累的渲染壓力就比較大,這些都會體現在Overdraw消耗上。

根據對Shader的功能需求,對復雜度要進行控制。運算符要合理使用,變量的浮點精度要同時考慮計算需求和真機的實際支持的精度范圍。對Tex2D、紋理采樣的使用方式要合理,畢竟這類指令過多時會增加開銷。

Unity引擎自帶的Terrian系統,可以通過分區塊或者轉為Mesh解決此部分性能瓶頸。我們可以通過插件Terrain Slicing & Dynamic Loading Kit來分割地形,並調整地形的尺寸和精度等配置參數。

圖 04


一個特效包含粒子發射器的數量不能隨意創建,對渲染和內存都有不小的負載。當粒子存在發射Mesh的需要時,要控制Max Particles的數量。同時有些特效不一定要通過粒子系統實現,可以通過各種變通方式或低負載的方式制作。

內存

每一個Mesh的壓縮選項、Read/Write選項都要根據Mesh使用方式進行單獨設置,同時要做好當Mesh存在雙份數據時,CPU端數據的及時釋放。合理的減面也是必不可少的。

壓縮紋理的使用是毋庸置疑,而壓縮格式要根據項目的機型適配靈活選擇,保證質量和體積都能滿足需要。當編輯器中刷地形紋理時,需要紋理開啟Read/Write,而在打包時要關閉這個選項。

每個紋理的尺寸要根據它的用途、實際測試時內存占用的情況,進行合理的限制,不能隨意設定它。對於圖集需要最大限度利用面積,避免浪費寶貴的內存。另外當紋理使用ETC2、ASTC格式時,在不支持這些格式的設備上,壓縮紋理會被fallback為無壓縮的RGBA格式,不但增大了內存占用,同時增加了fallback的CPU開銷。

AnimationClip可以通過壓縮浮點數精度,剔除無用的scale曲線降低內存占用。同時AnimationClip加載策略也對內存占用有很大影響,全部預加載還是按需異步加載,需要根據項目實際情況決定。

Mono進行內存分配時,在不同類型的數據對象在內存中是相鄰的存在內存塊里,如果說釋放了一個數組,它所占的內存被釋放了。但是這個區域是不會還給系統內存,依然保留着。接着又創建了新的對象,新對象的內存大小比剛才被釋放的空間大,就無法直接放入這個空間,只能由Mono申請一份新的內存來存放。當Mono申請新內存時,Mono堆內存一般會擴大很大一部分,如見下圖05所示。

圖 05


在使用數組類型的對象時,如果初始化時時非定長數組,數組實際容量會根據Add操作以0、4、8、16、32倍逐步擴大,其中大量空間為Null,浪費了內存。這種情況常出現在客戶端初始化數據表保存到List、Dictionary時。

當我們需要手動釋放一些對象的內存時,會有很多種方式,Unity提供了很多卸載各種資源的函數。主動調GC.collect是不必要的,如果一個對象的引用不是Null時,是不可能釋放它的。GC只需要做好對象引用的清理就可以,剩下的還是由GC機制自動管理更好。我們可以通過自定義內存池和資源管理器,來很精細的控制每一種資源的生命周期。

AssetBundle壓縮格式一般使用LZ4,但要注意AssetBundle的合理Unload時機。而LZMA格式,由於存在加載時解壓后重壓縮為LZ4的開銷,一般情況下不建議使用。主Bundle卸載時,與它關聯的依賴Bundle一定要根據引用計數來控制是否可以卸載,否則依賴Bundle的Asset容易引發內存泄露。

IL2CPP在安卓系統使用時,要注意libil2coo.so的文件大小。在安卓系統中,so會在游戲啟動后直接加載在內存中,它的內存占用大小基本上和文件大小差不多。所以so的尺寸要有所控制,否則會影響整個游戲的內存數值。所以,使用il2cpp時要注意值類型的泛型、重復代碼等容易增大il2cpp的cpp代碼體積的情況。

其它

在PhysicsManagerSetting的LayerCollisionMatrix去掉不參加碰撞檢測的layer。Time Manager中的fixed time step要根據物理系統的使用情況設置間隔時長。游戲分辨率要通過高中低配置來動態調整。

Graphics Stettings和內置Shader有關的開關根據項目使用情況來有選擇的打開或關閉。同時建議所有Shader都要打包為Bundle來加載初始化。

項目的性能優化工作應該每隔一階段就進行一次性能分析評估,及時解決掉性能瓶頸。同時應該有專人負責這一項工作,提高執行力。

雖然Unity Asset Store資源商店提供的各種插件功能強大,但是插件內部的一些邏輯沒有考慮到移動平台的應用環境,存在很多不良代碼,需要開發者仔細檢查插件源代碼,根據情況進行改進。並在性能測試時觀察是否存在插件帶來的性能瓶頸。

通常在對項目進行性能分析時,會有很多工具輔助我們進行分析工作。下面是我們推薦的工具:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM