閱讀時間:15分鐘左右
概要
在優(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所示嗤谚。
圖 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)存開銷濒憋,真機上不存在,大家在Profiling時不要被誤導(dǎo)陶夜。( A萃浴)
通常Debug.Log一類的日志函數(shù)應(yīng)該只存在Debug階段,但是很多時候這些函數(shù)沒有屏蔽条辟。如果它們出現(xiàn)在調(diào)用次數(shù)較多的邏輯中黔夭,就帶來額外的CPU開銷。同樣Warning和Log存在相同的情況羽嫡。雖然日常在console或真機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的合批效率黍翎。除了幾個通用圖集外面徽,其它圖集按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ì)的模型才可以合并衷佃。
圖 02
動態(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)行耍目。
圖 03
紋理的尺寸會影響上傳紋理時帶寬的使用膏斤,也就是上傳耗時比較高。通常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)行控制蔑舞。運算符要合理使用,變量的浮點精度要同時考慮計算需求和真機的實際支持的精度范圍嘹屯。對Tex2D攻询、紋理采樣的使用方式要合理,畢竟這類指令過多時會增加開銷州弟。
Unity引擎自帶的Terrian系統(tǒng)钧栖,可以通過分區(qū)塊或者轉(zhuǎn)為Mesh解決此部分性能瓶頸。我們可以通過插件Terrain Slicing & Dynamic Loading Kit來分割地形婆翔,并調(diào)整地形的尺寸和精度等配置參數(shù)拯杠。
圖 04
一個特效包含粒子發(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ù)項目的機型適配靈活選擇,保證質(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)存一般會擴大很大一部分,如見下圖05所示誊薄。
圖 05
在使用數(shù)組類型的對象時履恩,如果初始化時是非定長數(shù)組,數(shù)組實際容量會根據(jù)Add操作以0呢蔫、4切心、8、16片吊、32倍逐步擴大绽昏,其中大量空間為Null,浪費了內(nèi)存俏脊。這種情況常出現(xiàn)在客戶端初始化數(shù)據(jù)表保存到List全谤、Dictionary時。
當(dāng)我們需要手動釋放一些對象的內(nèi)存時爷贫,會有很多種方式认然,Unity提供了很多卸載各種資源的函數(shù)补憾。主動調(diào)GC.collect是不必要的,如果一個對象的引用不是Null時卷员,是不可能釋放它的余蟹。GC只需要做好對象引用的清理就可以,剩下的還是由GC機制自動管理更好子刮。我們可以通過自定義內(nèi)存池和資源管理器威酒,來很精細(xì)的控制每一種資源的生命周期。
AssetBundle壓縮格式一般使用LZ4挺峡,但要注意AssetBundle的合理Unload時機葵孤。而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資源商店提供的各種插件功能強大穆刻,但是插件內(nèi)部的一些邏輯沒有考慮到移動平臺的應(yīng)用環(huán)境置尔,存在很多不良代碼,需要開發(fā)者仔細(xì)檢查插件源代碼氢伟,根據(jù)情況進(jìn)行改進(jìn)榜轿。并在性能測試時觀察是否存在插件帶來的性能瓶頸幽歼。
通常在對項目進(jìn)行性能分析時,會有很多工具輔助我們進(jìn)行分析工作谬盐。下面是我們推薦的工具:
Xcode & Instrunments
RenderDoc
Snapdragon Profiler