Unity MMORPG游戏优化经验分享
今天由Unity技術(shù)支持工程師高巖,根據(jù)實際的技術(shù)支持工作經(jīng)驗積累,分享如何對Unity MMORPG游戲進(jìn)行優(yōu)化。
在優(yōu)化Unity游戲時,我們一般從四個方面:CPU、GPU、內(nèi)存、工程配置等入手,它們都可能是影響游戲性能瓶頸的關(guān)鍵。
CPU
我們平常游戲的很多性能瓶頸都在CPU。例如:MONO內(nèi)存分配帶來CPU開銷,當(dāng)Mono內(nèi)存從50M、60M、70M,一直增大到100M,這些內(nèi)存分配都相當(dāng)于CPU的開銷。當(dāng)在Update函數(shù)中存在比較復(fù)雜的邏輯時,很容易出現(xiàn)每一幀都觸發(fā)內(nèi)存分配,如圖01所示。
?
雖然截圖中一幀里的GC Alloc只有0.6KB,但是當(dāng)游戲運行很長時間后,累計數(shù)量是相當(dāng)高的,這就讓每一幀都存在GC Alloc帶來的CPU開銷。
處理客戶端與服務(wù)器通信的數(shù)據(jù)包時,會存在序列化與反序列化,如果實現(xiàn)方式不合理時,會帶來多余的內(nèi)存分配。一般很多項目都現(xiàn)在使用Protobuff,如果是自行設(shè)計的數(shù)據(jù)包格式,就要考慮如何控制序列化與反序列化的內(nèi)存分配。
靜態(tài)數(shù)據(jù)表如果使用Json、xml等格式時,同時解析邏輯與數(shù)據(jù)結(jié)構(gòu)設(shè)計不良,在初始化數(shù)據(jù)表時容易由于過大的內(nèi)存分配而撐大MONO堆內(nèi)存。所以要在項目設(shè)計時找到最優(yōu)化的方式來實現(xiàn)功能需求與性能需求。
String是一個很常用的引用類型對象。當(dāng)代碼里存在字符串拼接、直接或間接調(diào)用ToString()函數(shù)時,會生成字符串的副本,也就產(chǎn)生了內(nèi)存分配。例如:調(diào)用Object.name屬性,即使每次返回值是固定的,依然是不同的String對象,因為這里每次返回都是一個對象拷貝。所以建議可以通過把這類字符串預(yù)先緩存,或者在打包時生成一個名字的列表作為靜態(tài)數(shù)據(jù),提供給運行時的邏輯直接讀取。
部分Unity內(nèi)置API在被調(diào)用時,都是返回對象拷貝。例如:Getcomponents、Sprite.Vertices、Input.Touches等。從設(shè)計角度是考慮代碼安全性,防止外部直接去修改真正的對象數(shù)據(jù)。所以,這些屬性返回值要做緩存。或者通過其他API來實現(xiàn)需求從而規(guī)避掉這個問題。請注意,Getcomponent只會在編輯器環(huán)境下存在內(nèi)存開銷,真機(jī)上不存在,大家在Profiling時不要被誤導(dǎo)。
通常Debug.Log一類的日志函數(shù)應(yīng)該只存在Debug階段,但是很多時候這些函數(shù)沒有屏蔽。如果它們出現(xiàn)在調(diào)用次數(shù)較多的邏輯中,就帶來額外的CPU開銷。同樣Warning和Log存在相同的情況。雖然日常在console或真機(jī)Log里常見,但是經(jīng)常沒有被處理。建議對待Warning也要找到它的觸發(fā)原因并解決,防止在Release中出現(xiàn)。Log函數(shù)不會因為打包為release版本就會自動屏蔽,需要使用宏定義來屏蔽。
閉包與匿名函數(shù)盡可能不要使用。閉包中調(diào)用外部變量,需要創(chuàng)建一個臨時class對象來包含外部變量并且傳給閉包函數(shù),從而帶來內(nèi)存開銷。匿名函數(shù)在作為一個函數(shù)的參數(shù)傳入時,也存在內(nèi)存分配。il2cpp中如果使用匿名函數(shù)當(dāng)參數(shù),不要用預(yù)聲明的函數(shù)。
ParticleSystem API在Unity 2017.2之前的版本中,Stop和Simulate內(nèi)部實現(xiàn)使用了閉包。粒子系統(tǒng)的一些API,例如:Start、Stop、Pause、Clear、Simulate在調(diào)用它們時會遞歸調(diào)用當(dāng)前粒子節(jié)點下面的所有子級節(jié)點,并會觸發(fā)GetComponent,這帶來了一定的CPU開銷。如果需要調(diào)這幾個方法的時候,函數(shù)參數(shù)withChildren可以設(shè)為false,不觸發(fā)遍歷子節(jié)點。在粒子對象初始化時,預(yù)存子節(jié)點,在需要時直接根據(jù)緩存的子節(jié)點列表分別調(diào)用它們的Start。
Camera.main的調(diào)用是存在開銷的,可以把Object.FindObjectWithTag(“MainCamera”)緩存下來來代替。調(diào)用射線檢測函數(shù)時應(yīng)該使用那些不存在開銷的函數(shù),例如Physics.RaycastNonAlloc。
當(dāng)Canvas重建時,會引起材質(zhì)的重新創(chuàng)建、排序、Mesh重建,這都會帶來CPU的開銷。當(dāng)Canvas內(nèi)容非常復(fù)雜的時候,每次重建很可能會帶來比較明顯的卡頓。UGUI里面的Mask會使用StencilBuffer,蒙版內(nèi)的元素是沒法和外面的元素做合批,即便在圖集與材質(zhì)都是相同的。這時可以用RectMask2D來實現(xiàn)蒙版,可以稍微降低一些開銷。Canvas上的GraphicRaycaster選項,在不需要有交互時可以不勾選。而Layout組件會涉及到節(jié)點的遍歷操作,都有內(nèi)存與CPU的開銷,如果能不用就不用它,或者自行硬編碼實現(xiàn)簡單的自動布局。
Canvas都建議做動靜分離,頻繁改動的元素和固定不變的元素分開到不同的Canvas。需要注意Canvas數(shù)量,數(shù)量多少根據(jù)UI的復(fù)雜程度、動靜分離的Canvas個數(shù)進(jìn)行測試,評估多少個Canvas是合理的。目前發(fā)現(xiàn)Unity2017.3中,出現(xiàn)過當(dāng)Canvas數(shù)量達(dá)到十幾個或更多時,帶來的開銷反而比不分拆時還大。
UI元素存在半透并很多元素進(jìn)行疊加,就導(dǎo)致OverDraw消耗比較大。可以通過減少疊加層數(shù)、縮小Sprite的空白區(qū)域等方式來控制。
當(dāng)Canvas 處于Worldspace或者Screen Space時,Canvas存在Event Camera或者Render Camera屬性,需要掛接Camera。此處若為None,運行時每幀都會有十幾次訪問它,底層默認(rèn)返回Camera.main。所以預(yù)先關(guān)聯(lián)Camera對象。
圖集的分類方式直接影響到UI的合批效率。二手手游轉(zhuǎn)讓除了幾個通用圖集外,其它圖集按UI模塊類型區(qū)分,一個或多個UI公用一套圖集。圖集的面積利用率要做到最高,避免圖集存在太多空白區(qū)域。而圖標(biāo)是分散還是合并到圖集上,要看項目實際情況,并沒有固定的規(guī)則。
UI背景圖不要出現(xiàn)NPOT尺寸,如果要用NPOT,嘗試多個NPOT圖合并為POT尺寸,或者美術(shù)對NPOT圖拉伸為POT,在Unity中還原為原始尺寸。
通常靜態(tài)合批通過給場景上的物體勾上Static實現(xiàn),但是有時會因為導(dǎo)致包體太大,改為運行時調(diào)用staticBatchingUtility.Combine進(jìn)行物件合并。但是運行時手動靜態(tài)合批會有不小的CPU開銷,同時Mesh可讀寫選項也開啟,在內(nèi)存中邊存在雙份的Mesh數(shù)據(jù),同時合并后模型也是一份新Mesh數(shù)據(jù)。建議可以用第三方插件Mesh Baker來進(jìn)行靜態(tài)合批。同時,各個模型的材質(zhì)也要針對靜態(tài)合批來制作,畢竟相同材質(zhì)的模型才可以合并。
動態(tài)合批對于大部分有Lightmap的模型是無效的,還存在900左右頂點的合批限制。在Unity 2017.3支持32bit Mesh index buffers,可以合并Mesh時支持更多的頂點,可以在FBX選項內(nèi)Index Format打開或者運行時設(shè)置Mesh.indexFormat。
骨骼蒙皮計算一般使用CPU Skinning,雖然引擎也是支持GPU skinning的,但需要注意性能瓶頸在CPU端還是GPU端。如果GPU端是性能瓶頸時,盲目打開GPU skinning,會變成一種負(fù)優(yōu)化。當(dāng)角色模型的骨骼數(shù)超過100根、150根時,某些身體部位的骨骼動畫,可以用BlendShapes代替。當(dāng)某一部位骨骼動畫不播放時,可以把這個部位的Animator組件關(guān)掉。Animation Instancing也是一個可以優(yōu)化大量角色動畫性能的手段。
物理系統(tǒng)中,MeshCollider的使用在場景比較復(fù)雜龐大時,Bake的性能比較差。可以通過配合射線檢測和自定義高度圖數(shù)據(jù)控制角色高度。
GPU
頂點數(shù)量的控制,首先要從美術(shù)方面,控制模型的合理面數(shù)。有的建筑物被遮擋了一部分,被遮擋部分可以減面甚至把這一塊摳掉留空。避免場景中出現(xiàn)大量小物體組合出一個更大的物件,設(shè)計之初就對零散物體合并材質(zhì)、貼圖、Mesh。場景地圖也可以分區(qū)塊制作、加載管理,同時配合LODGroup使用。還可以通過第三方插件Mesh Baker LOD輔助進(jìn)行。
?
紋理的尺寸會影響上傳紋理時帶寬的使用,也就是上傳耗時比較高。通常3D模型的紋理,都會把打開Mipmap,可以提高紋理采樣的質(zhì)量,降低命中耗時,提升IO速度。同時紋理過濾模式的選擇,對于UI紋理使用Bilinear足矣,Trilinear配合打開Mipmap后的插值計算,效果更好。
當(dāng)一個角色帶有一對翅膀,設(shè)置Mesh.alpha進(jìn)行隱藏或顯示,翅膀在Alpha=0時,依然被渲染。而顯示全屏UI時,它擋住了后面的主場景,但由于場景Camera未關(guān)閉使得場景依然被渲染,如果此時UI里還顯示角色模型,積累的渲染壓力就比較大,這些都會體現(xiàn)在Overdraw消耗上。
根據(jù)對Shader的功能需求,對復(fù)雜度要進(jìn)行控制。運算符要合理使用,變量的浮點精度要同時考慮計算需求和真機(jī)的實際支持的精度范圍。對Tex2D、紋理采樣的使用方式要合理,畢竟這類指令過多時會增加開銷。
Unity引擎自帶的Terrian系統(tǒng),可以通過分區(qū)塊或者轉(zhuǎn)為Mesh解決此部分性能瓶頸。我們可以通過插件Terrain Slicing & Dynamic Loading Kit來分割地形,并調(diào)整地形的尺寸和精度等配置參數(shù)。
?
一個特效包含粒子發(fā)射器的數(shù)量不能隨意創(chuàng)建,對渲染和內(nèi)存都有不小的負(fù)載。當(dāng)粒子存在發(fā)射Mesh的需要時,要控制Max Particles的數(shù)量。同時有些特效不一定要通過粒子系統(tǒng)實現(xiàn),可以通過各種變通方式或低負(fù)載的方式制作。
內(nèi)存
每一個Mesh的壓縮選項、Read/Write選項都要根據(jù)Mesh使用方式進(jìn)行單獨設(shè)置,同時要做好當(dāng)Mesh存在雙份數(shù)據(jù)時,CPU端數(shù)據(jù)的及時釋放。合理的減面也是必不可少的。
壓縮紋理的使用是毋庸置疑,而壓縮格式要根據(jù)項目的機(jī)型適配靈活選擇,保證質(zhì)量和體積都能滿足需要。當(dāng)編輯器中刷地形紋理時,需要紋理開啟Read/Write,而在打包時要關(guān)閉這個選項。
每個紋理的尺寸要根據(jù)它的用途、實際測試時內(nèi)存占用的情況,進(jìn)行合理的限制,不能隨意設(shè)定它。對于圖集需要最大限度利用面積,避免浪費寶貴的內(nèi)存。另外當(dāng)紋理使用ETC2、ASTC格式時,在不支持這些格式的設(shè)備上,壓縮紋理會被fallback為無壓縮的RGBA格式,不但增大了內(nèi)存占用,同時增加了fallback的CPU開銷。
AnimationClip可以通過壓縮浮點數(shù)精度,剔除無用的scale曲線降低內(nèi)存占用。同時AnimationClip加載策略也對內(nèi)存占用有很大影響,全部預(yù)加載還是按需異步加載,需要根據(jù)項目實際情況決定。
Mono進(jìn)行內(nèi)存分配時,在不同類型的數(shù)據(jù)對象在內(nèi)存中是相鄰的存在內(nèi)存塊里,如果說釋放了一個數(shù)組,它所占的內(nèi)存被釋放了。但是這個區(qū)域是不會還給系統(tǒng)內(nèi)存,依然保留著。接著又創(chuàng)建了新的對象,新對象的內(nèi)存大小比剛才被釋放的空間大,就無法直接放入這個空間,只能由Mono申請一份新的內(nèi)存來存放。當(dāng)Mono申請新內(nèi)存時,Mono堆內(nèi)存一般會擴(kuò)大很大一部分,如見下圖05所示。
?
在使用數(shù)組類型的對象時,如果初始化時時非定長數(shù)組,數(shù)組實際容量會根據(jù)Add操作以0、4、8、16、32倍逐步擴(kuò)大,其中大量空間為Null,浪費了內(nèi)存。這種情況常出現(xiàn)在客戶端初始化數(shù)據(jù)表保存到List、Dictionary時。
當(dāng)我們需要手動釋放一些對象的內(nèi)存時,會有很多種方式,Unity提供了很多卸載各種資源的函數(shù)。主動調(diào)GC.collect是不必要的,如果一個對象的引用不是Null時,是不可能釋放它的。GC只需要做好對象引用的清理就可以,剩下的還是由GC機(jī)制自動管理更好。我們可以通過自定義內(nèi)存池和資源管理器,來很精細(xì)的控制每一種資源的生命周期。
AssetBundle壓縮格式一般使用LZ4,但要注意AssetBundle的合理Unload時機(jī)。而LZMA格式,由于存在加載時解壓后重壓縮為LZ4的開銷,一般情況下不建議使用。主Bundle卸載時,與它關(guān)聯(lián)的依賴Bundle一定要根據(jù)引用計數(shù)來控制是否可以卸載,否則依賴Bundle的Asset容易引發(fā)內(nèi)存泄露。
IL2CPP在安卓系統(tǒng)使用時,要注意libil2coo.so的文件大小。在安卓系統(tǒng)中,so會在游戲啟動后直接加載在內(nèi)存中,它的內(nèi)存占用大小基本上和文件大小差不多。所以so的尺寸要有所控制,否則會影響整個游戲的內(nèi)存數(shù)值。所以,使用il2cpp時要注意值類型的泛型、重復(fù)代碼等容易增大il2cpp的cpp代碼體積的情況。
其它
在PhysicsManagerSetting的LayerCollisionMatrix去掉不參加碰撞檢測的layer。Time Manager中的fixed time step要根據(jù)物理系統(tǒng)的使用情況設(shè)置間隔時長。游戲分辨率要通過高中低配置來動態(tài)調(diào)整。
Graphics Stettings和內(nèi)置Shader有關(guān)的開關(guān)根據(jù)項目使用情況來有選擇的打開或關(guān)閉。同時建議所有Shader都要打包為Bundle來加載初始化。
項目的性能優(yōu)化工作應(yīng)該每隔一階段就進(jìn)行一次性能分析評估,及時解決掉性能瓶頸。同時應(yīng)該有專人負(fù)責(zé)這一項工作,提高執(zhí)行力。
雖然Unity Asset Store資源商店提供的各種插件功能強(qiáng)大,但是插件內(nèi)部的一些邏輯沒有考慮到移動平臺的應(yīng)用環(huán)境,存在很多不良代碼,需要開發(fā)者仔細(xì)檢查插件源代碼,根據(jù)情況進(jìn)行改進(jìn)。并在性能測試時觀察是否存在插件帶來的性能瓶頸。
通常在對項目進(jìn)行性能分析時,會有很多工具輔助我們進(jìn)行分析工作。下面是我們推薦的工具:
?
- Xcode & Instrunments
- RenderDoc
- Snapdragon Profiler
小結(jié)
Unity MMORPG游戲優(yōu)化經(jīng)驗分享就為大家介紹到這里,更多Unity優(yōu)化經(jīng)驗分享請訪問Unity官方
中文論壇(Unitychina.cn)!
總結(jié)
以上是生活随笔為你收集整理的Unity MMORPG游戏优化经验分享的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《Exploring in UE4》多线
- 下一篇: 使用Unity从零点五开始,做半个不能玩