浅谈Unity中的优化
文章目錄
- 0-前言
- 1-GC介紹
- 1.1-腦圖
- 1.1-介紹
- 1.2-Unity內(nèi)存管理機(jī)制簡(jiǎn)介
- 1.3-棧內(nèi)存分配和回收機(jī)制
- 1.4-堆內(nèi)存分配和回收機(jī)制
- 1.5-垃圾回收時(shí)的操作
- 1.6-何時(shí)會(huì)觸發(fā)垃圾回收
- 1.7-GC操作帶來的問題
- 2-GC分析及優(yōu)化
- 2.1-分析GC帶來的問題
- 2.2-分析堆內(nèi)存的分配
- 2.2.1-堆內(nèi)存和堆棧內(nèi)存分配的變量類型
- 2.2.2-利用profiler window 來檢測(cè)堆內(nèi)存分配
- 2.3-降低GC的影響的方法
- 2.4-減少內(nèi)存垃圾的數(shù)量
- 2.4.1-緩存
- 2.4.2-不要在頻繁調(diào)用的函數(shù)中反復(fù)進(jìn)行堆內(nèi)存分配
- 2.4.3-清除鏈表
- 2.4.4-對(duì)象池
- 2.5-造成不必要的堆內(nèi)存分配的因素
- 2.5.1-字符串
- 2.5.2-Unity函數(shù)調(diào)用
- 2.5.3-裝箱操作
- 2.5.4-協(xié)程
- 2.5.5-函數(shù)引用
- 2.5.6-LINQ和常量表達(dá)式
- 2.6-重構(gòu)代碼來減小GC的影響
- 2.7-定時(shí)執(zhí)行GC操作
- 2.7.1-主動(dòng)調(diào)用GC操作
- 3-解讀Unity性能優(yōu)化
- 3.1-常見的Unity性能問題
- 3.2-Unity運(yùn)行時(shí)的內(nèi)存占用情況
- 3.3-內(nèi)存標(biāo)準(zhǔn)
- 3.4-Mono內(nèi)存管理策略
- 3.5-Mono內(nèi)存泄漏分析
- 3.6-優(yōu)化
- 3.6.1-DrawCall優(yōu)化
- 3.6.2-CPU優(yōu)化最直接的方法
- 3.6.3-GPU優(yōu)化
- 3.7-研發(fā)團(tuán)隊(duì)需要關(guān)注的引擎模塊
- 3.8-開始優(yōu)化工作
- 4-Unity3D性能優(yōu)化——初識(shí)Unity-Statistics
- 5-Unity3D性能優(yōu)化——Unity篇
- 5.1-Unity 腳本開發(fā)方面
- 5.2-Unity資源相關(guān)優(yōu)化
- 5.2.1-音頻
- 5.2.2-Texture
- 5.2.3-模型
- 5.2.4-文本文件優(yōu)化
- 5.2.5-AssetBundle管理
- 5.2.6-資源工作流管理
- 5.3-Unity相機(jī)優(yōu)化
- 5.3.1-Occlusion Culling遮擋剔除
- 5.3.2-Frustum Culling視錐體剔除
- 5.4-UnityUGUI優(yōu)化
- 5.5-C# 語言層面的優(yōu)化
- 5.6-圖形渲染層面
- 6-Unity3D性能優(yōu)化——工具篇
- 6.1-游戲性能簡(jiǎn)述
- 6.2-Unity3d性能分析工具
- 6.2.1-Unity Profile
- 6.2.2-其他性能分析工具
0-前言
此文章為網(wǎng)上轉(zhuǎn)載收集而成,非原創(chuàng)文章,請(qǐng)尊重別人的勞動(dòng)成果,讓分享成為一種美德,歡迎轉(zhuǎn)載。另外,文章在表述和代碼方面如有不妥之處,歡迎批評(píng)指正。同時(shí)大家有更好的優(yōu)化方案,或者自己獨(dú)立見解的優(yōu)化想法,也請(qǐng)發(fā)相關(guān)郵件于我,我將持續(xù)更新這篇文章,努力將“淺談”轉(zhuǎn)變?yōu)椤吧钊搿?#xff01;
觀前提示:本文篇幅較長(zhǎng),請(qǐng)耐心觀看或收藏
本文鏈接https://blog.csdn.net/qq_23420435/article/details/110109812
當(dāng)前版本:V0.0.0 更新時(shí)間:2020-11-25 更新內(nèi)容:首次整合更新 更新管理:小小Editor收集整合
18328685848@163.com----轉(zhuǎn)載收集整合 小小Editor章節(jié)一《CG介紹》、章節(jié)二《GC分析及優(yōu)化》
http://www.cnblogs.com/msxh/p/6531725.html https://blog.csdn.net/worisaa/article/details/64121436----原創(chuàng) 路人張德帥----轉(zhuǎn)載 worisaa章節(jié)三《解讀Unity性能優(yōu)化》
https://www.jianshu.com/p/e427a38e91c3----Jumbo章節(jié)四《Unity3D性能優(yōu)化——初識(shí)Unity-Statistics》
https://blog.csdn.net/wdmzjzlym/article/details/51335915----萌萌的一天章節(jié)五《Unity3D性能優(yōu)化——Unity篇》
https://blog.csdn.net/weixin_43967425/article/details/84928476----原創(chuàng) Kyle_GISer----原創(chuàng) 網(wǎng)絡(luò)章節(jié)五點(diǎn)二《5.2-Unity資源相關(guān)優(yōu)化》
https://blog.csdn.net/u012565990/article/details/51794486 https://blog.csdn.net/u013709166/article/details/54959464?----原創(chuàng) CZandQZ----原創(chuàng) ResetOTER章節(jié)五點(diǎn)三點(diǎn)一《Occlusion Culling遮擋剔除》
https://blog.csdn.net/qq_37672438/article/details/100933982----原創(chuàng) 路人張德帥章節(jié)五點(diǎn)三點(diǎn)二《Frustum Culling視錐體剔除》
https://blog.csdn.net/xinjay1992/article/details/107550433?----原創(chuàng) 程序員茶館章節(jié)五點(diǎn)四《5.4-UnityUGUI優(yōu)化》
https://mp.weixin.qq.com/s?__biz=MzU5MjQ1NTEwOA==&mid=2247495094&idx=1&sn= 4a948884855c5a6f26f73ff7845c8caf&chksm=fe1dd91dc96a500baf2970fed3b13009e89 7997472f6596325102dff0cad616defd7a2470d52&token=2019102974&lang=zh_CN#rd----原創(chuàng) Unity官方平臺(tái)----原創(chuàng) 網(wǎng)絡(luò)章節(jié)六《Unity3D性能優(yōu)化——工具篇》
https://zhuanlan.zhihu.com/p/39529241----朔宇1-GC介紹
1.1-腦圖
1.1-介紹
在游戲運(yùn)行的時(shí)候,數(shù)據(jù)主要存儲(chǔ)在內(nèi)存中,當(dāng)游戲的數(shù)據(jù)不在需要的時(shí)候,存儲(chǔ)當(dāng)前數(shù)據(jù)的內(nèi)存就可以被回收再次使用。內(nèi)存垃圾是指當(dāng)前廢棄數(shù)據(jù)所占用的內(nèi)存,垃圾回收(GC)是指將廢棄的內(nèi)存重新回收再次使用的過程。
Unity中將垃圾回收當(dāng)作內(nèi)存管理的一部分,如果游戲中垃圾回收十分復(fù)雜,則游戲的性能會(huì)受到極大影響,此時(shí)垃圾回收會(huì)成為游戲性能的一大障礙點(diǎn)。
下面我們將會(huì)學(xué)習(xí)垃圾回收的機(jī)制,掌握垃圾回收如何被觸發(fā)以及如何提高垃圾回收效率來減小其對(duì)游戲行性能的影響。
1.2-Unity內(nèi)存管理機(jī)制簡(jiǎn)介
要想了解垃圾回收如何工作以及何時(shí)被觸發(fā),我們首先需要了解unity的內(nèi)存管理機(jī)制。Unity主要采用自動(dòng)內(nèi)存管理的機(jī)制,開發(fā)時(shí)在代碼中不需要詳細(xì)地告訴unity如何進(jìn)行內(nèi)存管理,unity內(nèi)部自身會(huì)進(jìn)行內(nèi)存管理。
unity的自動(dòng)內(nèi)存管理可以理解為以下幾個(gè)部分:
- unity內(nèi)部有兩個(gè)內(nèi)存管理池:堆內(nèi)存和堆棧內(nèi)存。堆棧內(nèi)存(stack)主要用來存儲(chǔ)較小的和短暫的數(shù)據(jù)片段,堆內(nèi)存(heap)主要用來存儲(chǔ)較大的和存儲(chǔ)時(shí)間較長(zhǎng)的數(shù)據(jù)片段。
- unity中的變量只會(huì)在堆?;蛘叨褍?nèi)存上進(jìn)行內(nèi)存分配。
- 只要變量處于激活狀態(tài),則其占用的內(nèi)存會(huì)被標(biāo)記為使用狀態(tài),則該部分的內(nèi)存處于被分配的狀態(tài),變量要么存儲(chǔ)在堆棧內(nèi)存上,要么處于堆內(nèi)存上。
- 一旦變量不再激活,則其所占用的內(nèi)存不再需要,該部分內(nèi)存可以被回收到內(nèi)存池中被再次使用,這樣的操作就是內(nèi)存回收。處于堆棧上的內(nèi)存回收及其快速,處于堆上的內(nèi)存并不是及時(shí)回收的,其對(duì)應(yīng)的內(nèi)存依然會(huì)被標(biāo)記為使用狀態(tài)。
- 垃圾回收主要是指堆上的內(nèi)存分配和回收,unity中會(huì)定時(shí)對(duì)堆內(nèi)存進(jìn)行GC操作。
在了解了GC的過程后,下面詳細(xì)了解堆內(nèi)存和堆棧內(nèi)存的分配和回收機(jī)制的差別。
1.3-棧內(nèi)存分配和回收機(jī)制
棧上的內(nèi)存分配和回收十分快捷簡(jiǎn)單,主要是棧上只會(huì)存儲(chǔ)短暫的較小的變量。內(nèi)存分配和回收都會(huì)以一種可控制順序和大小的方式進(jìn)行。
棧的運(yùn)行方式就像 stack :只是一個(gè)數(shù)據(jù)的集合,數(shù)據(jù)的進(jìn)出都以一種固定的方式運(yùn)行。正是這種簡(jiǎn)潔性和固定性使得堆棧的操作十分快捷。當(dāng)數(shù)據(jù)被存儲(chǔ)在棧上的時(shí)候,只需要簡(jiǎn)單地在其后進(jìn)行擴(kuò)展。當(dāng)數(shù)據(jù)失效的時(shí)候,只需要將其從棧上移除復(fù)用。
1.4-堆內(nèi)存分配和回收機(jī)制
堆內(nèi)存上的內(nèi)存分配和存儲(chǔ)相對(duì)而言更加復(fù)雜,主要是堆內(nèi)存上可以存儲(chǔ)短期較小的數(shù)據(jù),也可以存儲(chǔ)各種類型和大小的數(shù)據(jù)。其上的內(nèi)存分配和回收順序并不可控,可能會(huì)要求分配不同大小的內(nèi)存單元來存儲(chǔ)數(shù)據(jù)。
堆上的變量在存儲(chǔ)的時(shí)候,主要分為以下幾步:
- 首先,unity檢測(cè)是否有足夠的閑置內(nèi)存單元用來存儲(chǔ)數(shù)據(jù),如果有,則分配對(duì)應(yīng)的內(nèi)存單元;
- 如果沒有足夠的存儲(chǔ)單元,unity會(huì)觸發(fā)垃圾回收來釋放不再被使用的堆內(nèi)存。這步操作是一步緩慢的操作,如果垃圾回收后有足夠的內(nèi)存單元,則進(jìn)行內(nèi)存分配。
- 如果垃圾回收后并沒有足夠的內(nèi)存單元,則unity會(huì)擴(kuò)展堆內(nèi)存的大小,這步操作會(huì)很緩慢,然后分配對(duì)應(yīng)的內(nèi)存單元給變量。
堆內(nèi)存的分配有可能會(huì)變得十分緩慢,特別是需要垃圾回收和堆內(nèi)存需要擴(kuò)展的情況下。
1.5-垃圾回收時(shí)的操作
當(dāng)一個(gè)變量不再處于激活狀態(tài)的時(shí)候,其所占用的內(nèi)存并不會(huì)立刻被回收,不再使用的內(nèi)存只會(huì)在GC的時(shí)候才會(huì)被回收。
每次運(yùn)行GC的時(shí)候,主要進(jìn)行下面的操作:
- GC會(huì)檢查堆內(nèi)存上的每個(gè)存儲(chǔ)變量;
- 對(duì)每個(gè)變量會(huì)檢測(cè)其引用是否處于激活狀態(tài);
- 如果變量的引用不再處于激活狀態(tài),則會(huì)被標(biāo)記為可回收;
- 被標(biāo)記的變量會(huì)被移除,其所占有的內(nèi)存會(huì)被回收到堆內(nèi)存上。
GC操作是一個(gè)極其耗費(fèi)的操作,堆內(nèi)存上的變量或者引用越多則其運(yùn)行的操作會(huì)更多,耗費(fèi)的時(shí)間越長(zhǎng)。
1.6-何時(shí)會(huì)觸發(fā)垃圾回收
主要有三個(gè)操作會(huì)觸發(fā)垃圾回收:
- 在堆內(nèi)存上進(jìn)行內(nèi)存分配操作而內(nèi)存不夠的時(shí)候都會(huì)觸發(fā)垃圾回收來利用閑置的內(nèi)存;
- GC會(huì)自動(dòng)的觸發(fā),不同平臺(tái)運(yùn)行頻率不一樣;
- GC可以被強(qiáng)制執(zhí)行。
GC操作可以被頻繁觸發(fā),特別是在堆內(nèi)存上進(jìn)行內(nèi)存分配時(shí)內(nèi)存單元不足夠的時(shí)候,這就意味著頻繁在堆內(nèi)存上進(jìn)行內(nèi)存分配和回收會(huì)觸發(fā)頻繁的GC操作。
1.7-GC操作帶來的問題
在了解GC在unity內(nèi)存管理中的作用后,我們需要考慮其帶來的問題。最明顯的問題是GC操作會(huì)需要大量的時(shí)間來運(yùn)行,如果堆內(nèi)存上有大量的變量或者引用需要檢查,則檢查的操作會(huì)十分緩慢,這就會(huì)使得游戲運(yùn)行緩慢。其次GC可能會(huì)在關(guān)鍵時(shí)候運(yùn)行,例如CPU處于游戲的性能運(yùn)行關(guān)鍵時(shí)刻,其他的任何一個(gè)額外的操作都可能會(huì)帶來極大的影響,使得游戲幀率下降。
另外一個(gè)GC帶來的問題是堆內(nèi)存碎片。當(dāng)一個(gè)內(nèi)存單元從堆內(nèi)存上分配出來,其大小取決于其存儲(chǔ)的變量的大小。當(dāng)該內(nèi)存被回收到堆內(nèi)存上的時(shí)候,有可能使得堆內(nèi)存被分割成碎片化的單元。也就是說堆內(nèi)存總體可以使用的內(nèi)存單元較大,但是單獨(dú)的內(nèi)存單元較小,在下次內(nèi)存分配的時(shí)候不能找到合適大小的存儲(chǔ)單元,這就會(huì)觸發(fā)GC操作或者堆內(nèi)存擴(kuò)展操作。
堆內(nèi)存碎片會(huì)造成兩個(gè)結(jié)果,一個(gè)是游戲占用的內(nèi)存會(huì)越來越大,一個(gè)是GC會(huì)更加頻繁地被觸發(fā)。
2-GC分析及優(yōu)化
2.1-分析GC帶來的問題
GC操作帶來的問題主要表現(xiàn)為幀率運(yùn)行低,性能間歇中斷或者降低。如果游戲有這樣的表現(xiàn),則首先需要打開unity中的profiler window來確定是否是GC造成。
了解如何運(yùn)用profiler window,可以參考此處,如果游戲確實(shí)是由GC造成的,可以繼續(xù)閱讀下面的內(nèi)容。
2.2-分析堆內(nèi)存的分配
如果GC造成游戲的性能問題,我們需要知道游戲中的哪部分代碼會(huì)造成GC,內(nèi)存垃圾在變量不再激活的時(shí)候產(chǎn)生,所以首先我們需要知道堆內(nèi)存上分配的是什么變量。
2.2.1-堆內(nèi)存和堆棧內(nèi)存分配的變量類型
在Unity中,值類型變量都在堆棧上進(jìn)行內(nèi)存分配,其他類型的變量都在堆內(nèi)存上分配。如果你不知道值類型和引用類型的差別,可以查看此處。
下面的代碼可以用來理解值類型的分配和釋放,其對(duì)應(yīng)的變量在函數(shù)調(diào)用完后會(huì)立即回收:
下面的代碼可以用來理解值類型的分配和釋放,其對(duì)應(yīng)的變量在函數(shù)調(diào)用完后會(huì)立即回收:
void ExampleFunciton() {int localInt = 5; }對(duì)應(yīng)的引用類型的參考代碼如下,其對(duì)應(yīng)的變量在GC的時(shí)候才回收:
void ExampleFunction() {List localList = new List(); }2.2.2-利用profiler window 來檢測(cè)堆內(nèi)存分配
我們可以在profier window中檢查堆內(nèi)存的分配操作:在CPU usage分析窗口中,我們可以檢測(cè)任何一幀cpu的內(nèi)存分配情況。其中一個(gè)選項(xiàng)是GC alloc,通過分析其來定位是什么函數(shù)造成大量的堆內(nèi)存分配操作。一旦定位該函數(shù),我們就可以分析解決其造成問題的原因從而減少內(nèi)存垃圾的產(chǎn)生。
2.3-降低GC的影響的方法
大體上來說,我們可以通過三種方法來降低GC的影響:
- 減少GC的運(yùn)行次數(shù);
- 減少單次GC的運(yùn)行時(shí)間;
- 將GC的運(yùn)行時(shí)間延遲,避免在關(guān)鍵時(shí)候觸發(fā),比如可以在場(chǎng)景加載的時(shí)候調(diào)用GC
基于此,我們可以采用三種策略:
- 對(duì)游戲進(jìn)行重構(gòu),減少堆內(nèi)存的分配和引用的分配。更少的變量和引用會(huì)減少GC操作中的檢測(cè)個(gè)數(shù)從而提高GC的運(yùn)行效率。
- 降低堆內(nèi)存分配和回收的頻率,尤其是在關(guān)鍵時(shí)刻。也就是說更少的事件觸發(fā)GC操作,同時(shí)也降低堆內(nèi)存碎片。
- 我們可以試著測(cè)量GC和堆內(nèi)存擴(kuò)展的時(shí)間,使其按照可預(yù)測(cè)的順序執(zhí)行。當(dāng)然這樣操作的難度極大,但是這會(huì)大大降低GC的影響。
2.4-減少內(nèi)存垃圾的數(shù)量
減少內(nèi)存垃圾主要可以通過一些方法來減少:
2.4.1-緩存
如果在代碼中反復(fù)調(diào)用某些造成堆內(nèi)存分配的函數(shù)但是其返回結(jié)果并沒有使用,這就會(huì)造成不必要的內(nèi)存垃圾,我們可以緩存這些變量來重復(fù)利用,這就是緩存。
例如下面的代碼每次調(diào)用的時(shí)候就會(huì)造成堆內(nèi)存分配,主要是每次都會(huì)分配一個(gè)新的數(shù)組:
void OnTriggerEnter(Collider other) {Renderer[] allRenderers = FindObjectsOfType<Renderer>();ExampleFunction(allRenderers); }對(duì)比下面的代碼,只會(huì)生產(chǎn)一個(gè)數(shù)組用來緩存數(shù)據(jù),實(shí)現(xiàn)反復(fù)利用而不需要造成更多的內(nèi)存垃圾:
private Renderer[] allRenderers;void Start() {allRenderers = FindObjectsOfType<Renderer>(); }void OnTriggerEnter(Collider other) {ExampleFunction(allRenderers); }2.4.2-不要在頻繁調(diào)用的函數(shù)中反復(fù)進(jìn)行堆內(nèi)存分配
在MonoBehaviour中,如果我們需要進(jìn)行堆內(nèi)存分配,最壞的情況就是在其反復(fù)調(diào)用的函數(shù)中進(jìn)行堆內(nèi)存分配,例如Update()和LateUpdate()函數(shù)這種每幀都調(diào)用的函數(shù),這會(huì)造成大量的內(nèi)存垃圾。我們可以考慮在Start()或者Awake()函數(shù)中進(jìn)行內(nèi)存分配,這樣可以減少內(nèi)存垃圾。
下面的例子中,update函數(shù)會(huì)多次觸發(fā)內(nèi)存垃圾的產(chǎn)生:
void Update() {ExampleGarbageGenerationFunction(transform.position.x); }通過一個(gè)簡(jiǎn)單的改變,我們可以確保每次在x改變的時(shí)候才觸發(fā)函數(shù)調(diào)用,這樣避免每幀都進(jìn)行堆內(nèi)存分配:
private float previousTransformPositionX;void Update() {float transformPositionX = transform.position.x;if(transfromPositionX != previousTransformPositionX){ExampleGarbageGenerationFunction(transformPositionX); previousTransformPositionX = trasnformPositionX;} }另外的一種方法是在update中采用計(jì)時(shí)器,特別是在運(yùn)行有規(guī)律但是不需要每幀都運(yùn)行的代碼中,例如:
void Update() {ExampleGarbageGeneratiingFunction() }通過添加一個(gè)計(jì)時(shí)器,我們可以確保每隔1s才觸發(fā)該函數(shù)一次:
private float timeSinceLastCalled; private float delay = 1f; void Update() {timSinceLastCalled += Time.deltaTime;if(timeSinceLastCalled > delay){ExampleGarbageGenerationFunction();timeSinceLastCalled = 0f;} }通過這樣細(xì)小的改變,我們可以使得代碼運(yùn)行的更快同時(shí)減少內(nèi)存垃圾的產(chǎn)生。
2.4.3-清除鏈表
在堆內(nèi)存上進(jìn)行鏈表的分配的時(shí)候,如果該鏈表需要多次反復(fù)的分配,我們可以采用鏈表的clear函數(shù)來清空鏈表從而替代反復(fù)多次的創(chuàng)建分配鏈表。
void Update() {List myList = new List();PopulateList(myList); }通過改進(jìn),我們可以將該鏈表只在第一次創(chuàng)建或者該鏈表必須重新設(shè)置的時(shí)候才進(jìn)行堆內(nèi)存分配,從而大大減少內(nèi)存垃圾的產(chǎn)生:
private List myList = new List(); void Update() {myList.Clear();PopulateList(myList); }2.4.4-對(duì)象池
即便我們?cè)诖a中盡可能地減少堆內(nèi)存的分配行為,但是如果游戲有大量的對(duì)象需要產(chǎn)生和銷毀依然會(huì)造成GC。對(duì)象池技術(shù)可以通過重復(fù)使用objects來降低堆內(nèi)存的分配和回收頻率。對(duì)象池在游戲中廣泛的使用,特別是在游戲中需要頻繁的創(chuàng)建和銷毀相同的游戲?qū)ο蟮臅r(shí)候,例如槍的子彈。
要詳細(xì)的講解對(duì)象池已經(jīng)超出本文的范圍,但是該技術(shù)值得我們深入的研究This tutorial on object pooling on the Unity Learn site對(duì)于對(duì)象池有詳細(xì)深入的講解。
2.5-造成不必要的堆內(nèi)存分配的因素
我們已經(jīng)知道值類型變量在堆棧上分配,其他的變量在堆內(nèi)存上分配,但是任然有一些情況下的堆內(nèi)存分配會(huì)讓我們感到吃驚。下面讓我們分析一些常見的不必要的堆內(nèi)存分配行為并對(duì)其進(jìn)行優(yōu)化。
2.5.1-字符串
在c#中,字符串是引用類型變量而不是值類型變量,即使看起來它是存儲(chǔ)字符串的值的。這就意味著字符串會(huì)造成一定的內(nèi)存垃圾,由于代碼中經(jīng)常使用字符串,所以我們需要對(duì)其格外小心。
c#中的字符串是不可變更的,也就是說其內(nèi)部的值在創(chuàng)建后是不可被變更的。每次在對(duì)字符串進(jìn)行操作的時(shí)候(例如運(yùn)用字符串的“加”操作),unity會(huì)新建一個(gè)字符串用來存儲(chǔ)新的字符串,使得舊的字符串被廢棄,這樣就會(huì)造成內(nèi)存垃圾。
我們可以采用以下的一些方法來最小化字符串的影響:
1)減少不必要的字符串的創(chuàng)建,如果一個(gè)字符串被多次利用,我們可以創(chuàng)建并緩存該字符串。
2)減少不必要的字符串操作,例如如果在Text組件中,有一部分字符串需要經(jīng)常改變,但是其他部分不會(huì),則我們可以將其分為兩個(gè)部分的組件。
3)如果我們需要實(shí)時(shí)的創(chuàng)建字符串,我們可以采用StringBuilderClass來代替,StringBuilder專為不需要進(jìn)行內(nèi)存分配而設(shè)計(jì),從而減少字符串產(chǎn)生的內(nèi)存垃圾。
4)移除游戲中的Debug.Log()函數(shù)的代碼,盡管該函數(shù)可能輸出為空,對(duì)該函數(shù)的調(diào)用依然會(huì)執(zhí)行,該函數(shù)會(huì)創(chuàng)建至少一個(gè)字符(空字符)的字符串。如果游戲中有大量的該函數(shù)的調(diào)用,這會(huì)造成內(nèi)存垃圾的增加。
在下面的代碼中,在Update函數(shù)中會(huì)進(jìn)行一個(gè)string的操作,這樣的操作就會(huì)造成不必要的內(nèi)存垃圾:
public Text timerText; private float timer; void Update() {timer += Time.deltaTime;timerText.text = "Time:"+ timer.ToString(); }通過將字符串進(jìn)行分隔,我們可以剔除字符串的加操作,從而減少不必要的內(nèi)存垃圾:
public Text timerHeaderText; public Text timerValueText; private float timer; void Start() {timerHeaderText.text = "TIME:"; }void Update() {timerValueText.text = timer.ToString(); }2.5.2-Unity函數(shù)調(diào)用
在代碼編程中,我們需要知道當(dāng)我們調(diào)用不是我們自己編寫的代碼,無論是Unity自帶的還是插件中的,我們都可能會(huì)產(chǎn)生內(nèi)存垃圾。Unity的某些函數(shù)調(diào)用會(huì)產(chǎn)生內(nèi)存垃圾,我們?cè)谑褂玫臅r(shí)候需要注意它的使用。
這兒沒有明確的列表指出哪些函數(shù)需要注意,每個(gè)函數(shù)在不同的情況下有不同的使用,所以最好仔細(xì)地分析游戲,定位內(nèi)存垃圾的產(chǎn)生原因以及如何解決問題。有時(shí)候緩存是一種有效的辦法,有時(shí)候盡量降低函數(shù)的調(diào)用頻率是一種辦法,有時(shí)候用其他函數(shù)來重構(gòu)代碼是一種辦法。現(xiàn)在來分析unity中中常見的造成堆內(nèi)存分配的函數(shù)調(diào)用。
在Unity中如果函數(shù)需要返回一個(gè)數(shù)組,則一個(gè)新的數(shù)組會(huì)被分配出來用作結(jié)果返回,這不容易被注意到,特別是如果該函數(shù)含有迭代器,下面的代碼中對(duì)于每個(gè)迭代器都會(huì)產(chǎn)生一個(gè)新的數(shù)組:
void ExampleFunction() {for(int i=0; i < myMesh.normals.Length;i++){Vector3 normal = myMesh.normals[i];} }對(duì)于這樣的問題,我們可以緩存一個(gè)數(shù)組的引用,這樣只需要分配一個(gè)數(shù)組就可以實(shí)現(xiàn)相同的功能,從而減少內(nèi)存垃圾的產(chǎn)生:
void ExampleFunction() {Vector3[] meshNormals = myMesh.normals;for(int i=0; i < meshNormals.Length;i++){Vector3 normal = meshNormals[i];} }此外另外的一個(gè)函數(shù)調(diào)用GameObject.name 或者 GameObject.tag也會(huì)造成預(yù)想不到的堆內(nèi)存分配,這兩個(gè)函數(shù)都會(huì)將結(jié)果存為新的字符串返回,這就會(huì)造成不必要的內(nèi)存垃圾,對(duì)結(jié)果進(jìn)行緩存是一種有效的辦法,但是在Unity中都對(duì)應(yīng)的有相關(guān)的函數(shù)來替代。對(duì)于比較gameObject的tag,可以采用GameObject.CompareTag()來替代。
在下面的代碼中,調(diào)用gameobject.tag就會(huì)產(chǎn)生內(nèi)存垃圾:
private string playerTag="Player"; void OnTriggerEnter(Collider other) {bool isPlayer = other.gameObject.tag ==playerTag; }采用GameObject.CompareTag()可以避免內(nèi)存垃圾的產(chǎn)生:
private string playerTag = "Player"; void OnTriggerEnter(Collider other) {bool isPlayer = other.gameObject.CompareTag(playerTag); }不只是GameObject.CompareTag,unity中許多其他的函數(shù)也可以避免內(nèi)存垃圾的生成。比如我們可以用Input.GetTouch()和Input.touchCount()來代替Input.touches,或者用Physics.SphereCastNonAlloc()來代替Physics.SphereCastAll()。
2.5.3-裝箱操作
裝箱操作是指一個(gè)值類型變量被用作引用類型變量時(shí)候的內(nèi)部變換過程,如果我們向帶有對(duì)象類型參數(shù)的函數(shù)傳入值類型,這就會(huì)觸發(fā)裝箱操作。比如String.Format()函數(shù)需要傳入字符串和對(duì)象類型參數(shù),如果傳入字符串和int類型數(shù)據(jù),就會(huì)觸發(fā)裝箱操作。如下面代碼所示:
void ExampleFunction() {int cost = 5;string displayString = String.Format("Price:{0} gold",cost); }在Unity的裝箱操作中,對(duì)于值類型會(huì)在堆內(nèi)存上分配一個(gè)System.Object類型的引用來封裝該值類型變量,其對(duì)應(yīng)的緩存就會(huì)產(chǎn)生內(nèi)存垃圾。裝箱操作是非常普遍的一種產(chǎn)生內(nèi)存垃圾的行為,即使代碼中沒有直接的對(duì)變量進(jìn)行裝箱操作,在插件或者其他的函數(shù)中也有可能會(huì)產(chǎn)生。最好的解決辦法是盡可能的避免或者移除造成裝箱操作的代碼。
2.5.4-協(xié)程
調(diào)用 StartCoroutine()會(huì)產(chǎn)生少量的內(nèi)存垃圾,因?yàn)閡nity會(huì)生成實(shí)體來管理協(xié)程。所以在游戲的關(guān)鍵時(shí)刻應(yīng)該限制該函數(shù)的調(diào)用?;诖?#xff0c;任何在游戲關(guān)鍵時(shí)刻調(diào)用的協(xié)程都需要特別的注意,特別是包含延遲回調(diào)的協(xié)程。
yield return 0;由于需要返回0,引發(fā)了裝箱操作,所以會(huì)產(chǎn)生內(nèi)存垃圾。這種情況下,為了避免內(nèi)存垃圾,我們可以這樣返回:
yield return null;另外一種對(duì)協(xié)程的錯(cuò)誤使用是每次返回的時(shí)候都new同一個(gè)變量,例如:
while(!isComplete) {yield return new WaitForSeconds(1f); }我們可以采用緩存來避免這樣的內(nèi)存垃圾產(chǎn)生:
WaitForSeconds delay = new WaiForSeconds(1f); while(!isComplete) {yield return delay; }如果游戲中的協(xié)程產(chǎn)生了內(nèi)存垃圾,我們可以考慮用其他的方式來替代協(xié)程。重構(gòu)代碼對(duì)于游戲而言十分復(fù)雜,但是對(duì)于協(xié)程而言我們也可以注意一些常見的操作,比如如果用協(xié)程來管理時(shí)間,最好在update函數(shù)中保持對(duì)時(shí)間的記錄。如果用協(xié)程來控制游戲中事件的發(fā)生順序,最好對(duì)于不同事件之間有一定的信息通信的方式。對(duì)于協(xié)程而言沒有適合各種情況的方法,只有根據(jù)具體的代碼來選擇最好的解決辦法。
2.5.5-函數(shù)引用
函數(shù)的引用,無論是指向匿名函數(shù)還是顯式函數(shù),在unity中都是引用類型變量,這都會(huì)在堆內(nèi)存上進(jìn)行分配。匿名函數(shù)的調(diào)用完成后都會(huì)增加內(nèi)存的使用和堆內(nèi)存的分配。具體函數(shù)的引用和終止都取決于操作平臺(tái)和編譯器設(shè)置,但是如果想減少GC最好減少函數(shù)的引用。
2.5.6-LINQ和常量表達(dá)式
由于LINQ和常量表達(dá)式以裝箱的方式實(shí)現(xiàn),所以在使用的時(shí)候最好進(jìn)行性能測(cè)試。
2.6-重構(gòu)代碼來減小GC的影響
即使我們減小了代碼在堆內(nèi)存上的分配操作,代碼也會(huì)增加GC的工作量。最常見的增加GC工作量的方式是讓其檢查它不必檢查的對(duì)象。struct是值類型的變量,但是如果struct中包含有引用類型的變量,那么GC就必須檢測(cè)整個(gè)struct。如果這樣的操作很多,那么GC的工作量就大大增加。在下面的例子中struct包含一個(gè)string,那么整個(gè)struct都必須在GC中被檢查:
public struct ItemData {public string name;public int cost;public Vector3 position; } private ItemData[] itemData;我們可以將該struct拆分為多個(gè)數(shù)組的形式,從而減小GC的工作量:
private string[] itemNames; private int[] itemCosts; private Vector3[] itemPositions;另外一種在代碼中增加GC工作量的方式是保存不必要的Object引用,在進(jìn)行GC操作的時(shí)候會(huì)對(duì)堆內(nèi)存上的object引用進(jìn)行檢查,越少的引用就意味著越少的檢查工作量。在下面的例子中,當(dāng)前的對(duì)話框中包含一個(gè)對(duì)下一個(gè)對(duì)話框引用,這就使得GC的時(shí)候回去檢查下一個(gè)對(duì)象框:
public class DialogData {private DialogData nextDialog;public DialogData GetNextDialog(){return nextDialog;} }通過重構(gòu)代碼,我們可以返回下一個(gè)對(duì)話框?qū)嶓w的標(biāo)記,而不是對(duì)話框?qū)嶓w本身,這樣就沒有多余的object引用,從而減少GC的工作量:
public class DialogData {private int nextDialogID;public int GetNextDialogID(){return nextDialogID;} }當(dāng)然這個(gè)例子本身并不重要,但是如果我們的游戲中包含大量的含有對(duì)其他Object引用的object,我們可以考慮通過重構(gòu)代碼來減少GC的工作量。
2.7-定時(shí)執(zhí)行GC操作
2.7.1-主動(dòng)調(diào)用GC操作
如果我們知道堆內(nèi)存在被分配后并沒有被使用,我們希望可以主動(dòng)地調(diào)用GC操作,或者在GC操作并不影響游戲體驗(yàn)的時(shí)候(例如場(chǎng)景切換的時(shí)候),我們可以主動(dòng)的調(diào)用GC操作:
System.GC.Collect()通過主動(dòng)的調(diào)用,我們可以主動(dòng)驅(qū)使GC操作來回收堆內(nèi)存。
3-解讀Unity性能優(yōu)化
3.1-常見的Unity性能問題
VSS:Virtual Set Size,虛擬耗用內(nèi)存。它是一個(gè)進(jìn)程能訪問的所有內(nèi)存空間地址的大小。這個(gè)大小包含了 一些沒有駐留在RAM中的內(nèi)存,就像mallocs已經(jīng)被分配,但還沒有寫入。VSS很少用來測(cè)量程序的實(shí)際使 用內(nèi)存。
RSS:Resident Set Size,實(shí)際使用物理內(nèi)存。RSS是一個(gè)進(jìn)程在RAM中實(shí)際持有的內(nèi)存大小。RSS可能會(huì) 產(chǎn)生誤導(dǎo),因?yàn)樗怂性撨M(jìn)程使用的共享庫所占用的內(nèi)存,一個(gè)被加載到內(nèi)存中的共享庫可能有很 多進(jìn)程會(huì)使用它。RSS不是單個(gè)進(jìn)程使用內(nèi)存量的精確表示。
PSS:Proportional Set Size,實(shí)際使用的物理內(nèi)存,它與RSS不同,它會(huì)按比例分配共享庫所占用的內(nèi)存。 例如,如果有三個(gè)進(jìn)程共享一個(gè)占30頁內(nèi)存控件的共享庫,每個(gè)進(jìn)程在計(jì)算PSS的時(shí)候,只會(huì)計(jì)算10頁。 PSS是一個(gè)非常有用的數(shù)值,如果系統(tǒng)中所有的進(jìn)程的PSS相加,所得和即為系統(tǒng)占用內(nèi)存的總和。當(dāng)一個(gè) 進(jìn)程被殺死后,它所占用的共享庫內(nèi)存將會(huì)被其他仍然使用該共享庫的進(jìn)程所分擔(dān)。在這種方式下,PSS 也會(huì)帶來誤導(dǎo),因?yàn)楫?dāng)一個(gè)進(jìn)程被殺后,PSS并不代表系統(tǒng)回收的內(nèi)存大小。
VSS:Virtual Set Size,虛擬耗用內(nèi)存。它是一個(gè)進(jìn)程能訪問的所有內(nèi)存空間地址的大小。這個(gè)大小包含了 一些沒有駐留在RAM中的內(nèi)存,就像mallocs已經(jīng)被分配,但還沒有寫入。VSS很少用來測(cè)量程序的實(shí)際使 用內(nèi)存。
RSS:Resident Set Size,實(shí)際使用物理內(nèi)存。RSS是一個(gè)進(jìn)程在RAM中實(shí)際持有的內(nèi)存大小。RSS可能會(huì) 產(chǎn)生誤導(dǎo),因?yàn)樗怂性撨M(jìn)程使用的共享庫所占用的內(nèi)存,一個(gè)被加載到內(nèi)存中的共享庫可能有很 多進(jìn)程會(huì)使用它。RSS不是單個(gè)進(jìn)程使用內(nèi)存量的精確表示。
PSS:Proportional Set Size,實(shí)際使用的物理內(nèi)存,它與RSS不同,它會(huì)按比例分配共享庫所占用的內(nèi)存。 例如,如果有三個(gè)進(jìn)程共享一個(gè)占30頁內(nèi)存控件的共享庫,每個(gè)進(jìn)程在計(jì)算PSS的時(shí)候,只會(huì)計(jì)算10頁。 PSS是一個(gè)非常有用的數(shù)值,如果系統(tǒng)中所有的進(jìn)程的PSS相加,所得和即為系統(tǒng)占用內(nèi)存的總和。當(dāng)一個(gè) 進(jìn)程被殺死后,它所占用的共享庫內(nèi)存將會(huì)被其他仍然使用該共享庫的進(jìn)程所分擔(dān)。在這種方式下,PSS 也會(huì)帶來誤導(dǎo),因?yàn)楫?dāng)一個(gè)進(jìn)程被殺后,PSS并不代表系統(tǒng)回收的內(nèi)存大小。
3.2-Unity運(yùn)行時(shí)的內(nèi)存占用情況
3.3-內(nèi)存標(biāo)準(zhǔn)
- 限定內(nèi)存占用不超過200M(iPhone4接近容易Crash,低端機(jī)型)
- 項(xiàng)目中Reserved Total(總體分配)內(nèi)存盡量控制在150M以內(nèi),如下 Texture 50M Mesh 20M AnimationClip 15M AudioClip 15M Mono堆內(nèi)存 40M 字體等 10M
- 項(xiàng)目中盡量嚴(yán)格控制,即使在中高端機(jī)型可較大內(nèi)存運(yùn)行。
3.4-Mono內(nèi)存管理策略
- 字符串連接處理,建議StringBuilder
- 盡量不使用foreach,Unity5.4以上解決了GC問題
- 不要頻繁實(shí)例化和銷毀對(duì)象,建議對(duì)象池管理
- 場(chǎng)景切換時(shí),主動(dòng)調(diào)用System.GC.Collect(),及時(shí)清理內(nèi)存
- Mono通過垃圾回收機(jī)制(Garbage Collect,簡(jiǎn)稱GC)對(duì)內(nèi)存進(jìn)行管理。Mono內(nèi)存分為兩部分,已用內(nèi)存(used)和堆內(nèi)存(heap),已用內(nèi)存指的是mono實(shí)際需要使用的內(nèi)存,堆內(nèi)存指的是mono向操作系統(tǒng)申請(qǐng)的內(nèi)存,兩者的差值就是mono的空閑內(nèi)存。當(dāng)mono需要分配內(nèi)存時(shí),會(huì)先查看空閑內(nèi)存是否足夠,如果足夠的話,直接在空閑內(nèi)存中分配,否則mono會(huì)進(jìn)行一次GC以釋放更多的空閑內(nèi)存,如果GC之后仍然沒有足夠的空閑內(nèi)存,則mono會(huì)向操作系統(tǒng)申請(qǐng)內(nèi)存。
3.5-Mono內(nèi)存泄漏分析
- Mono通過引用關(guān)系,判斷哪些內(nèi)存不再使用
- 【Mono內(nèi)存泄漏】對(duì)象已經(jīng)不再使用,卻未被GC回收
- Mono內(nèi)存泄漏使空閑內(nèi)存減少,GC頻繁,mono堆不斷擴(kuò)大,最終導(dǎo)致游戲內(nèi)存占用的增大
- 大部分mono內(nèi)存泄漏的情況都是由于靜態(tài)對(duì)象的引用引起
- 不再需要的對(duì)象將其引用設(shè)置為null,使其可以被GC及時(shí)回收
3.6-優(yōu)化
3.6.1-DrawCall優(yōu)化
-
先了解下DrawCall相關(guān)概念,便于優(yōu)化
DrawCall是CPU調(diào)用底層圖形接口的操作
DrawCall_Num = 25K * CPU_Frame * CPU_Percentage / FPS
DrawCall_Num : DrawCall數(shù)量(最大支持)
CPU_Frame : CPU 工作頻率(GHz單位)
CPU_Percentage:CPU 分配在DrawCall這件事情上的時(shí)間率 (百分比) FPS:希望的游戲幀率
-
DrawCall Batching(DC批處理)
Dynamic Batching(動(dòng)態(tài)批處理)
Static Batching(靜態(tài)批處理)
-
Bus總線帶寬
CPU完成一次DrawCall,除了需要調(diào)用一次DrawCall的命令之外,還需要把內(nèi)存中頂點(diǎn)數(shù)據(jù)、紋理貼圖、shader參數(shù)通過bus總線拷貝到內(nèi)存分配給GPU的顯存之中,注意這是拷貝,不是指針傳遞,速度不快。項(xiàng)目中不會(huì)同時(shí)出現(xiàn)的資源不要打包到一起,保證單張合并紋理不大于1024*1024一般就不會(huì)有問題了。
3.6.2-CPU優(yōu)化最直接的方法
-
VSync(垂直同步)是CPU優(yōu)化最直接的方式(發(fā)熱、耗電原因之一)
-
打開Edit-Project Settings-Quality找到V Sync Count
V Sync Count
Don’t Sync 不同步
Every V Blank 每一個(gè)垂直同步
Every Second V Blank 每一秒垂直同步
通常我們選擇Don’t Sync,同時(shí)Application.targetFrameRate設(shè)置目標(biāo)FPS,讓性能保持一個(gè)好的狀態(tài)。注意選擇其他項(xiàng),Application.targetFrameRate設(shè)置不生效。
垂直同步講會(huì)在下節(jié)“5.2-Unity Profile中詳細(xì)介紹”
3.6.3-GPU優(yōu)化
渲染流程
GPU接收頂點(diǎn)數(shù)據(jù)作為輸入傳遞給頂點(diǎn)著色器。頂點(diǎn)著色器的處理單元是頂點(diǎn),輸入進(jìn)來的每個(gè)頂點(diǎn)都會(huì)調(diào)用一次頂點(diǎn)著色器。(頂點(diǎn)著色器本身不可以創(chuàng)建或銷毀任何頂點(diǎn),并無法得到頂點(diǎn)與頂點(diǎn)之間的關(guān)系)。頂點(diǎn)著色器是完全可編程的,它主要完成的工作有:坐標(biāo)變換和逐頂點(diǎn)光照。 坐標(biāo)變換:就是對(duì)頂點(diǎn)的坐標(biāo)進(jìn)行某種變換—把頂點(diǎn)坐標(biāo)從模型空間轉(zhuǎn)換到齊次裁剪空間。頂點(diǎn)的多少直接決定了三角形面的多少,也直接決定了GPU的渲染流水線的工作量,所以減少頂點(diǎn)數(shù)是一個(gè)比較重要的優(yōu)化點(diǎn)。那么減少頂點(diǎn)怎么操作呢,又有哪些途徑?
- 頂點(diǎn)著色器 優(yōu)化基本幾何體(模型減面減頂點(diǎn)) 使用LOD(Level of detail)技術(shù) 使用遮擋剔除(Occlusion culling)技術(shù)
- 中間操作 曲面細(xì)分著色器:是一個(gè)可選的著色器,主要用于細(xì)分圖元 幾何著色器:是一個(gè)可選的著色器,可用于執(zhí)行逐圖元的著色操作,或者被用于產(chǎn)生更多的圖元。 裁剪:這一階段是可配置的。目的是把那些不在視野內(nèi)的頂點(diǎn)裁剪掉,并剔除某些三角形圖元的面片。部分在視野內(nèi)的圖元需要做裁剪處理,在裁剪邊緣產(chǎn)生新的頂點(diǎn)和三角形進(jìn)行處理。 屏幕映射:這一階段是可配置和編程的,負(fù)責(zé)把每個(gè)圖元的坐標(biāo)(三維坐標(biāo)系)轉(zhuǎn)換成屏幕坐標(biāo)(二維坐標(biāo)系)。
- 三角形設(shè)置:開始進(jìn)入光柵化階段,不再是數(shù)學(xué)上點(diǎn)了,而會(huì)把所有的點(diǎn)都映射到屏幕的具體像素坐標(biāo)上,計(jì)算每條邊上的像素坐標(biāo)而得到三角形邊界的表示方式即為三角形設(shè)置。 三角形遍歷:這一階段會(huì)檢查每個(gè)像素是否被一個(gè)三角風(fēng)格所覆蓋。如果覆蓋的話,就會(huì)生成一個(gè)片元(一個(gè)片元并不是真正意義上的像素,而是包含了很多狀態(tài)的集合,這些狀態(tài)用于計(jì)算每個(gè)像素的最終顏色。這些狀態(tài)包括了屏幕坐標(biāo)、深度信息,及從幾何階段輸出的頂點(diǎn)信息,如法線和紋理坐標(biāo)等。),這樣一個(gè)查找哪些像素被三角形覆蓋的過程就是三角形遍歷。
- 片元著色器 盡量減少overdraw 減少實(shí)時(shí)光照 不要使用動(dòng)態(tài)陰影 盡量使用簡(jiǎn)單的shader
片元著色器的輸入就是上一階段對(duì)頂點(diǎn)信息插值得到的結(jié)果,更具體點(diǎn)說,是根據(jù)從頂點(diǎn)著色器中輸出的數(shù)據(jù)插值得到的。而這一階段的輸出是一個(gè)或者多個(gè)顏色值。這一階段可以完成很多重要的渲染技術(shù),如紋理采樣,但是它的局限在于,它僅可以影響單個(gè)片元。片元著色器是比較花時(shí)間的,因?yàn)樗亲罱K顏色的計(jì)算者,在某些情況下,例如復(fù)雜燈光環(huán)境下,片元著色器會(huì)出現(xiàn)GPU流水線主要的拖后腿的存在。為了讓片元著色器的計(jì)算更加快,我們需要從很多方面進(jìn)行提前的優(yōu)化:片元著色器最容易拖后腿的情況就是,overdraw!和Android app的開發(fā)一樣,就是同一個(gè)像素點(diǎn)繪制了多次,某些情況會(huì)造成計(jì)算力的浪費(fèi),增加耗電量。前面提到的遮擋剔除有減少overdraw非常有用。在PC上,資源無限,為了得到最準(zhǔn)確的渲染結(jié)果,繪制順序可能是從后往前繪制不透明物體,然后再繪制透明物體進(jìn)行混合。但是在移動(dòng)平臺(tái)上,對(duì)于不透明物體,我們可以設(shè)置從前往后繪制,對(duì)于有透明通道的物體(很多UI紋理就是含有透明通道的),再設(shè)置從后往前繪制。unity中shader設(shè)置為“Geometry” 隊(duì)列的對(duì)象總是從前往后繪制的,而其他固定隊(duì)列(如“Transparent”“Overla”等)的物體,則都是從后往前繪制的。這意味這,我們可以盡量把物體的隊(duì)列設(shè)置為“Geometry” 。對(duì)于GUI,尤其要注意和設(shè)計(jì)師商量,能用不透明的設(shè)計(jì)就用不透明的,對(duì)于粒子效果,也要注意不要引入透明值,多半情況下,移動(dòng)平臺(tái)的粒子效果透明值沒有作用。
移動(dòng)平臺(tái)的最大敵人。一個(gè)場(chǎng)景里如果包含了三個(gè)逐像素的點(diǎn)光源,而且使用了逐像素的shader,那么很有可能將Draw Calls提高了三倍,同時(shí)也會(huì)增加overdraws。這是因?yàn)?#xff0c;對(duì)于逐像素的光源來說,被這些光源照亮的物體要被再渲染一次。更糟糕的是,無論是動(dòng)態(tài)批處理還是動(dòng)態(tài)批處理(其實(shí)文檔中只提到了對(duì)動(dòng)態(tài)批處理的影響,但不知道為什么實(shí)驗(yàn)結(jié)果對(duì)靜態(tài)批處理也沒有用),對(duì)于這種逐像素的pass都無法進(jìn)行批處理,也就是說,它們會(huì)中斷批處理。所以當(dāng)你需要光照效果時(shí),可以使用Lightmaps,提前烘焙好,提前把場(chǎng)景中的光照信息存儲(chǔ)在一張光照紋理中,然后在運(yùn)行時(shí)刻只需要根據(jù)紋理采樣得到光照信息即可。當(dāng)你需要金屬性強(qiáng)(鏡面)的效果,可以使用Light Probes。當(dāng)你需要一束光的時(shí)候,可以使用體積光去模擬這個(gè)效果。
動(dòng)態(tài)陰影很酷,但是對(duì)于片元著色器來說是災(zāi)難,陰影計(jì)算是三角投影計(jì)算,非常耗性能。如果想要陰影,可以使用
3.7-研發(fā)團(tuán)隊(duì)需要關(guān)注的引擎模塊
3.8-開始優(yōu)化工作
前面已經(jīng)介紹了性能相關(guān)概念以及需關(guān)注模塊,接下來該開始優(yōu)化工作,具體步驟如下:
4-Unity3D性能優(yōu)化——初識(shí)Unity-Statistics
當(dāng)運(yùn)行一個(gè)U3D場(chǎng)景后,可以在界面右上方看到一個(gè)叫做"Status"的按鈕,點(diǎn)開它就會(huì)出現(xiàn)一個(gè)重疊界面顯示出實(shí)時(shí)統(tǒng)計(jì)數(shù)據(jù),比如下圖這種樣子:
? 如果你是一名U3D開發(fā)新手,或者對(duì)此功能非常不熟悉,那么你可能會(huì)在游戲優(yōu)化過程中遇到很多麻煩。接下來的篇幅著重講講該窗口的作用和必要的相關(guān)名詞解釋。
? Statistics窗口,全稱叫做 Rendering Statistics Window,即渲染統(tǒng)計(jì)窗口(或渲染數(shù)據(jù)統(tǒng)計(jì)窗口),窗口中羅列出關(guān)于聲音、圖像、網(wǎng)絡(luò)狀況等多種統(tǒng)計(jì)信息
- FPS(Time per frame andFPS):frames per seconds表示引擎處理和渲染一個(gè)游戲幀所花費(fèi)的時(shí)間,該數(shù)字主要受到場(chǎng)景中渲染物體數(shù)量和 GPU性能的影響,FPS數(shù)值越高,游戲場(chǎng)景的動(dòng)畫顯示會(huì)更加平滑和流暢。一般來說,超過30FPS的畫面人眼不會(huì)感覺到卡,由于視覺殘留的特性,光在視網(wǎng)膜上停止總用后人眼還會(huì)保持1/24秒左右的時(shí)間,因此游戲畫面每秒幀數(shù)至少要保證在30以上。另外,Unity中的FPS數(shù)值僅包括此游戲Scene里更新和渲染的幀,編輯器中繪制的Scene和其它監(jiān)視窗口的進(jìn)程不包括在內(nèi)。
- CPU:獲取到當(dāng)前占用CPU進(jìn)行計(jì)算的時(shí)間絕對(duì)值,或時(shí)間點(diǎn),如果Unity主進(jìn)程處于掛斷或休眠狀態(tài)時(shí),CPU time將會(huì)保持不變。
- Render thread:GPU渲染線程處理圖像所花費(fèi)的時(shí)間,具體數(shù)值由GPU性能來決定,
- Batches:即Batched Draw Calls,是Unity內(nèi)置的Draw Call Batching技術(shù)。
? 首先解釋下什么叫做“Draw call”,CPU每次通知GPU發(fā)出一個(gè)glDrawElements(OpenGl中的圖元渲染函數(shù))或DrawIndexedPrimitive(DirectX中的頂點(diǎn)繪制方法)的過程稱為一次Draw call,一般來說,引擎每對(duì)一個(gè)物體進(jìn)行一次DrawCall,就會(huì)產(chǎn)生一個(gè)Batch,這個(gè)Batch里包含著該物體所有的網(wǎng)格和頂點(diǎn)數(shù)據(jù),當(dāng)渲染另一個(gè)相同的物體時(shí),引擎會(huì)直接調(diào)用Batch里的信息,將相關(guān)頂點(diǎn)數(shù)據(jù)直接送到GPU,從而讓渲染過程更加高效,即Batching技術(shù)是將所有材質(zhì)相近的物體進(jìn)行合并渲染。
? 對(duì)于含有多個(gè)不同Shader和Material的物體,渲染的過程比較耗時(shí),因?yàn)闀?huì)產(chǎn)生多個(gè)Batches。每次對(duì)物體的材質(zhì)或者貼圖進(jìn)行修改,都會(huì)影響B(tài)atches里數(shù)據(jù)集的構(gòu)成。因此,如果場(chǎng)景中有大量材質(zhì)不同的物體,會(huì)很明顯的影響到GPU的渲染效率。這里說幾點(diǎn)關(guān)于Batches優(yōu)化相關(guān)的方案。
雖然Unity引擎自帶Draw Call Batching技術(shù),我們也可以通過手動(dòng)的方式合并材質(zhì)接近的物體;
盡量不要修改Batches里物體的Scale,因?yàn)檫@樣會(huì)生成新的Batch。
為了提升GPU的渲染效率,應(yīng)當(dāng)盡可能的在一個(gè)物體上使用較少的材質(zhì),減少Batches過多的開銷;
對(duì)于場(chǎng)景中不會(huì)運(yùn)動(dòng)的物體,考慮設(shè)置Static屬性,Static聲明的物體會(huì)自動(dòng)進(jìn)行內(nèi)部批處理優(yōu)化。
- Verts:攝像機(jī)視野(field of view)內(nèi)渲染的頂點(diǎn)總數(shù)。
- Tris: 攝像機(jī)視野(field of view)內(nèi)渲染的的三角面總數(shù)量。
? 關(guān)于Tris和Verts,突然想到一些問題,這里需要多嘴說幾句:
Camera的渲染性能受到Draw calls的影響。之前說過,對(duì)一個(gè)物體進(jìn)行渲染,會(huì)生成相應(yīng)的Draw call,處理一個(gè)Draw Call的時(shí)間是由它上邊的Tris和Verts數(shù)目決定。盡可能得合并物體,會(huì)很大程度的提高性能。舉個(gè)很簡(jiǎn)單例子,比如場(chǎng)景一種有1000個(gè)不同的物體,每個(gè)物體都有10個(gè)Tris;場(chǎng)景二中有10個(gè)不同的物體,每個(gè)物體有1000個(gè)Tris。在渲染處理中,場(chǎng)景一中會(huì)產(chǎn)生1000個(gè)Draw Calls,它的渲染時(shí)間明顯比場(chǎng)景二慢。
Unity stats 視圖中的 Tris 和 Verts 并不僅僅是視錐中的梯形內(nèi)的 Tris 和 Verts,而是Camera中 field of view所有取值下的tris和verts,換句話說,哪怕你在當(dāng)前game視圖中看不到這個(gè) cube,如果當(dāng)你把 field of view調(diào)大到 179 過程中都看不到這個(gè)cube,stats面板才不會(huì)統(tǒng)計(jì),GPU才不會(huì)渲染,否則都會(huì)渲染,而且unity不會(huì)把模型拆分,這個(gè)模型哪怕只有1個(gè)頂點(diǎn)需要渲染,unity也會(huì)把整個(gè)模型都渲出來。(參考自Mess的《Unity Camera組件部分參數(shù)詳解》)
之前有童鞋問過我,新建一個(gè)空的場(chǎng)景,里邊沒有添加任何物體,為什么Status面板上顯示有1.7k Tris以及5.0kVerts。這是因?yàn)榭盏膱?chǎng)景自帶默認(rèn)的天空盒。點(diǎn)擊Windows—Lighting打開Lighting下的Scene面板,把Skybox里的材質(zhì)設(shè)為空,比如像我下圖這樣:**
? 可以看到,場(chǎng)景中的Tris數(shù)量變?yōu)?,Verts數(shù)量變?yōu)榱?,這是由于攝像機(jī)存在的關(guān)系,刪掉它,你就會(huì)發(fā)現(xiàn)Tris 和 Verts 都變?yōu)?了。
- Screen:獲當(dāng)前Game屏幕的分辨率大小,后邊的2.1MB表示總的內(nèi)存使用數(shù)值。
- SetPass calls:又碰到一個(gè)神奇的詞“SetPass calls”。如果你是一個(gè)Unity的老用戶,你可能會(huì)注意到原來的Stats面板的第一項(xiàng)是“Draw calls”,然而到了Unity5.X版本,Stats上沒有了“Draw calls”,卻多出來一項(xiàng)”SetPass calls“,那么這個(gè)玩意到底是做什么的???(你猜… )← ←!
? 感覺又要說一大堆東西了…之前有講到Batches,比如說場(chǎng)景中有100個(gè)gameobject,它們擁有完全一樣的Material,那么這100個(gè)物體很可能會(huì)被Unity里的Batching機(jī)制結(jié)合成一個(gè)Batch。所以用“Batches”來描述Unity的渲染性能是不太合適的,它只能反映出場(chǎng)景中需要批處理物體的數(shù)量。那么可否用“Draw calls”來描述呢?答案同樣是不適合。每一個(gè)“Draw calls”是CPU發(fā)送個(gè)GPU的一個(gè)渲染請(qǐng)求,請(qǐng)求中包括渲染對(duì)象所有的頂點(diǎn)參數(shù)、三角面、索引值、圖元個(gè)數(shù)等,這個(gè)請(qǐng)求并不會(huì)占用過多的消耗,真正消耗渲染資源的是在GPU得到請(qǐng)求指令后,把指令發(fā)送給對(duì)應(yīng)物體的Shader,讓Shader讀取指令并通知相應(yīng)的渲染通道(Pass)進(jìn)行渲染操作。
- Shadow casters:表示場(chǎng)景中有多少個(gè)可以投射陰影的物體,一般這些物體都作為場(chǎng)景中的光源。
- visible skinned meshed:渲染皮膚網(wǎng)格的數(shù)量。
- Animations:正在播放動(dòng)畫的數(shù)量。
5-Unity3D性能優(yōu)化——Unity篇
5.1-Unity 腳本開發(fā)方面
基本思想減少動(dòng)態(tài)內(nèi)存分配和釋放,減少耗時(shí)函數(shù)的調(diào)用,盡可能的使用緩存。
1:控制Startcorountien 的使用次數(shù),避免頻繁的開啟協(xié)程,開啟一個(gè)協(xié)程至少分配37B的內(nèi)存空間(corountien類的實(shí)例 分配21B Enumerator 分配16B)
如果僅僅是延遲運(yùn)行,定時(shí)運(yùn)行函數(shù),可以使用invoke invokerepeating 代替
2:對(duì)象查找 gameobject.find(); 全局的查找方式,無法查找隱藏的對(duì)象,效率低下
Transform.find 從自身位置查找,性能較優(yōu)的推薦方法
Gameobject.findobjectswithTag 通過標(biāo)簽查找,效率較高,但是需要設(shè)置標(biāo)簽稍顯麻煩。
建議在Awake 或者Start 函數(shù)中查找對(duì)象并保存引用,切忌在Update中動(dòng)態(tài)的查找
3:緩存組件
注意獲取組件的方法GetCompent 方法大約會(huì)分配39B的堆內(nèi)存,并且GetCompent是去訪問unity原生代碼效率很低,使用緩存訪問,性能相差幾十倍。緩存訪問性能要高得多。
(a)盡量避免在Update函數(shù)中做計(jì)算,可以使用InvokeRepeating,間隔一段計(jì)算一次。
(b)避免使用SendMessage 性能底下,使用了反射機(jī)制,要使用委托 delegate的事件模擬消息機(jī)制。
(c)刪除無用的預(yù)制體上的組件,刪除類中無用的函數(shù)。
(d)腳本的禁用,腳本使用的時(shí)候開啟,不用的時(shí)候關(guān)閉,需要專門寫一套管理機(jī)制。
4.減少.Count .Length的調(diào)用
for(int i = 0; i < list.Count; i++) { //do something}應(yīng)改為:
for(int i = 0, j = list.Count; i < j ; i++) { //do something}5.減少 gameobject,transform,GetComponent 的使用
我們可以在Start()方法中預(yù)先存儲(chǔ)好這些值,之后使用的時(shí)候調(diào)用預(yù)先存儲(chǔ)好的值即可。
GameObject m_gameObject; Transform m_transform; Text m_text; void Start () { m_gameObject = gameObject; m_transform = transform; m_text = GetComponent<Text>(); }6.減少SetActive(bool)的使用
對(duì)于要頻繁顯示隱藏的物體,我們可以減少使用SetActive(bool),而是通過transform.scale信息將其縮小為0,例如UI的隱藏。
7.如果某個(gè)活動(dòng)狀態(tài)(gameObject.active == true)的GameObject上的腳本中含有Awake()方法,即使這個(gè)腳本沒有被啟用(enabled==false),Awake()方法也會(huì)執(zhí)行。如果游戲中含有非常多的帶有Update()方法的MonoBehavior,應(yīng)該嘗試改變代碼結(jié)構(gòu)來減少開銷,
8.Camera.main所引起的問題與Find()方法類似,應(yīng)該避免使用Camera.main并且手動(dòng)管理對(duì)相機(jī)的引用。
9.Update()和LateUpdate()等事件方法的每次調(diào)用都需要引擎代碼與托管代碼之間進(jìn)行通信,還要Unity進(jìn)行安全檢查(GameObject狀態(tài)是否合法等),即使這些事件方法的方法體是空的,引擎任然會(huì)對(duì)其進(jìn)行調(diào)用。因此,為避免浪費(fèi)CPU時(shí)間,應(yīng)該 刪除空的事件方法。
10.設(shè)置position和rotation會(huì)觸發(fā)內(nèi)部的OnTransformChanged事件并傳播到所有的子級(jí)對(duì)象中,對(duì)于含有非常對(duì)子物體的對(duì)象來說,這種操作開銷很大,應(yīng)該 減少對(duì)position和rotation的修改。
11.嘗試使用localPosition替代position。localPosition存儲(chǔ)在transform中,訪問該值時(shí),Unity會(huì)直接將其返回,而position在每次訪問時(shí)都會(huì)重新計(jì)算,如果要經(jīng)常獲取position,可以將其緩存起來。
12.不要附加 Animation Component 在靜態(tài)實(shí)體上附加 Animation 部件雖然對(duì)結(jié)果沒有影響,但卻會(huì)增加一定的 CPU 開銷來調(diào)用這一組件,所以盡量去掉該組件。
13.關(guān)于相機(jī)相機(jī)
裁剪平面
將遠(yuǎn)平面設(shè)置成合適的距離。遠(yuǎn)平面過大會(huì)將一些不必要的物體加入渲染,降低效率。
根據(jù)不同的物體設(shè)置不同的遠(yuǎn)裁剪平面
Unity 提供了可以根據(jù)不同的 layer 來設(shè)置不同的 view distance ,所以我們可以實(shí)現(xiàn)將物體進(jìn)行分層,大物體層設(shè)置的可視距離大些,而小物體層可以設(shè)置地小些,另外,一些開銷比較大的實(shí)體(如粒子系統(tǒng))可以設(shè)置得更小些等等。
14.非運(yùn)動(dòng)物體盡量打上 Static 標(biāo)簽
Unity 在運(yùn)行時(shí)會(huì)對(duì) static 物體進(jìn)行自動(dòng)優(yōu)化處理,所以應(yīng)該盡可能將非運(yùn)行實(shí)體勾上 static 標(biāo)簽。
15.不要實(shí)例化(Instantiate)對(duì)象,事先建好對(duì)象池,并使用Translate“生成”對(duì)象;
16.使用 Resource.Load 方法在需要的時(shí)候再讀取資源;各種資源在使用完成后,盡快用Resource.UnloadAsset和UnloadUnusedAsset卸載掉;
17.靈活運(yùn)用AssetBundle的Load和Unload方法動(dòng)態(tài)加載資源,避免主要場(chǎng)景內(nèi)的初始化內(nèi)存占用過高;(實(shí)現(xiàn)起來真的很難…)
18.采用www加載了AssetBundle后,要用www.Dispose 及時(shí)釋放;
19.在關(guān)卡內(nèi)謹(jǐn)慎使用DontDestroyOnLoad,被標(biāo)注的資源會(huì)常駐內(nèi)存;
20.盡量少使用FindObjectsOfType函數(shù),這個(gè)函數(shù)非常慢,盡量少用且一定不要在Update里調(diào)用;
21.場(chǎng)景中若存在不需要顯示的模型,盡量不使用SetActive方法,而是將其Scale置為Vectory.Zero,并禁用其MeshRenderer
24.場(chǎng)景中絕對(duì)靜態(tài)的模型可以使用合并Mesh操作來進(jìn)行減少DrawCall
private void CombineMesh(){ for (int i = 0; i < rootTr.Count; i++){GameObject rootModel = rootTr[i].gameObject;//給父物體添加meshfilterMeshFilter combineMeshFilter = rootModel.GetComponent<MeshFilter>();if (combineMeshFilter == null){combineMeshFilter = rootModel.AddComponent<MeshFilter>();}//獲取子物體中所有的meshfiltervar filters = rootModel.GetComponentsInChildren<MeshFilter>();//將子物體的meshfilter添加到combineInstance中CombineInstance[] combines = new CombineInstance[filters.Length];for (int j = 0; j < filters.Length; j++){combines[j].mesh = filters[j].sharedMesh;combines[j].transform = filters[j].transform.localToWorldMatrix;}//合并mesh并將mesh賦值給父物體Mesh finalmesh = new Mesh(); finalmesh.CombineMeshes(combines);rootModel.gameObject.GetComponent<MeshFilter>().sharedMesh = finalmesh;}}25.支持分級(jí)Log(自定義logger),避免大量且頻繁的Log,在構(gòu)建時(shí)屏蔽log。
26.使用gameObject.CompareTag(“XXX”)而非gameObject.tag,后者會(huì)產(chǎn)生額外的內(nèi)存與性能消耗。
27.使用內(nèi)建的常量,例如Vector3.zero等等,避免頻繁創(chuàng)建相同的對(duì)象。
5.2-Unity資源相關(guān)優(yōu)化
這節(jié)主要講unity資源優(yōu)化,先學(xué)習(xí)一個(gè)概念:資源管道,原始文件通過內(nèi)容管道變成了一個(gè)可被Unity高效使用的中間文件 ,這不是Unity里面的一個(gè)概念,但Unity的工作行為和它很類似;Unity在導(dǎo)入資源的時(shí)候可以使用類型豐富的文件,這不意味著在我們生成的App中同樣也是這些文件,資源通過Unity的資源管道變成了較為統(tǒng)一的格式。我們?cè)趯?dǎo)入資源的時(shí)候有很多參數(shù)可以調(diào)整,
5.2.1-音頻
在Unity中導(dǎo)入聲音文件我們能看到類似下面的面板 , 我們先看第一個(gè)選擇Load Type,他有三個(gè)可值:
Decompress On Load,Compressed In Memory ,Streaming。
Decompress On Load
在硬盤上壓縮這個(gè)文件,并在第一次加載到內(nèi)存的時(shí)候解壓它,這是加載聲音文件的默認(rèn)選項(xiàng),大多數(shù)情況下我們應(yīng)該使用這個(gè)選擇 。
加載后解壓縮聲音,聲音文件將在他們加載不久后就解壓,這個(gè)選項(xiàng)適用于較小的壓縮聲音,以避免即時(shí)解壓縮的性能開銷。要知道在加載時(shí)解壓 Vorbis編碼的聲音將使用的內(nèi)存是壓縮狀態(tài)的十倍或更多(ADPCM編碼大概3.5倍)所以不要使用此選項(xiàng)用于大文件。
Decompress On Load 默認(rèn)選項(xiàng)適用于小文件。
? Compressed In Memory
? 保持聲音在內(nèi)存中是壓縮的并在播放時(shí)解壓縮。這有更多的CPU開銷(尤其是OGG / Vorbis格式的壓縮文件),但可以提高加載速度并減少內(nèi)存消 耗,因此這個(gè)選項(xiàng)適用于大文件。在性能窗口可以看"DSP CPU"。
? Compressed In Memory 適用于大文件。
? Streaming 選項(xiàng)
? 直接從磁盤流音頻數(shù)據(jù)。這只使用了原始聲音占內(nèi)存大小的很小一部分。 該方法使用最少的內(nèi)存和最多的CPU,它有個(gè)很明顯的缺點(diǎn)就是不能被引用超過一次。試著讓 Audio Clip產(chǎn)生多個(gè)副本的時(shí)候會(huì)每個(gè)都產(chǎn)生數(shù)據(jù)緩沖區(qū),如果非要這么做會(huì)產(chǎn)生大量的內(nèi)存和cpu消耗。因此這個(gè)選擇最好是給單實(shí)例的Audio Clip,如背景和環(huán)境音效。對(duì)于手游而言不要優(yōu)先考慮使用這種方式。
在場(chǎng)景中把一個(gè)Audio Clip賦值給Audio Source組件,這個(gè)音頻文件將在場(chǎng)景初始化的時(shí)候加載到內(nèi)存中。但是如果它啟用了,加載Audio Clip就變成了一個(gè)后臺(tái)任務(wù)它將推遲到場(chǎng)景初始化完成后再加載,換句話說就是在游戲開始后開始加載。 通過啟用這個(gè)選擇我們可以提高場(chǎng)景的啟動(dòng)速度,但Play聲音的時(shí)候它還在后臺(tái)加載的話播放將推遲到加載完成,我們可以使用AudioClip對(duì)象的loadState 屬性來檢查是否加載完成以保證聲音在不會(huì)在一個(gè)不恰當(dāng)?shù)臅r(shí)間播放。
? Preload Audio Data 默認(rèn)是啟用的,它表示Unity自動(dòng)在場(chǎng)景初始化時(shí)加載文件。禁用此選項(xiàng)將推遲加載直到 AudioSource的 Play() 或 PlayOneShot() 方法執(zhí)行的時(shí)候。從硬盤加載聲音文件,然后解壓,再push到內(nèi)存里然后再播放,這么一系列的動(dòng)作可能 會(huì)導(dǎo)致 CPU的峰值。
? 由于播放延遲和性能消耗,不建議在播放的瞬間加載。我們應(yīng)該控制加載在播放之前的某些方便的時(shí)間使用AudioClip對(duì)象的LoadAudioData()方法加載。我們也可以用AudioClip對(duì)象的UnloadAudioData()方法手動(dòng)控制聲音文件的內(nèi)存釋放。
? 聲音文件的編碼格式和質(zhì)量
? Unity支持三種聲音文件編碼格式的,由于平臺(tái)依賴性在某些特殊情況下會(huì)有其他的選項(xiàng)(如Xbox One的XMA和PS Vita的HEVAG )
- Vorbis/MP3
- PCM
- ADPCM
? 我們導(dǎo)入到Unity的音頻文件可能是各種各樣常見的音頻格式,但通過內(nèi)容管道,Standalone、WebGL和其他的一些非移動(dòng)平臺(tái)使用Ogg-Vorbis格式的壓縮,而移動(dòng)平臺(tái)使用MPEG-3(MP3)格式。
? PCM 提供高品質(zhì)但犧牲文件大小最適合使用在很短的音效上。
? ADPCM 這種格式適用于大量音效上如腳步爆破和武器,它比PCM小3.5倍但CPU使用率遠(yuǎn)低于Vorbis/MP3
? Vorbis/MP3 比PCM小但是品質(zhì)比PCM低,比ADPCM消耗更多CPU。但大多數(shù)情況下我們還是應(yīng)該使用這種格式,這個(gè)選擇還多了個(gè)Quality可以調(diào)節(jié)質(zhì)量改變文件大小增強(qiáng)音頻性能
**盡量減少音頻數(shù)量 **
? 因?yàn)槊總€(gè) Audio Source的播放都會(huì)消耗一定量的cpu,所以我們可以控制場(chǎng)景中 Audio Source的數(shù)量來節(jié)省cpu。
? 一種方式是控制我們的音頻來源,這種方式我們硬盤上 的AudioClip可以同時(shí)播放的數(shù)量和總共播放的總數(shù)進(jìn)行節(jié)流控制。我們通常會(huì)做一個(gè)AudioPool做這些節(jié)流操作,這很適合2d聲音和單實(shí)例的3d聲音(3d聲音在播放的時(shí)候仍然要放在場(chǎng)景中具體的位置 )。 更甚至是移除一些音頻,這種做法會(huì)影響到用戶體驗(yàn),我們應(yīng)該在質(zhì)量和性能上做權(quán)衡。
減少AudioClip引用
? 場(chǎng)景中每個(gè)Audio Source的Audio Clip引用和Preload Audio Data 啟用都將消耗一定量的內(nèi)存(壓縮解壓或者緩存),這些內(nèi)存將貫徹整個(gè)場(chǎng)景,如果兩個(gè)或兩個(gè)以上的Audio Source引用相同的Audio Clip沒有額外的內(nèi)存消耗。Audio Clips在Unity中是非托管資源這意味著他們不會(huì)通過設(shè)置為null釋放內(nèi)存。
? Unity希望我們能夠加載和釋放這些資源,經(jīng)常使用的音效長(zhǎng)久的在內(nèi)存中保存文件是合理的,因?yàn)槊看渭虞d一個(gè)文件到
? 內(nèi)存中都會(huì)消耗CPU。然而,如果我們發(fā)現(xiàn)因?yàn)橐粜Ф褂锰嗟膬?nèi)存的時(shí)候,我們必須做出艱難的選擇是降低音頻質(zhì)量還是完全移除他們來節(jié)省內(nèi)存。另一方面,保存不常用的音效在一個(gè)很長(zhǎng)的場(chǎng)景中將會(huì)造成重大問題。
? 我們可能會(huì)有很多一次性音效,如對(duì)話片段,它們沒有需 要長(zhǎng)久保存在內(nèi)存中。創(chuàng)建Audio Sources并分配一個(gè)AudioClip如果只是這樣即使在游戲中只有一次使用也會(huì)導(dǎo)致內(nèi)存消耗過剩,我們應(yīng)該利用Resources.Load()和Resources.UnloadAsset()來保持需要播放的音頻數(shù)據(jù)在內(nèi)存中,一旦它不在需要就立即釋放它。
5.2.2-Texture
? 術(shù)語“Texture” 和“Sprite”新手在游戲開發(fā)中經(jīng)常會(huì)產(chǎn)生困惑,所以值得區(qū)分下,在Unity3d中Texture僅僅是個(gè)圖片,我們習(xí)慣讀作貼圖。本質(zhì)上來講它是一個(gè)大的顏色數(shù)組告訴程序每個(gè)像素是什么顏色。
? 而Sprite是一個(gè)2d網(wǎng)格,一個(gè)平對(duì)著當(dāng)前攝像機(jī)的quad,我們習(xí)慣讀作精靈。
? 還有一種東西叫做Sprite Sheets,我們稱它為圖集;它用一張大圖包含了很多圖片,最常見的使用時(shí)2d角色動(dòng)畫。這些文件可以用Unity的Sprite Editor工具分割成小圖做幀動(dòng)畫。
? 別管這些令人困擾的命名,來簡(jiǎn)單談下Texture:我們導(dǎo)入的這些圖片文件一般都生成于Adobe Photoshop國(guó)內(nèi)基本都是或者一些小眾軟件Gimp等。在運(yùn)行的時(shí)候這些文件被加載到內(nèi)存中,然后Push到GPU,應(yīng)用Shader完成一次Draw Call。一般情況下,渲染一次擁有一個(gè)網(wǎng)格并攜帶一種材質(zhì)的物體便會(huì)使用一次Draw Call,Draw Call的次數(shù)是決定性能比較重要的指標(biāo)。
? 壓縮格式
? 像音頻文件一樣,Unity為我們多種文件壓縮技術(shù)來更有效的儲(chǔ)存文件。在導(dǎo)入Texture文件時(shí),有幾個(gè)選項(xiàng)我們可以設(shè)置。首先是Texture Type ,這個(gè)設(shè)置不會(huì)影響到文件本身而是說Unity將如何解析,操作和壓縮它。
? 在Texture Type為Texture模式下Unity只給我們看到四個(gè)格式選項(xiàng)
Compressed, 16-bit,True Color和Crunched
? 如果我們將Texture Type設(shè)置為Advanced,那么我們就有了更多的設(shè)置選項(xiàng),它給了我們更多的 Texture解析的控制權(quán)。
? 紋理的壓縮方式取決于大小和質(zhì)量的平衡,更多的需要做的是了解每種格式來選擇最適合的。但是這里著重要提一下的是Crunched格式,這個(gè)格式壓縮需要很長(zhǎng)時(shí)間,但在運(yùn)行時(shí)減壓是非???。在新版本的Unity中加入了這個(gè)選擇可以看到它被壓縮成PVRTC格式,推薦在開發(fā)手機(jī)應(yīng)用程序時(shí)使用 PVRTC 紋理。你應(yīng)該知道當(dāng)使用 PVRTC 時(shí)與標(biāo)準(zhǔn)JPEG或PNG圖像相比有可能有些圖像質(zhì)量的下降。是否值得在你的程序中做出一些犧牲取決于一些因素,但使用 PVRTC 紋理可以節(jié)省大量的內(nèi)存空間,PVRTC 是個(gè)很好的選擇。
? 明智的使用 Mip Maps
? 呈現(xiàn)小物件的時(shí)候,像巖石樹木這樣的遠(yuǎn)處物體,一個(gè)高精度的貼圖對(duì)于玩家來說是沒有意義的,因?yàn)榭床坏侥敲炊嗟募?xì)節(jié)。但是如果使用這張高精度的貼圖會(huì)損失原本不應(yīng)該的性能,這時(shí)候需要一個(gè)較少細(xì)節(jié)的紋理來提升程序性能。
? Mip Maps的發(fā)明就是來解決這個(gè)問題 的,通過啟用 Generate Mip Maps
? 聽著好像很爽的樣子但是他在我們手游中幾乎沒用,啟用Mip Maps還會(huì)讓最后生成的紋理文件增大33%左右。那他存在的意義何在如何什么時(shí)候使用它呢?它唯一有用的地方是攝像機(jī)需要渲染不同距離的紋理時(shí)。如果我們的紋理總是呈現(xiàn)在攝像頭的同樣距離,這時(shí)候開啟Mip Map就一點(diǎn)用都沒只是在浪費(fèi)空間。還有如果只有單一的一種遠(yuǎn)距離物體我們也應(yīng)該禁用它,應(yīng)該從原紋理縮放一個(gè)小紋理給這個(gè)遠(yuǎn)處物體。
? **以下時(shí)候應(yīng)該禁用這個(gè)選項(xiàng): **
- 在2d游戲中的幾乎所有的紋理(2d游戲正交投影不存在近大遠(yuǎn)小)
- UI界面
- Mesh貼圖,Sprites,Particle Effects,因?yàn)樗麄兛偸窃跀z像機(jī)差不多的距離渲染不會(huì)有很明顯的距離差
? 考慮把圖片打包起來變成圖集
? 把圖塊變成圖集之后會(huì)減少Draw Calls的次數(shù)(其實(shí)就是減少M(fèi)aterial 數(shù)量)明顯減少CPU消耗,內(nèi)存消耗上基本相同。一個(gè)圖集下的所有貼圖都應(yīng)該是使用相同Shader的,不然后果很嚴(yán)重做了還不如不做。
? 圖集經(jīng)常使用在UI界面,2d游戲中,它在手游中幾乎是必不可少的,因?yàn)镈raw Calls往往會(huì)成為手游的瓶頸。
? 我們不應(yīng)該手動(dòng)去生成圖集,有很多現(xiàn)成的工具有Unity自己的有第三方的還有一些源碼工程。要注意圖集不能大于目標(biāo)平臺(tái)的最大要求,圖集最好合理的分布不要為了節(jié)省空間而視圖把圖集塞滿。如果渲染器需要從不同的圖集中調(diào)用紋理會(huì)導(dǎo)致大量的高速緩存命中失敗,從而有引發(fā)內(nèi)存帶寬不足等等問題。 如果做pc游戲的話圖集就沒有特別的必要了,因?yàn)镈raw Calls很少會(huì)成為pc游戲的性能瓶頸。正確的使用圖集它不是僅僅把圖片堆起來。
? 對(duì)于通用紋理,盡可能的使用九宮格。如果用大塊的紋理則會(huì)占用較大的內(nèi)存空間。而針對(duì)對(duì)稱紋理則可以使用shader或者Scale翻轉(zhuǎn)等方法來重復(fù)利用以減小內(nèi)存消耗。
? **調(diào)整非正方形紋理的壓縮率 **
? 不建議導(dǎo)入非正方形或者非2的次冪的紋理到我們的應(yīng)用程序,因?yàn)镚PU往往需要把紋理是變成正方形和2的次冪導(dǎo)致不必要的處理畸形的紋理尺 寸工作量。如果紋理不是2的次冪,Unity會(huì)scale、pad紋理,以使得達(dá)到2的次冪,這樣會(huì)花費(fèi)更多內(nèi)存,讓加載更慢,所以建議是避免導(dǎo)入非正方形和2的次冪的紋理。
? Other
- 通過設(shè)置“MaxSize”來限制圖片的大小,一般來說我們應(yīng)該講圖片縮小到肉眼剛好看不出壓縮的程度。圖片不要超過2048.
- “Format”則表示壓縮的方式,不同的平臺(tái)我們應(yīng)該用不同的壓縮方式來進(jìn)行壓縮。例如:安卓使用ETC而IOS則使用PVRTC。
- 一般情況下我們需要關(guān)閉圖片的Read&Write選項(xiàng),否則內(nèi)存會(huì)大一倍。除非我們對(duì)圖像需要進(jìn)行讀寫操作。
- 針對(duì)顏色范圍不大的素材,我們可以降低圖片的色階以減小圖片的大小。
5.2.3-模型
- 保證不可讀寫。
- 將沒有動(dòng)畫的模型上的動(dòng)畫腳本去除,否則會(huì)消耗cpu。
- 多個(gè)角色分享一套rig,可以解決資源。
- 使用MeshCompression
- 模型莫名期末有很多particle view物體。在3DMAX中當(dāng)你按下6時(shí)候,會(huì)彈出particle view窗口。同時(shí)會(huì)生成一個(gè)多余的particle view物體(這個(gè)物體在大綱中是找不到的,但當(dāng)使用Ctrl+A的時(shí)候,他就會(huì)被選到,而且這個(gè)物體會(huì)占用一定的文件空間),在導(dǎo)出時(shí)particle view會(huì)嚴(yán)重?cái)_亂物體的軸心。決方法很簡(jiǎn)單,只要在max導(dǎo)出之前,按下f11鍵,進(jìn)入max的腳本編輯器,輸入delete $’particle view’,回車,此時(shí)下一行會(huì)提示有幾個(gè)particle view被刪除了。看到這個(gè)數(shù)字即可以放心導(dǎo)出了。
- 當(dāng)動(dòng)畫播放出現(xiàn)褶皺、破損、奇葩的時(shí)候,估計(jì)是點(diǎn)受骨骼影響太多了,U3D設(shè)置中一個(gè)點(diǎn)最多只能受4個(gè)骨骼影響。
- 在UI中最好不要使用Mesh特效,否則無法判斷深度。除非使用RendererTexture來適應(yīng),盡可能讓美術(shù)用別的方案來替代。
5.2.4-文本文件優(yōu)化
- 解析文本是比較慢的。
- 將文本轉(zhuǎn)成二進(jìn)制文件,可以有效提高讀寫速度。
- 將文本分成小塊,只讀取需要的部分。讀入之后如有必要?jiǎng)tCache起來。
- 利用線程進(jìn)行多線程讀取。
5.2.5-AssetBundle管理
目前市面上的大多數(shù)游戲都要涉及到熱更新,從Unity5.0開始,簡(jiǎn)化的API讓AssetBundle的打包似乎不再是一件困難的工作,但是想要管理好AssetBundle的關(guān)系卻并不是那么容易的一件事。
在游戲中加載資源我使用過兩種做法,都需要自己對(duì)加載方式進(jìn)行封裝,通過多態(tài)來靈活加載工程內(nèi)或者熱更的資源:
一種是講所有需要加載的資源,例如prefab、文本等等放入到Resources下面(盡可能減少資源否則會(huì)影響啟動(dòng)速度),而依賴的資源放在別的文件夾下,熱更的時(shí)候?qū)①Y源加載到沙盒目錄下。在資源加載的時(shí)候,首先對(duì)沙盒目錄進(jìn)行檢查,若有的話,則使用熱更資源,否則就加載Resource下的資源。優(yōu)點(diǎn)是在工程內(nèi)不需要打包,而缺點(diǎn)是由于沒有打包,導(dǎo)致在最后出包的時(shí)候打包緩慢。
而另一種是將所有的資源打成AssetBundle放入StreamingAssets下,熱更的時(shí)候同樣十八AssetBundle下載到沙盒目錄下。資源加載的時(shí)候,首先對(duì)沙盒目錄進(jìn)行檢查,若有的話使用熱更資源,否則就加載StreamingAssets下的資源。在加載的時(shí)候需要提供資源名和包名,在編輯器下可以通過AssetDatabase來直接從編輯器中加載資源,通過將場(chǎng)景加入到BuildSetting中來加載場(chǎng)景,避免每次進(jìn)行修改的時(shí)候都需要重新打AssetBundle。這種方法在最后出包的時(shí)候比較快,在最終確定下資源正確性的時(shí)候構(gòu)建AssetBundle。
通過延遲加載來對(duì)AssetBundle進(jìn)行加載,在一般的使用場(chǎng)景下,我們并不需要將所有的AssetBundle進(jìn)行載入。在游戲中,我們將建立一張常用的Bundle列表,用于進(jìn)入場(chǎng)景時(shí)加載該場(chǎng)景中的常駐資源。而不一定會(huì)出現(xiàn)的資源則在需要的時(shí)候進(jìn)行即時(shí)加載,并且放入Bundle池中隨時(shí)準(zhǔn)備取用,當(dāng)Bundle閑置達(dá)到一定的時(shí)間后對(duì)其進(jìn)行自動(dòng)的卸載,這取決于該Bundle的使用頻度。在切換場(chǎng)景之后卸載該場(chǎng)景的常用Bundle而去加載另一個(gè)場(chǎng)景的常用Bundle。
要注意Bundle的細(xì)粒度,如果Bundle的細(xì)粒度超過一定數(shù)量的話必然會(huì)引起熱更包體積過大,玩家的更新需要下載更多的資源包,而在場(chǎng)景中也需要加載更多原本并不被需要的資源,而過小細(xì)粒度則會(huì)造成場(chǎng)景加載的緩慢,給管理上也會(huì)增加難度。所以適當(dāng)?shù)募?xì)粒度在AssetBundle的分包中也非常重要。
將公用的資源單獨(dú)打成包:如果一個(gè)資源本身沒有標(biāo)記任何的Bundle包,而被許多別的Bundle包引用則會(huì)被打入每一個(gè)引用它的Bundle中,造成包體積的膨脹。例如Shader這樣的材質(zhì)就很有必要單獨(dú)打成一個(gè)包用作公用,否則包體積和游戲內(nèi)存占用會(huì)變成一個(gè)大問題。
當(dāng)手機(jī)的容量較小時(shí),可以通過WWW.LoadFromCacheOrDownload來進(jìn)行加載,而不必?fù)?dān)心內(nèi)存不足的問題。
在將代碼打包到Prefab的時(shí)候?qū)τ贑omponent要用動(dòng)態(tài)加載的方式,考慮使用lua腳本進(jìn)行操作,或者是直接動(dòng)態(tài)從程序集加載,這樣可以避免資源與代碼不同步的情況發(fā)生。可以只對(duì)代碼進(jìn)行修改而不需要重新進(jìn)行資源打包。
在使用第二種方案建立項(xiàng)目的時(shí)候可以建立一個(gè)或者幾個(gè)資源項(xiàng)目,用于大量資源的打包,用于將AssetBundle打包并放入主項(xiàng)目中,在主項(xiàng)目的在打包的時(shí)候不必再對(duì)所有的AssetBundle資源進(jìn)行再打包,可以大大提高打包效率,并且可以將工作流放入到資源項(xiàng)目當(dāng)中,提高資源的迭代效率。
在為資源分包的時(shí)候可以按照文件夾來進(jìn)行區(qū)分,以便于管理。
當(dāng)在一個(gè)地方需要用到包中的一小個(gè)資源,例如一個(gè)2048*2048圖集中的一個(gè)小icon。拷貝一份,并且放入到目前需要使用的包中,避免由于小的資源需求而引入大內(nèi)存消耗。
5.2.6-資源工作流管理
作為程序,我們?cè)谫Y源上面花的精力自然是越少越好,但是如果沒有好的工具鏈,好的流程,我們必定將會(huì)困在永遠(yuǎn)做不完的資源管理中。美術(shù)發(fā)過來的max文件或許需要經(jīng)過你的導(dǎo)出、導(dǎo)入到Unity、拖成預(yù)制體、掛動(dòng)畫、掛碰撞盒等等的操作之后才能成為一個(gè)真正可用的資源。這個(gè)時(shí)候一個(gè)好的工具顯得格外重要。
- uTomate,用于管理流程的Unity插件,我們可以通過簡(jiǎn)單的節(jié)點(diǎn)連接來對(duì)我們的資源進(jìn)行一系列操作的配置。除此之外,我們還可以用它來做一鍵打包等功能。
- Defaulter – Custom Importer Defaults ,用于管理資源的導(dǎo)入統(tǒng)一化,通過路徑來決定其中資源的格式,例如:貼圖對(duì)應(yīng)著的MaxSize、Format、Read&Write等等,還支持其他很多的資源,通過這個(gè)我們不再需要對(duì)每一個(gè)導(dǎo)入的資源進(jìn)行手動(dòng)的設(shè)置了,由于其開源的有點(diǎn),我們也可以根據(jù)我們自己的需要進(jìn)行優(yōu)化。由于作者不再維護(hù)了,所以我們或許需要自己來進(jìn)行編寫。
- 熟悉一些簡(jiǎn)單的程序腳本,例如maxscript或者是ps中的ExtendScript ToolkitCS6。我就曾經(jīng)自己寫過一個(gè)自動(dòng)切圖的小插件,不過效率不是很行,但是語言本身并不難學(xué),能給美術(shù)和程序自己帶來很多方便,通過C#對(duì)命令行調(diào)用的方式集成到Unity中,相信整個(gè)工作會(huì)輕松不少。
- 利用Jenkins或者Hudson進(jìn)行持續(xù)集成。人肉集成是對(duì)人力與資源的一種浪費(fèi),極其容易出現(xiàn)錯(cuò)誤,而本地打包則大大占用了程序員的本地帶寬,讓程序員無法繼續(xù)進(jìn)行工。而通過配置Jenkins來自動(dòng)實(shí)現(xiàn)可參數(shù)化的、穩(wěn)健的、可持續(xù)的集成,項(xiàng)目組可以集中更多的力量來進(jìn)行產(chǎn)品的迭代。
- 使用Spine或者Unity Anima2D來制作2D動(dòng)畫,可以事倍功半。
- 當(dāng)然最靠譜的還是自己用C#來寫工具,雖然會(huì)花一些時(shí)間,但是磨刀不誤砍柴工,花一天時(shí)間寫工具,今后的幾個(gè)月當(dāng)中可能會(huì)減少你好幾天的工作量。好工具所帶來的生產(chǎn)力可能大大超出你的想象。
資源的工作流除了用于生成資源,對(duì)于資源的管理也是非常的重要。
5.3-Unity相機(jī)優(yōu)化
5.3.1-Occlusion Culling遮擋剔除
unity 中的剔除包括兩種,一種是視角剔除,凡是不在攝像機(jī)視野內(nèi)的物體,不進(jìn)行渲染,第二種就是遮擋剔除,被擋住的物體不被渲染,即使它在相機(jī)視野內(nèi),兩種方法可以共存
不進(jìn)行視角剔除,也不進(jìn)行遮擋剔除
只進(jìn)行視角剔除
二者都進(jìn)行
Occlusion Culling使用兩種存儲(chǔ)方式, 一個(gè)為 View Cells (靜態(tài)物體) 另一種為Target Cells (移動(dòng)的物體).二者的數(shù)據(jù)分別單獨(dú)存儲(chǔ)
較大的物體不易當(dāng)被遮擋物,較小的物體不易當(dāng)遮擋物
可以通過‘overdraw’來觀看場(chǎng)景中的遮擋情況,注意該模式下視野就代表攝像機(jī),
注意:黃色的區(qū)域?yàn)檎趽跆蕹齾^(qū)域,只有在這個(gè)區(qū)域內(nèi),遮擋剔除效果才會(huì)顯現(xiàn)
一.Occlusion Culling Window
在 Window > Rendering打開Occlusion Culling Window
Object菜單欄:scene Filter:文件過濾器,all 就是顯示場(chǎng)景中所有的物體,renderers 顯示場(chǎng)景中帶有render組建的物體,occlusion Areas 顯示場(chǎng)景中帶有occlusion Area組件的物體
選中一個(gè)物體后,也可以在這個(gè)面板設(shè)置它的Occluder Static和Oculudee Static
NOTE: Whenever your camera is outside occlusion areas, occlusion culling will not be applied. It is important to set up your Occlusion Areas to cover the places where the camera can potentially be, but making the areas too large incurs a cost during baking.
當(dāng)你的相機(jī)在遮擋區(qū)域之外時(shí),遮擋剔除將不被應(yīng)用。設(shè)置遮擋區(qū)域以覆蓋相機(jī)可能出現(xiàn)的位置是很重要的,但是在烘焙過程中,使遮擋區(qū)域過大會(huì)帶來成本。
當(dāng)使用l (LOD) 時(shí). (LOD0) 作為 Occluder.也就是說如果當(dāng)前時(shí)Lod1則不會(huì)發(fā)生遮擋
Smallest Occluder:遮擋物的最小體積,小于這個(gè)數(shù)則不會(huì)遮擋其他物體
Smallest Hole:有的墻上有縫隙,你可以通過這個(gè)縫隙看到后面的物體,這個(gè)值是縫隙的直徑
Backface Threshold:背面閾值,有些背面你是不可以到達(dá)的,比如說地形的下面,一個(gè)封閉房間的外面,這些地方你始終看不了,但是烘焙之后這些數(shù)據(jù)是包含在unity里面的,100代表不剔除這些數(shù)據(jù),值越小數(shù)據(jù)就會(huì)越小,只是剔除背面數(shù)據(jù)
Clear:清除烘焙數(shù)據(jù) Bake:烘焙數(shù)據(jù)
二.Occlusion Area
Occlusion Area 遮擋區(qū)域:應(yīng)用于遮擋剔除移動(dòng)的物體. (移動(dòng)的物體不能設(shè)置為 static). 你可以在一個(gè)空物體上面添加組件 Occlusion Area (Component -> Rendering -> Occlusion Area ).
添加Occlusion Area組件之后, 點(diǎn)擊 Is View Volume 檢測(cè)遮擋移動(dòng)的物體
Size:遮擋區(qū)域的大小
Center:遮擋區(qū)域的位置
Is View Volume:是否應(yīng)用遮擋剔除移動(dòng)的物體
遮擋區(qū)域可以有多個(gè)
在該區(qū)域內(nèi)的物體,可以移動(dòng),不用設(shè)置靜態(tài),只要被遮擋就不會(huì)渲染,不被遮擋就渲染,事先也得烘焙
5.3.2-Frustum Culling視錐體剔除
一.應(yīng)用背景
在現(xiàn)代游戲中,游戲資源越來越多,游戲場(chǎng)景也越來越大越來越復(fù)雜,雖說硬件設(shè)備更新迭代很快,性能也日漸強(qiáng)大,但這還遠(yuǎn)不能緩解復(fù)雜繁多的資源帶來的性能壓力,因而性能優(yōu)化仍然很有必要。場(chǎng)景資源的剔除是性能優(yōu)化的一個(gè)重要方面,剔除方式也有很多,比如OcclusionCulling、Frustum Culling、layerCullingDistance等。由于項(xiàng)目的需要,這里重點(diǎn)關(guān)注Frustum Culling(視錐體剔除)。
視錐體剔除的基本思想:判斷對(duì)象是否在相機(jī)視錐體內(nèi)(相交也算),在則不剔除,不在則剔除。判斷的方法也有很多,比較常見的方法是判斷對(duì)象的BoundingBox與相機(jī)視錐體的六個(gè)剪裁平面的關(guān)系,來判斷對(duì)象是否在視錐體中。為此Unity也提供了原生API以支持基于視錐體的剔除方案。
二.問題要點(diǎn)
這里基于Unity提供的原生API來探討基于視錐體的剔除流程,需要使用到GeometryUtility中提供的API。
1.獲取相機(jī)的剪裁平面:
有多個(gè)API可獲得剪裁平面:
①public static Plane[] CalculateFrustumPlanes(Camera camera);
② public static Plane[] CalculateFrustumPlanes(Matrix4x4 worldToProjectionMatrix);
③ public static void CalculateFrustumPlanes(Camera camera, Plane[] planes);
④ public static void CalculateFrustumPlanes(Matrix4x4 worldToProjectionMatrix, Plane[] planes);
前三個(gè)API最終都是調(diào)用了④來實(shí)現(xiàn)剪裁面獲取功能的,其中①和②由于在內(nèi)部創(chuàng)建了Plane數(shù)組,并返回,因此存在GC,而③和④需要預(yù)先定義一個(gè)長(zhǎng)度為6的Plane數(shù)組,并傳入方法,方法內(nèi)部會(huì)修改這些對(duì)象的值,因而不存在GC。所以建議使用③或者④。
通過上述API獲取的剪裁平面的順序依次是:左、右、下、上、近、遠(yuǎn)。
2.傳入需要檢測(cè)對(duì)象的BondingBox:
public static bool TestPlanesAABB(Plane[] planes, Bounds bounds);
調(diào)用上述API,傳入通過①獲取的剪裁平面及對(duì)象的BoundingBox即可檢測(cè)出該對(duì)象是否在視錐體內(nèi)。
三.Demo源碼
using System.Collections.Generic; using UnityEditor; using UnityEngine; using System.Linq;public class FrustumTest : MonoBehaviour {public Camera CulingCamera;public Renderer[] CullingTestObjects;private Plane[] planes;void OnEnable(){planes = new Plane[6];}void Update(){GeometryUtility.CalculateFrustumPlanes(CulingCamera, planes);for (var index = 0; index < CullingTestObjects.Length; index++){var bounds = CullingTestObjects[index].bounds;var result = GeometryUtility.TestPlanesAABB(planes, bounds);CullingTestObjects[index].enabled = result;}}[MenuItem("Test/Create")]static void Create(){var gos = new List<GameObject>();var root = new GameObject("Root").transform;for (var i = 0; i < 10; i++){for (var j = 0; j < 10; j++){for (var k = 0; k < 10; k++){var go = GameObject.CreatePrimitive(PrimitiveType.Cube);go.transform.position = new Vector3(i, j, k) * 2;go.transform.parent = root;gos.Add(go);}}}var test = new GameObject("FrustumTest").AddComponent<FrustumTest>();test.CulingCamera = Camera.main;test.CullingTestObjects = gos.Select(item => item.GetComponent<Renderer>()).ToArray();} }四.實(shí)驗(yàn)效果
五.存在的問題:
通過上述API獲取剪裁面時(shí),只能一次性獲所有的剪裁面,而在一些特殊情況下我們往往只需要部分剪裁面即可。同時(shí)上述API底層采用了P/Invoke方式調(diào)用了非托管C++庫來實(shí)現(xiàn)剪裁面的計(jì)算,頻繁調(diào)用會(huì)有一定的性能損耗。為了實(shí)現(xiàn)更加個(gè)性化的基于視錐體的裁剪方案,我們往往需要自行計(jì)算剪裁面,并進(jìn)行包含檢測(cè)。
5.4-UnityUGUI優(yōu)化
1.UGUI中若不需要使用到RaycastTarget的,務(wù)必將其關(guān)閉,一般UI里也就是按鈕才需要接收響應(yīng)事件,那么大部分image和text是是不需要開RaycastTarget的。
但是問題就來了,Unity默認(rèn)在hierarchy窗口Create->UI->Image 、Text的時(shí)候就會(huì)自動(dòng)幫我們勾選上RaycastTarget, 一個(gè)復(fù)雜點(diǎn)的界面至少也300+個(gè)Image和Text, 總不能一個(gè)個(gè)取消吧。 所以我們可以重寫Create->UI->Image的事件。
[MenuItem("GameObject/UI/Image")]static void CreatImage(){if(Selection.activeTransform){if(Selection.activeTransform.GetComponentInParent<Canvas>()){GameObject go = new GameObject("image",typeof(Image));go.GetComponent<Image>().raycastTarget = false;go.transform.SetParent(Selection.activeTransform);}}}2.禁用空的Image。在Unity項(xiàng)目中,有時(shí)候會(huì)用空的Image并將alpha設(shè)置為0來接收點(diǎn)擊事件。這樣會(huì)產(chǎn)生不必要的overdraw,增加性能負(fù)擔(dān),可以使用以下腳本,只監(jiān)聽事件,不畫網(wǎng)格,從而減少overdraw。
using UnityEngine.UI;public class EmptyRaycast : Graphic {public override void SetMaterialDirty(){}public override void SetVerticesDirty(){}protected override void OnPopulateMesh(VertexHelper vh){vh.Clear();} }按如下使用方式,可以實(shí)現(xiàn)點(diǎn)擊事件的監(jiān)聽,并且不產(chǎn)生overdraw。
而如果是使用Image的話,則會(huì)產(chǎn)生overdraw。
3.場(chǎng)景中應(yīng)盡量保持少許的Canvas,新增一個(gè)Canvas,都會(huì)增加一個(gè)Batches.
4.UI 上的的Color屬性 不建議直接修改,修改會(huì)導(dǎo)致重繪mesh,建議修改材質(zhì)球顏色不會(huì)出現(xiàn)這個(gè)問題
5.在Loading界面使用動(dòng)態(tài)打圖集技術(shù) 可以很大的優(yōu)化DC。但是動(dòng)態(tài)打圖集很增加Loading時(shí)間。
6.設(shè)置UI點(diǎn)擊其他區(qū)域退出當(dāng)前UI這樣的類似功能 會(huì)產(chǎn)生大量Overdraw,最好的方法寫個(gè)代碼 繼承Image 重新 OnPopulateMesh 方法,只寫一行 tofill.clear(); 就搞定了
7.Sprite導(dǎo)入 UI一般關(guān)閉 Read/Write 和 Generate MipMaps
8.GameObject.SetActive 盡量少使用。因?yàn)樵贠nEnable 和OnDisenable 都會(huì)重新設(shè)置一遍所有的臟標(biāo)志. 尤其不要對(duì)大量的 text 進(jìn)行 setactive。單個(gè)UI建議使用 canvas renderer的.cull屬性。如果多個(gè)UI建議使用canvas group中的alpha 設(shè)置0。
9.避免使用Camera.main
當(dāng)設(shè)置畫布進(jìn)行渲染時(shí),不管該畫布是在世界空間還是攝像機(jī)的屏幕空間,都可以指定用于為UI中Graphic Raycaster生成交互事件的攝像機(jī)。渲染模式為“Screen Space - Camera”的畫布需要使用該設(shè)置,該設(shè)置名為“Render Camera”。
然而在渲染模式為“World Space”的畫布上,該設(shè)置是可選的,名為“Event Camera”。
如果將世界空間畫布的Event Camera字段留空,這不意味著該畫布不會(huì)接收事件。它會(huì)使用游戲的主攝像機(jī)。為了確定哪個(gè)攝像機(jī)是主攝像機(jī),該畫布會(huì)訪問Camera.main屬性。
根據(jù)Unity所使用的代碼路徑,每幀中每有一個(gè)Graphic Raycaster和世界空間畫布,該畫布會(huì)訪問7到10次Camera.main。每次訪問Camera.main都會(huì)調(diào)用Object.FindObjectWithTag。這個(gè)做法在運(yùn)行時(shí)并不合適。
解決方案:避免使用Camera.main
緩存攝像機(jī)的引用,然后創(chuàng)建系統(tǒng)來跟蹤主攝像機(jī)。如果使用世界空間畫布,要指定Event Camera,不要將該屬性留空。如果需要修改Event Camera,編寫代碼來更新Event Camera屬性。
10.避免使用布局分組
問題:每個(gè)影響布局的UI元素都會(huì)至少執(zhí)行一次GetComponents調(diào)用。
當(dāng)修改布局系統(tǒng)的一個(gè)或多個(gè)子元素時(shí),會(huì)使布局變臟。修改后的子元素會(huì)使擁有該元素的布局系統(tǒng)(Layout System)無效化。
簡(jiǎn)單介紹一下布局系統(tǒng):布局系統(tǒng)是一組連續(xù)的布局分組(Layout Group),它們?cè)诓季衷?#xff08;Layout Element)之上。布局元素不只是名為L(zhǎng)ayout Element的組件,它們還包括UI圖像、文字和Scroll Rect組件,而且Scroll Rect同時(shí)也是布局分組。
回到問題本身,每個(gè)使布局變臟的UI元素都會(huì)至少執(zhí)行一次GetComponents調(diào)用,該調(diào)用會(huì)在布局元素父對(duì)象上尋找有效的布局分組。找到有效布局分組后,它會(huì)繼續(xù)遍歷Transform層級(jí),直到停止尋找分組或是到達(dá)層級(jí)的根部分,無論先滿足哪個(gè)條件都會(huì)停止尋找過程。因此。每個(gè)布局分組會(huì)給每個(gè)子布局元素的改變過程添加一次GetComponents調(diào)用,使嵌套布局分組的性能變差。
解決方案:避免使用布局分組。
使用錨點(diǎn)進(jìn)行比例布局。在擁有動(dòng)態(tài)元素?cái)?shù)量的活躍UI上,考慮編寫代碼來計(jì)算布局,僅在需要時(shí)運(yùn)行該代碼,而不是每次發(fā)生改變的時(shí)候。
11.Other
動(dòng)態(tài)打圖集算法
Github: unityRuntimeSpriteSheetsGenerator
圖片壓縮格式選擇
- 安卓主要用RGB ETC 4bit 不支持A通道,如果透明 用ETC2 8bit 或者 再用一張ETC 1通道
- Crunched 是untiy的二次壓縮方式,缺點(diǎn)就是壓縮時(shí)間過久
- IOS主要用 RGBAPVRTC4 其次選用 SATC
- 大部分壓縮格式要求 寬高是2的整數(shù)次冪(POT) 或4的整數(shù)倍數(shù) .如果不能滿足,就單獨(dú)打圖集
- 安卓 RGB ETC4通道分離 split alpha channel。要注意在Project setting中g(shù)raphics shader 中添加 DefaultETC1。否則在真機(jī)可能出錯(cuò) ?;蛑苯佑肊TC2(優(yōu)先選擇)
打圖集的規(guī)則
- 盡量把同一個(gè)界面放在一個(gè)圖集
- 盡量不要把同一個(gè)圖放在多個(gè)公用圖集
- 使用頻率高的圖 放在一個(gè)圖集
- 大圖UI盡量不打進(jìn)圖集
- 建議使用插件打包圖集,效果會(huì)略微比Unity自帶的圖集打包好一點(diǎn)。節(jié)省空間很大一點(diǎn)
UI 合批流程
- 遍歷UI
- 合批測(cè)試(當(dāng)前UI會(huì)判斷底下UI是否可以合批 如果不合批 深度底下最大的+1)
- 判斷順序 如果深度一樣 然后判斷材質(zhì)是否一樣 再判斷 圖片是否一樣 最后再根據(jù)面板順序排序,如果深度為-1 則不渲染
- 得出排序數(shù)組 看相鄰元素是否一樣 然后合批
會(huì)打斷合批的操作
父物體 的 Pos 改變z軸 , 旋轉(zhuǎn)改變 x y 軸,會(huì)退出批處理 會(huì)打斷上下合批(UI和其他UI不在同一平面就不能合批)
UI 網(wǎng)格重建流程
某ui改變----將此UI設(shè)置臟標(biāo)志—添加臟標(biāo)志隊(duì)列–下一幀重建mesh
5.5-C# 語言層面的優(yōu)化
主要思想是減少存的動(dòng)態(tài)分配和釋放,以及內(nèi)存泄露,減少垃圾回收。
1:
(1)字符串的處理 使用stringbuild 類代替string 進(jìn)行多個(gè)字符串的拼接。
(2)String.format();
避免使用“+”“aaa”+“bbb”這種方式 原因是“+”對(duì)字符串進(jìn)行拼接,會(huì)導(dǎo)致臨時(shí)
堆string 對(duì)象發(fā)生頻繁的堆對(duì)象的分配和釋放。
2.盡可能使用for循環(huán)代替foreach
數(shù)組 泛型list<> 一律使用for ,字典使用foreach
原因: 每次foreach產(chǎn)生一個(gè)臨時(shí)的迭代器對(duì)象,迭代器會(huì)額外的分配內(nèi)存。
3:頻繁調(diào)用的函數(shù)中,如果有臨時(shí)變量是引用類型,要將其改為成員變量。避免頻繁的堆對(duì)象的創(chuàng)建和釋放。
4:如果函數(shù)運(yùn)行需要一個(gè)List 需要設(shè)置List為成員變量 獨(dú)立與函數(shù)的運(yùn)行,可以
通過clear()函數(shù)清空,重復(fù)使用全局成員集合,清空函數(shù)不會(huì)刪除內(nèi)存空間,大大的減輕性能的負(fù)擔(dān)。
5:避免使用Lambda表達(dá)式,存在內(nèi)存泄露的內(nèi)存隱患。
6:盡量使用對(duì)象池 避免對(duì)象的頻繁創(chuàng)建和釋放,導(dǎo)致大量無效內(nèi)存的積累,引發(fā)GC(垃圾回收)操作,造成性能的降低
7:避免裝箱和拆箱的操作,會(huì)導(dǎo)致堆內(nèi)存的分配。
8:避免使用AraayList會(huì)把所有插入的數(shù)據(jù)當(dāng)作object類處理,存在一個(gè)裝箱的過程。盡量使用泛型List<>;
9:避免使用反射機(jī)制,運(yùn)行時(shí)獲取類型信息,性能低下。
10:即時(shí)的將對(duì)象設(shè)置為null、 委托事件即時(shí)注銷。
11.別把成員變量聲明為 public 或 protected。都聲明為 private 而使用 public/protected 的屬性
12.不在代碼中使用具體的路徑和驅(qū)動(dòng)器名。 使用相對(duì)路徑,并使路徑可編程。
13.避免不必要的調(diào)用 ToUpper 或 ToLower 方法
String是不變類,調(diào)用ToUpper或ToLower方法都會(huì)導(dǎo)致創(chuàng)建一個(gè)新的字符串。如果被頻繁調(diào)用,將導(dǎo)致頻繁創(chuàng)建字符串對(duì)象。這違背了“避免頻繁創(chuàng)建對(duì)象”這一基本原則。
例如,bool.Parse方法本身已經(jīng)是忽略大小寫的,調(diào)用時(shí)不要調(diào)用ToLower方法。
另一個(gè)非常普遍的場(chǎng)景是字符串比較。高效的做法是使用 Compare 方法,這個(gè)方法可以做大小寫忽略的比較,并且不會(huì)創(chuàng)建新字符串。
還有一種情況是使用 HashTable 的時(shí)候,有時(shí)候無法保證傳遞 key 的大小寫是否符合預(yù)期,往往會(huì)把 key 強(qiáng)制轉(zhuǎn)換到大寫或小寫方法。實(shí)際上 HashTable 有不同的構(gòu)造形式,完全支持采用忽略大小寫的 key: new HashTable(StringComparer.OrdinalIgnoreCase)。
14…絕對(duì)不要在循環(huán)中使用try-Catch.
15.利用using和try/finally語句來清理資源
16.盡量少用模運(yùn)算和除法運(yùn)算,比如a/5f,一定要寫成a*0.2f。
17.不要濫用靜態(tài)對(duì)象:由于靜態(tài)對(duì)象始終存在于內(nèi)存當(dāng)中,過度濫用的話容易出現(xiàn)占用內(nèi)存過多的情況。當(dāng)不再需要的時(shí)候,將靜態(tài)大對(duì)象置空,保證GC能夠正常進(jìn)行。
18.使用尾遞歸而非其他的遞歸,尾遞歸的性能好于頭遞歸。
5.6-圖形渲染層面
1.不要使用實(shí)時(shí)陰影,使用陰影紋理或者簡(jiǎn)單的圖片模擬陰影。
2.減少頂點(diǎn),減少三角面數(shù),峰值小于10w面/每幀,使用LOD進(jìn)行多層次模型設(shè)置,進(jìn)行遮擋。
3.減少Drawcall 峰值<200 對(duì)場(chǎng)景物件 比如說:石頭,樹木等使用相同材質(zhì)相同貼圖的物體進(jìn)行批處理,降低DC。人物模型(人物模型一般都是帶骨骼的)SkinMesh。即便使用相同的材質(zhì)貼圖,DC也不能進(jìn)行批處理。使用Lightmap技術(shù)降低DC。 UI圖素進(jìn)行合并(打包圖集)降低DC。
4.盡量使用簡(jiǎn)單材質(zhì),適合移動(dòng)平臺(tái)(手機(jī))Mobile這種Shader或者使用自定義一些優(yōu)化過的Shader,高級(jí)Shader嚴(yán)重降低幀率,加大能耗,或者不可用。
5.在紋理和質(zhì)量之間做平衡,在保證質(zhì)量不受太大影響的情況下,盡量使用小尺寸的紋理,提升效率。減少安裝包的大小,九宮格等
6.減少骨骼數(shù)量的使用15個(gè)上下左右,減少粒子系統(tǒng),粒子的數(shù)量。盡量減少粒子數(shù)量。
7.共享材質(zhì) 多一個(gè)材質(zhì)DC就會(huì)上升 使用共享材質(zhì) 將多張貼圖畫在一張較大的貼圖上面,達(dá)到共享材質(zhì)的作用。
8.減少,水面折射效果,或者霧效果。
9.嘗試使用遮擋剔除Occlusion Culling進(jìn)行渲染上面優(yōu)化。
10.使用LOD技術(shù),在LOD2和LOD1可以修改貼圖的品質(zhì),Cast Shadow和 ReceiveShadow可以移除
11.能不使用實(shí)時(shí)光渲染則盡量不要使用實(shí)時(shí)光渲染,使用光照貼圖(LightMap)替代
12.將場(chǎng)景中的靜態(tài)物體進(jìn)行合并Mesh處理
13.場(chǎng)景中如果沒有使用燈光和像素?zé)?#xff0c;就不要使用法線貼圖,因?yàn)榉ň€效果只有在有光源(Direct Light/Point Light/Angle Light/Pixel Light)的情況下才有效果。
14.如果硬陰影可以解決問題就不要用軟陰影,并且使用不影響效果的低分辨率陰影;
6-Unity3D性能優(yōu)化——工具篇
6.1-游戲性能簡(jiǎn)述
提起游戲性能,首先要提到的就是,不僅開發(fā)人員,所有游戲玩家都應(yīng)該會(huì)接觸到的一個(gè)名詞:幀率(Frame rate)。
幀率是衡量游戲性能的基本指標(biāo)。在游戲中,“一幀”便是繪制到屏幕上的一個(gè)靜止畫面。繪制一幀到屏幕上也叫做渲染一幀。每秒的幀數(shù)(fps)或者說幀率表示GPU處理時(shí)每秒鐘能夠更新的次數(shù)。高的幀率可以得到更流暢、更逼真的動(dòng)畫。
現(xiàn)階段大多數(shù)游戲的理想幀率是60FPS,其帶來的交互感和真實(shí)感會(huì)更加強(qiáng)烈。通常來說,幀率在30FPS以上都是可以接受的,特別是對(duì)于不需要快速反應(yīng)互動(dòng)的游戲,例如休閑、解密、冒險(xiǎn)類游戲等。有些項(xiàng)目有特殊的需求,比如VR游戲,至少需要90FPS。當(dāng)幀率降低到30FPS以下時(shí),玩家通常會(huì)有不好的體驗(yàn)。
而現(xiàn)階段隨著支持144HZ刷新率的硬件設(shè)備的涌現(xiàn),能否維持對(duì)應(yīng)高幀率又是一項(xiàng)新的指標(biāo),尤其是在電競(jìng)領(lǐng)域
但在游戲中重要的不僅僅幀率的速度,幀率同時(shí)也必須非常穩(wěn)定。玩家通常對(duì)幀率的變化比較敏感,不穩(wěn)定的幀率通常會(huì)比低一些但是很穩(wěn)定的幀率表現(xiàn)更差。
雖然幀率是一個(gè)我們談?wù)撚螒蛐阅艿幕緲?biāo)準(zhǔn),但是當(dāng)我們提升游戲性能時(shí),更因該想到的是渲染一幀需要多少毫秒。幀率的相對(duì)改變?cè)诓煌秶鷷?huì)有不同的變化。比如,從60到50FPS呈現(xiàn)出的是額外3.3毫秒的運(yùn)行時(shí)間,但是從30到20FPS呈現(xiàn)出的是額外的16.6毫秒的運(yùn)行時(shí)間。在這里,同樣降低了10FPS,但是渲染一幀上時(shí)間的差別是很顯著的。
我們還需要了解渲染一幀需要多少毫秒才能滿足當(dāng)前幀率。通過公式 1000/(想要達(dá)到的幀率)。通過這個(gè)公式可以得到,30FPS必須在33.3毫秒之內(nèi)渲染完一幀,60FPS必須在16.6毫秒內(nèi)渲染完一幀。
渲染一幀,Unity需要執(zhí)行很多任務(wù)。比如,Unity需要更新游戲的狀態(tài)。有一些任務(wù)在每一幀都需要執(zhí)行,包括執(zhí)行腳本,運(yùn)行光照計(jì)算等。除此之外,有許多操作是在一幀執(zhí)行多次的,例如物理運(yùn)算。當(dāng)所有這些任務(wù)都執(zhí)行的足夠快時(shí),我們的游戲才會(huì)有穩(wěn)定且理想的幀率。當(dāng)這些任務(wù)執(zhí)行不滿足需求時(shí),渲染一幀將花費(fèi)更多的時(shí)間,并且?guī)蕰?huì)因此下降。
知道哪些任務(wù)花費(fèi)了過多的時(shí)間,是游戲性能問題的關(guān)鍵。一旦我們知道了哪些任務(wù)降低了幀率,便可以嘗試優(yōu)化游戲的這一部分。這就是為什么性能分析工具是游戲優(yōu)化的重點(diǎn)之一。
6.2-Unity3d性能分析工具
工欲善其事必先利其器,這里我們來講解Unity3D優(yōu)化所需的工具
如果游戲存在性能問題,游戲運(yùn)行就會(huì)出現(xiàn)緩慢、卡頓、掉幀甚至直接閃退等現(xiàn)象。在我們嘗試解決問題前,需要先知道其問題的起因,而嘗試不同的解決方案。若僅靠猜測(cè)或者依據(jù)自身原有的經(jīng)驗(yàn)去解決問題,那我們可能會(huì)做無用功,甚至引申出更復(fù)雜的問題。
在這些時(shí)候我們就需要用到性能分析工具,性能分析工具主要測(cè)試游戲運(yùn)行時(shí)各個(gè)方面性能,如CPU、GPU、內(nèi)存等。通過性能分析工具,我們能夠透過游戲運(yùn)行的外在表現(xiàn),獲取內(nèi)在信息,而這些信息便是我們鎖定引起性能問題的關(guān)鍵所在。
在我們進(jìn)行Unity性能優(yōu)化的過程中,最主要用的到性能分析工具包括,Unity自帶的Unity Profile,IOS端的XCode ,以及一些第三方插件,如騰訊推出的UPA性能分析工具。
我們主要針對(duì)Unity Profile進(jìn)行講解,之后也會(huì)略微介紹另外一些性能分析工具。
6.2.1-Unity Profile
Unity Profile是Unity中最常用的官方性能分析工具,在使用Unity開發(fā)游戲的過程中,借助Profiler來分析CPU、GPU及內(nèi)存使用狀況是至關(guān)重要的。
首先我們來了解Unity Profile的面板:
我們通過Window——>Profiler來激活Unity Profile面板
在下圖中我們可以看到Unity Profile面板,其中有很多profilers,每個(gè)profiler顯示我們當(dāng)前項(xiàng)目一個(gè)方面的信息,如CPU、GPU、渲染(Rendering)、內(nèi)存(Memory)、聲音(Audio)、視屏(Video)、物理(Physics)、ui及全局光照(global illumination)。
當(dāng)項(xiàng)目運(yùn)行時(shí),每個(gè)profilers會(huì)隨著運(yùn)行時(shí)間來顯示數(shù)據(jù),有些性能問題是持續(xù)性的,有些僅在某一幀中出現(xiàn),還有些性能問題可能會(huì)隨時(shí)間推移而逐漸顯出出來。
在面板的下半部分顯示了我們選中的profilers當(dāng)前幀的詳細(xì)內(nèi)容,我們可以通過選擇列標(biāo)題,通過這一列的信息值來排序。
在CPU usage profiler中的列表題分別為:
Total:當(dāng)前任務(wù)的時(shí)間消耗占當(dāng)前幀cpu消耗的時(shí)間比例。
Self:任務(wù)自身時(shí)間消耗占當(dāng)前幀cpu消耗的時(shí)間比例。
Calls:當(dāng)前任務(wù)在當(dāng)前幀內(nèi)被調(diào)用的次數(shù)。
GC Alloc:當(dāng)前任務(wù)在當(dāng)前幀內(nèi)進(jìn)行過內(nèi)存回收和分配的次數(shù)。
Time ms:當(dāng)前任務(wù)在當(dāng)前幀內(nèi)的耗時(shí)總時(shí)間。
Self ms:當(dāng)前任務(wù)自身(不包含內(nèi)部的子任務(wù))時(shí)間消耗。
當(dāng)我們?cè)趯蛹?jí)視圖中點(diǎn)擊函數(shù)名字時(shí),CPU usage profiler將在Profiler窗口上部的圖形視圖中高亮顯示這個(gè)函數(shù)的信息。比如我們選中Cameta.Render,Rendering的信息就會(huì)被高亮顯示出來。
我們可以Profiler的左下的下拉菜單中選擇Timeline。
Timeline顯示了兩件事:cpu任務(wù)的執(zhí)行順序和哪個(gè)線程負(fù)責(zé)什么任務(wù)。線程允許不同的任務(wù)同時(shí)執(zhí)行。當(dāng)一個(gè)線程執(zhí)行一個(gè)任務(wù)時(shí),另外的線程可以執(zhí)行另一個(gè)完全不同的任務(wù)。和Unity的渲染過程相關(guān)的線程有三種:主線程,渲染線程和worker threads。了解哪個(gè)線程負(fù)責(zé)哪些任務(wù)的用處非常之大,一旦我們知道了在哪個(gè)線程上的任務(wù)執(zhí)行的速率最低,那么我們就應(yīng)該集中優(yōu)化在那個(gè)線程上的操作。
以上所顯示的數(shù)據(jù)依賴于我們當(dāng)前選擇的profiler。例如,當(dāng)選中內(nèi)存時(shí),這個(gè)區(qū)域顯示例如游戲資源使用的內(nèi)存和總內(nèi)存占用等。如果選中渲染profiler,這里會(huì)顯示被渲染的對(duì)象數(shù)量或者渲染操作執(zhí)行次數(shù)等數(shù)據(jù)。
這些profiler會(huì)提供很多詳細(xì)信息,但是我們并不總需要使用所有的profiler。實(shí)際上,我們?cè)诜治鲇螒蛐阅軙r(shí)通常只是觀察一個(gè)或者兩個(gè)profiler,而不需要觀察的我們可以通過右上角的”X”關(guān)閉,如果需要在添加回來,可以通過左上角Add Profiler。
例如,當(dāng)我們的游戲運(yùn)行的比較慢時(shí),我們可能一開始先查看CPU usage profiler,CPU usage profiler也是在我們進(jìn)行優(yōu)化分析時(shí)最常用的Profiler。
當(dāng)然,除了CPU usage profiler,Unity Profiler中其他的Profiler在一些場(chǎng)合也非常的有用,比如GPU、內(nèi)存、渲染等,其使用方法和CPU usage profiler也是大同小異,可以按照以上的步驟來查看并學(xué)習(xí)。
我們?cè)谟^察數(shù)據(jù)時(shí),需要觀察的目標(biāo)有如下幾點(diǎn):
CPU:
- GC Allow: 任何一次性內(nèi)存分配大于2KB的選項(xiàng)。
每幀都具有20B以上內(nèi)存分配的選項(xiàng) 。
GC相關(guān)的問題和優(yōu)化,在之后我們會(huì)詳細(xì)的介紹。
-
Time ms:
注意占用5ms以上的選項(xiàng)
內(nèi)存 :
- Texture: 檢查是否有重復(fù)資源和超大內(nèi)存是否需要壓縮等.
- AnimationClip: 重點(diǎn)檢查是否有重復(fù)資源.。
- Mesh: 重點(diǎn)檢查是否有重復(fù)資源。
實(shí)際項(xiàng)目中的優(yōu)化建議
在了解了Unity Profiler后,現(xiàn)在我們?cè)谝粋€(gè)實(shí)際項(xiàng)目中來進(jìn)行一次性能分析。同時(shí)來了解一般在實(shí)際項(xiàng)目中,主要會(huì)引起也是我們主要去觀察的性能問題出現(xiàn)在什么地方。
以下是我做的一個(gè)簡(jiǎn)單的游戲項(xiàng)目,并未做任何性能優(yōu)化并且有大量引起性能問題的代碼,可以更方便大家觀察其性能問題。
我們來看一下在CPU usage profiler面板中的可觀察項(xiàng),在項(xiàng)目中我們可以先關(guān)閉VSync垂直同步來提高幀率。
下圖中我關(guān)閉了除VSync之外的顯示,可以看到VSync的消耗
具體步驟是edit->project settings->Quality,在Inspector面板中,V Sync count選擇don’t Sync.
我們來簡(jiǎn)單的介紹一下什么是垂直同步,以及關(guān)閉它之后會(huì)發(fā)生什么。
要理解垂直同步,首先明白顯示器的工作原理。
顯示器上的所有圖像都是單個(gè)像素組成了水平掃描線,水平掃描線在垂直方向的堆積形成了完整的畫面,無論是隔行掃描還是逐行掃描,顯示器都有兩種同步參數(shù)——水平同步和垂直同步。
垂直和水平是CRT顯示器中兩個(gè)基本的同步信號(hào),水平同步信號(hào)決定了CRT畫出一條橫越屏幕線的時(shí)間,垂直同步信號(hào)決定了CRT從屏幕頂部畫到底部,再返回原始位置的時(shí)間,而垂直同步代表著CRT顯示器的刷新率水準(zhǔn)。
在游戲項(xiàng)目中,如果我們選擇等待垂直同步信號(hào)也就是打開垂直同步,在游戲中或許性能較強(qiáng)的顯卡會(huì)迅速的繪制完一屏的圖像,但是沒有垂直同步信號(hào)的到達(dá),顯卡無法繪制下一屏,只有等85單位的信號(hào)到達(dá),才可以繪制。這樣FPS自然要受到刷新率運(yùn)行值的制約。
而如果我們選擇不等待垂直同步信號(hào),那么游戲中繪制完一屏畫面,顯卡和顯示器無需等待垂直同步信號(hào)就可以開始下一屏圖像的繪制,自然可以完全發(fā)揮顯卡的實(shí)力。
但是,正是因?yàn)榇怪蓖降拇嬖?#xff0c;才能使得游戲進(jìn)程和顯示器刷新率同步,使得畫面更加平滑和穩(wěn)定。取消了垂直同步信號(hào),固然可以換來更快的速度,但是在圖像的連續(xù)性上勢(shì)必會(huì)打折扣。
需要注意,LCD顯示器其實(shí)也是存在刷新率的,但其機(jī)制與CRT不同,這里不做過多贅述,但是垂直同步和水平同步對(duì)于LCD顯示器來說,一樣是有必要的。
在關(guān)閉垂直同步后,我們繼續(xù)看我們的項(xiàng)目
可以看到,我們以Total和Time ms排序,在圖中拉黑的項(xiàng)(Camera Render)始終排在最前面。
Camera Render是相機(jī)渲染工作的CPU占用量,在實(shí)際項(xiàng)目中,渲染是最常見的引起性能問題的原因。 而因?yàn)殇秩径鸬男阅軉栴}的優(yōu)化是一個(gè)非常大的工程,這方面的優(yōu)化方法在我們后續(xù)的文章中會(huì)有詳細(xì)的教程去學(xué)習(xí)和分析。在這里,我們只需要先了解。
我們這個(gè)項(xiàng)目的優(yōu)化中,無疑,渲染造成的性能損耗是一個(gè)大頭。
如果說,在我們性能分析中,渲染已經(jīng)沒有什么問題,那么我們接下來要重點(diǎn)觀察的就是GC,也就是垃圾回收性能分析。
我們按照GC Alloc的順序來顯示,可以看到下圖。
在之前我們提到過,GC Alloc中,任何一次性內(nèi)存分配大于2KB的選項(xiàng),每幀都具有20B以上內(nèi)存分配的選項(xiàng) ,是需要我們重點(diǎn)關(guān)注的,顯而易見,我們的項(xiàng)目中,對(duì)于GC的優(yōu)化,也有很大的問題。
這里我們大致介紹一下GC的機(jī)制,要想了解垃圾回收如何工作以及何時(shí)被觸發(fā),我們首先需要了解unity的內(nèi)存管理機(jī)制。Unity主要采用自動(dòng)內(nèi)存管理的機(jī)制,開發(fā)時(shí)在代碼中不需要詳細(xì)地告訴unity如何進(jìn)行內(nèi)存管理,unity內(nèi)部自身會(huì)進(jìn)行內(nèi)存管理。
Unity內(nèi)部有兩個(gè)內(nèi)存管理池,堆內(nèi)存和棧內(nèi)存,垃圾回收主要是指堆上的內(nèi)存分配和回收,unity中會(huì)定時(shí)對(duì)堆內(nèi)存進(jìn)行GC操作。
當(dāng)堆內(nèi)存上一個(gè)變量不再處于激活狀態(tài)的時(shí)候,其所占用的內(nèi)存并不會(huì)立刻被回收,不再使用的內(nèi)存只會(huì)在GC的時(shí)候才會(huì)被回收。
每次運(yùn)行GC的時(shí)候,GC會(huì)檢查堆內(nèi)存上的每個(gè)存儲(chǔ)變量,對(duì)每個(gè)變量會(huì)檢測(cè)其引用是否處于激活狀態(tài),如果變量的引用不再處于激活狀態(tài),則會(huì)被標(biāo)記為可回收,被標(biāo)記的變量會(huì)被移除,其所占有的內(nèi)存會(huì)被回收到堆內(nèi)存上。
GC操作是一個(gè)極其耗費(fèi)的操作,堆內(nèi)存上的變量或者引用越多則其運(yùn)行的操作會(huì)更多,耗費(fèi)的時(shí)間越長(zhǎng)。
如果我們也排除了GC的問題, 那么再接下來,我們就要考慮到是否是腳本的一些問題造成性能損耗。
這里的腳本,可能是我們自己寫的代碼,也有可能是我們使用的一些插件的代碼。在CPU usage profiler面板中,我們可以關(guān)注Script這一項(xiàng)。
如果在一個(gè)很慢的幀中,一大部分時(shí)間被腳本運(yùn)行所消耗,這意味著這些慢的腳本可能就是引起性能問題的主因。我們可以更加深入的分析數(shù)據(jù)來確認(rèn)。
首先我們按照Time ms來排序,然后選擇信息列表中的項(xiàng)目,如果是用戶腳本的函數(shù),那么在Profiler上方會(huì)有高亮腳本的部分。這種情況,說明游戲的性能問題是和用戶腳本相關(guān)的,如下圖中的顯示,這部分腳本性能問題一定是與我們FixedUpdate有關(guān)。
同時(shí),我們還可以再關(guān)注一些物理、ui方面的性能問題。
在上面我們討論的,是幾種最常見的性能問題,在實(shí)際項(xiàng)目?jī)?yōu)化中,如果有性能問題也逃不開這些,如果在這些方向都已經(jīng)達(dá)到了我們的要求,但我們的游戲仍然有性能問題,我們應(yīng)該遵循上面的方法解決問題:收集數(shù)據(jù)——>使用CPU usage profiler查看信息——>找到引起問題的函數(shù)。一旦我們知道了引起問題函數(shù)的名字,我們便可以針對(duì)性的,對(duì)其進(jìn)行優(yōu)化處理。
6.2.2-其他性能分析工具
在開頭我們說過,在我們進(jìn)行Unity性能優(yōu)化的過程中,最主要用的到性能分析工具包括,Unity自帶的Unity Profile,IOS端XCode Capture GPU frame以及一些第三方插件,如騰訊推出的UPA性能分析工具。
這里我們簡(jiǎn)單的介紹一下XCode和UPA.
Xcode是 Mac OS X上的集成開發(fā)工具。在我們打包Unity IOS的項(xiàng)目時(shí),必須使用到Xcode來幫助我們打包及發(fā)布。
Xcode的功能也十分的強(qiáng)大,在我們開發(fā)IOS端時(shí),可以使用其GPU frame Capture 功能為我們的項(xiàng)目進(jìn)行性能優(yōu)化分析。
在unity中,我們打包時(shí)在Run In Xcode as 選擇debug模式,并且勾選Development Build
打包完成后,使用Xcode打開文件,在Xcode中選擇Product ——> Scheme——> Manage Schemes
然后會(huì)出現(xiàn)如下界面
我們雙擊這個(gè)項(xiàng)目會(huì)出現(xiàn)如下界面
然后我們?cè)谧髠?cè)選中Run,然后在右側(cè)面板選擇Options
在GPU frame Capture中選擇OpenGL ES或者M(jìn)etal。
在Debug模式下運(yùn)行項(xiàng)目,當(dāng)項(xiàng)目在真機(jī)上完全加載后,就可以進(jìn)入Debug Navigator(View ——> Navigators ——> Show Debug Navigator)
以下是GPU frame Capture具體功能的界面,在圖形化界面中,可以在游戲運(yùn)行時(shí)清晰的了解到CPU、GPU、內(nèi)存的使用情況。
XCode的Capture GPU frame功能能高效且定量地定位到GPU中shader的消耗。
UPA是騰訊和Unity聯(lián)合打造的一款性能分析工具,據(jù)說王者榮耀的性能優(yōu)化分析就有使用到UPA,具體的使用方法可以通過客戶端性能測(cè)試【騰訊WeTest】去了解
總結(jié)
以上是生活随笔為你收集整理的浅谈Unity中的优化的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: n986原生android,【极光ROM
- 下一篇: html5虚拟摇杆,关于前端:babyl