剛開始寫這篇文章的時候選了一個很土的題目。。。《Unity3D優化全解析》。由於這是一篇臨時起意才寫的文章,並且陳述的都是既有的事實,於是給本身「文(dou)學(bi)」加工留下的餘地就少了不少。但又以爲這塊是不得不提的一個地方,平時見到不少人對此處也給予了忽略了事,須要時纔去網上扒一些隻言片語的資料。也恰逢年前,尋思着週末認真寫點東西遇到節假日沒準也沒什麼人讀,因此索性就寫了這篇臨時的文章。題目很土,由於用了指向性很明確的「Unity3D」,讓人少了遐(瞎)想的空間,同時用了「高大全」這樣的構詞法,也讓匹夫有成爲衆矢之的的可能。。。因此最後仍是改爲了如今各位看到的題目。話很少說,下面就開始正文~正所謂「草蛇灰線,伏脈千里」。那我們首先~~~~~~html
匹夫印象裏遇到的童靴,提Unity3D項目優化則必提DrawCall,這天然沒錯,但也有很很差影響。由於這會給人一個錯誤的認識:所謂的優化就是把DrawCall弄的比較低就對了。web
對優化有這種第一印象的人不在少數,drawcall的確是一個很重要的指標,但絕非所有。爲了讓各位和匹夫能達成儘量多的共識,匹夫首先介紹一下本文可能會涉及到的幾個概念,以後會提出優化所涉及的三大方面:編程
好啦,文中的幾個概念提早講清楚了,其實各位也能看的出來匹夫接下來要說的匹夫關注的優化時須要注意的方面:數組
因此,這篇文章也會按照CPU---->GPU---->內存的順序進行。緩存
上文中說了,drawcall影響的是CPU的效率,並且也是最知名的一個優化點。可是除了drawcall以外,還有哪些因素也會影響到CPU的效率呢?讓咱們一一列出暫時能想獲得的:性能優化
前面說過了,DrawCall是CPU調用底層圖形接口。好比有上千個物體,每個的渲染都須要去調用一次底層接口,而每一次的調用CPU都須要作不少工做,那麼CPU必然不堪重負。可是對於GPU來講,圖形處理的工做量是同樣的。因此對DrawCall的優化,主要就是爲了儘可能解放CPU在調用圖形接口上的開銷。因此針對drawcall咱們主要的思路就是每一個物體儘可能減小渲染次數,多個物體最好一塊兒渲染。因此,按照這個思路就有了如下幾個方案:ide
首先咱們要先理解爲什麼2個沒有使用相同材質的物體即便使用批處理,也沒法實現Draw Call數量的降低和性能上的提高。函數
由於被「批處理」的2個物體的網格模型須要使用相同材質的目的,在於其紋理是相同的,這樣才能夠實現同時渲染的目的。於是保證材質相同,是爲了保證被渲染的紋理相同。工具
所以,爲了將2個紋理不一樣的材質合二爲一,咱們就須要進行上面列出的第二步,將紋理打包成圖集。具體到合二爲一這種狀況,就是將2個紋理合成一個紋理。這樣咱們就能夠只用一個材質來代替以前的2個材質了。性能
而Draw Call Batching自己,也還會細分爲2種。
看名字,猜使用的情景。
靜態?那就是不動的咯。還有呢?額,聽上去狀態也不會改變,沒有「生命」,好比山山石石,樓房校舍啥的。那和什麼比較相似呢?嗯,聰明的各位必定以爲和場景的屬性很像吧!因此咱們的場景彷佛就能夠採用這種方式來減小draw call了。
那麼寫個定義:只要這些物體不移動,而且擁有相同的材質,靜態批處理就容許引擎對任意大小的幾何物體進行批處理操做來下降描繪調用。
那要如何使用靜態批來減小Draw Call呢?你只須要明確指出哪些物體是靜止的,而且在遊戲中永遠不會移動、旋轉和縮放。想完成這一步,你只須要在檢測器(Inspector)中將Static複選框打勾便可,以下圖所示:
至於效果如何呢?
舉個例子:新建4個物體,分別是Cube,Sphere, Capsule, Cylinder,它們有不一樣的網格模型,可是也有相同的材質(Default-Diffuse)。
首先,咱們不指定它們是static的。Draw Call的次數是4次,如圖:
咱們如今將它們4個物體都設爲static,在來運行一下:
如圖,Draw Call的次數變成了1,而Saved by batching的次數變成了3。
靜態批處理的好處不少,其中之一就是與下面要說的動態批處理相比,約束要少不少。因此通常推薦的是draw call的靜態批處理來減小draw call的次數。那麼接下來,咱們就繼續聊聊draw call的動態批處理。
有陰就有陽,有靜就有動,因此聊完了靜態批處理,確定跟着就要說說動態批處理了。首先要明確一點,Unity3D的draw call動態批處理機制是引擎自動進行的,無需像靜態批處理那樣手動設置static。咱們舉一個動態實例化prefab的例子,若是動態物體共享相同的材質,則引擎會自動對draw call優化,也就是使用批處理。首先,咱們將一個cube作成prefab,而後再實例化500次,看看draw call的數量。
for(int i = 0; i < 500; i++) { GameObject cube; cube = GameObject.Instantiate(prefab) as GameObject; }
draw call的數量:
能夠看到draw call的數量爲1,而 saved by batching的數量是499。而這個過程當中,咱們除了實例化建立物體以外什麼都沒作。不錯,unity3d引擎爲咱們自動處理了這種狀況。
可是有不少童靴也遇到這種狀況,就是我也是從prefab實例化建立的物體,爲什麼個人draw call依然很高呢?這就是匹夫上文說的,draw call的動態批處理存在着不少約束。下面匹夫就演示一下,針對cube這樣一個簡單的物體的建立,若是稍有不慎就會形成draw call飛漲的狀況吧。
咱們一樣是建立500個物體,不一樣的是其中的100個物體,每一個物體的大小都不一樣,也就是Scale不一樣。
for(int i = 0; i < 500; i++) { GameObject cube; cube = GameObject.Instantiate(prefab) as GameObject; if(i / 100 == 0) { cube.transform.localScale = new Vector3(2 + i, 2 + i, 2 + i); } }
draw call的數量:
咱們看到draw call的數量上升到了101次,而saved by batching的數量也降低到了399。各位看官能夠看到,僅僅是一個簡單的cube的建立,若是scale不一樣,居然也不會去作批處理優化。這僅僅是動態批處理機制的一種約束,那咱們總結一下動態批處理的約束,各位也許也能從中找到爲什麼動態批處理在本身的項目中不起做用的緣由:
因此,儘可能使用靜態的批處理。
曾幾什麼時候,匹夫在作一個策略類遊戲的時候須要在單元格上排兵佈陣,而要偵測到哪一個兵站在哪一個格子匹夫選擇使用了射線,因爲士兵單位不少,並且爲了精確每一幀都會執行檢測,那時候CPU的負擔叫一個慘不忍睹。後來匹夫果斷放棄了這種作法,而且對物理組件產生了心理的陰影。
這裏匹夫只提2點匹夫感受比較重要的優化措施:
1.設置一個合適的Fixed Timestep。設置的位置如圖:
那何謂「合適」呢?首先咱們要搞明白Fixed Timestep和物理組件的關係。物理組件,或者說遊戲中模擬各類物理效果的組件,最重要的是什麼呢?計算啊。對,須要經過計算才能將真實的物理效果展示在虛擬的遊戲中。那麼Fixed Timestep這貨就是和物理計算有關的啦。因此,若計算的頻率過高,天然會影響到CPU的開銷。同時,若計算頻率達不到遊戲設計時的要求,有會影響到功能的實現,因此如何抉擇須要各位具體分析,選擇一個合適的值。
2.就是不要使用網格碰撞器(mesh collider):爲啥?由於實在是太複雜了。網格碰撞器利用一個網格資源並在其上構建碰撞器。對於複雜網狀模型上的碰撞檢測,它要比應用原型碰撞器精確的多。標記爲凸起的(Convex )的網格碰撞器纔可以和其餘網格碰撞器發生碰撞。各位上網搜一下mesh collider的圖片,天然就會明白了。咱們的手機遊戲天然無需這種性價比不高的東西。
固然,從性能優化的角度考慮,物理組件能少用仍是少用爲好。
在CPU的部分聊GC,感受是否是怪怪的?其實小匹夫不這麼以爲,雖然GC是用來處理內存的,但的確增長的是CPU的開銷。所以它的確能達到釋放內存的效果,但代價更加沉重,會加劇CPU的負擔,所以對於GC的優化目標就是儘可能少的觸發GC。
首先咱們要明確所謂的GC是Mono運行時的機制,而非Unity3D遊戲引擎的機制,因此GC也主要是針對Mono的對象來講的,而它管理的也是Mono的託管堆。 搞清楚這一點,你也就明白了GC不是用來處理引擎的assets(紋理啦,音效啦等等)的內存釋放的,由於U3D引擎也有本身的內存堆而不是和Mono一塊兒使用所謂的託管堆。
其次咱們要搞清楚什麼東西會被分配到託管堆上?不錯咯,就是引用類型咯。好比類的實例,字符串,數組等等。而做爲int,float,包括結構體struct其實都是值類型,它們會被分配在堆棧上而非堆上。因此咱們關注的對象無外乎就是類實例,字符串,數組這些了。
那麼GC何時會觸發呢?兩種狀況:
因此爲了達到優化CPU的目的,咱們就不能頻繁的觸發GC。而上文也說了GC處理的是託管堆,而不是Unity3D引擎的那些資源,因此GC的優化說白了也就是代碼的優化。那麼匹夫以爲有如下幾點是須要注意的:
聊到代碼這個話題,也許有人會以爲匹夫畫蛇添足。由於代碼質量因人而異,很難像上面提到的幾點,有一個明確的評判標準。也是,公寫公有理,婆寫婆有理。可是匹夫這裏要提到的所謂代碼質量是基於一個前提的:Unity3D是用C++寫的,而咱們的代碼是用C#做爲腳原本寫的,那麼問題就來了~腳本和底層的交互開銷是否須要考慮呢?也就是說,咱們用Unity3D寫遊戲的「遊戲腳本語言」,也就是C#是由mono運行時託管的。而功能是底層引擎的C++實現的,「遊戲腳本」中的功能實現都離不開對底層代碼的調用。那麼這部分的開銷,咱們應該如何優化呢?
2.如上所述,最好不要頻繁使用GetComponent,尤爲是在循環中。
3.善於使用OnBecameVisible()和OnBecameVisible(),來控制物體的update()函數的執行以減小開銷。
4.使用內建的數組,好比用Vector3.zero而不是new Vector(0, 0, 0);
5.對於方法的參數的優化:善於使用ref關鍵字。值類型的參數,是經過將實參的值複製到形參,來實現按值傳遞到方法,也就是咱們一般說的按值傳遞。複製嘛,總會讓人感受很笨重。好比Matrix4x4這樣比較複雜的值類型,若是直接複製一份新的,反而不如將值類型的引用傳遞給方法做爲參數。
好啦,CPU的部分匹夫以爲到此就介紹的差很少了。下面就簡單聊聊其實匹夫並非十分熟悉的部分,GPU的優化。
GPU與CPU不一樣,因此側重點天然也不同。GPU的瓶頸主要存在在以下的方面:
那麼針對以上4點,其實仔細分析咱們就能夠發現,影響的GPU性能的無非就是2大方面,一方面是頂點數量過多,像素計算過於複雜。另外一方面就是GPU的顯存帶寬。那麼針鋒相對的兩方面舉措也就十分明顯了。
那麼第一個方面的優化也就是減小頂點數量,簡化複雜度,具體的舉措就總結以下了:
第二個方向呢?壓縮圖片,減少顯存帶寬的壓力。
這裏匹夫要着重介紹一下MipMap究竟是啥。由於有人說過MipMap會佔用內存呀,但爲什麼又會優化顯存帶寬呢?那就不得不從MipMap是什麼開始聊起。一張圖其實就能解決這個疑問。
上面是一個mipmap 如何儲存的例子,左邊的主圖伴有一系列逐層縮小的備份小圖
是否是很一目瞭然呢?Mipmap中每個層級的小圖都是主圖的一個特定比例的縮小細節的複製品。由於存了主圖和它的那些縮小的複製品,因此內存佔用會比以前大。可是爲什麼又優化了顯存帶寬呢?由於能夠根據實際狀況,選擇適合的小圖來渲染。因此,雖然會消耗一些內存,可是爲了圖片渲染的質量(比壓縮要好),這種方式也是推薦的。
既然要聊Unity3D運行時候的內存優化,那咱們天然首先要知道Unity3D遊戲引擎是如何分配內存的。大概能夠分紅三大部分:
第3類不是咱們關注的重點,因此接下來咱們會分別來看一下Unity3D內部內存和Mono託管內存,最後還將分析一個官網上Assetbundle的案例來講明內存的管理。
Unity3D的內部內存都會存放一些什麼呢?各位想想,除了用代碼來驅動邏輯,一個遊戲還須要什麼呢?對,各類資源。因此簡單總結一下Unity3D內部內存存放的東西吧:
由於咱們的遊戲腳本是用C#寫的,同時還要跨平臺,因此帶着一個Mono的託管環境顯然必須的。那麼Mono的託管內存天然就不得不放到內存的優化範疇中進行考慮。那麼咱們所說的Mono託管內存中存放的東西和Unity3D內部內存中存放的東西究竟有何不一樣呢?其實Mono的內存分配就是很傳統的運行時內存的分配了:
而Mono託管堆中的那些封裝的對象,除了在在Mono託管堆上分配封裝類實例化以後所須要的內存以外,還會牽扯到其背後對應的遊戲引擎內部控件在Unity3D內部內存上的分配。
舉一個例子:
一個在.cs腳本中聲明的WWW類型的對象www,Mono會在Mono託管堆上爲www分配它所須要的內存。同時,這個實例對象背後的所表明的引擎資源所須要的內存也須要被分配。
一個WWW實例背後的資源:
如圖:
那麼下面就舉一個AssetBundle的例子:
如下載Assetbundle爲例子,聊一下內存的分配。匹夫從官網的手冊上找到了一個使用Assetbundle的情景以下:
IEnumerator DownloadAndCache (){ // Wait for the Caching system to be ready while (!Caching.ready) yield return null; // Load the AssetBundle file from Cache if it exists with the same version or download and store it in the cache using(WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)){ yield return www; //WWW是第1部分 if (www.error != null) throw new Exception("WWW download had an error:" + www.error); AssetBundle bundle = www.assetBundle;//AssetBundle是第2部分 if (AssetName == "") Instantiate(bundle.mainAsset);//實例化是第3部分 else Instantiate(bundle.Load(AssetName)); // Unload the AssetBundles compressed contents to conserve memory bundle.Unload(false); } // memory is freed from the web stream (www.Dispose() gets called implicitly) } }
內存分配的三個部分匹夫已經在代碼中標識了出來:
那就分別解析一下:
WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)
AssetBundle bundle = www.assetBundle;
Instantiate(bundle.mainAsset);
最後各位可能看到了官網中的這個例子使用了:
using(WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)){ }
這種using的用法。這種用法其實就是爲了在使用完Web Stream以後,將內存釋放掉的。由於WWW也繼承了idispose的接口,因此可使用using的這種用法。其實至關於最後執行了:
//刪除Web Stream www.Dispose();
OK,Web Stream被刪除掉了。那還有誰呢?對Assetbundle。那麼使用
//刪除AssetBundle bundle.Unload(false);
ok,寫到這裏就先打住啦。寫的有點超了。有點趕也有點臨時,往後在補充編輯。
這篇文章當時寫的時候略顯倉促,所以並無特別介紹Unity Profiler工具,也更談不上用Unity Profiler工具來監測內存的使用狀態了。可是使用Unity Profiler工具來監測仍是十分必要的,下面就簡單補充一下這方面的知識。
在Profiler工具中提供了兩種模式供咱們監測內存的使用狀況,即簡易模式和詳細模式。在簡易模式中,咱們能夠看到總的內存(total)列出了兩列,即Used Total(使用總內存)和Reserved Total(預約總內存)。Used Total和Reserved 均是物理內存,其中Reserved是unity向系統申請的總內存,Unity底層爲了避免常常向系統申請開闢內存,開啓了較大一塊內存做爲緩存,即所謂的Reserved內存,而運行時,unity所使用的內存首先是向Reserved中來申請內存,當不使用時也是先向Reserved中釋放內存,從而來保證遊戲運行的流暢性。通常來講,Used Total越大,則Reserved Total越大,而當Used Total降下去後,Reserved Total也是會隨之降低的(但並不必定與Used Total同步)。
Unity3D的內存從大致上能夠分爲如下幾個部分:
而在簡易模式下的監視器最下方,則列出了常見的一些資源以及它們所消耗的內存。
而詳細模式則須要點擊「Take Sample」按鈕來捕獲詳細的內存使用狀況。須要注意的是,因爲得到數據須要花費必定的時間,所以咱們沒法得到實時的詳細內存的使用狀況。在詳細模式中,咱們能夠觀察每一個具體資源和遊戲對象的內存使用狀況。
若是各位看官以爲文章寫得還好,那麼就容小匹夫跪求各位給點個「推薦」,謝啦~