DrawCalls:控制電腦平台上DrawCalls幾千個之內,移動平台上DrawCalls200百左右
Verts:PC平台的話保持場景中顯示的頂點數少於300W,移動設備的話少於10W,一切取決於你的目標GPU與CPU。
需要注意的是:
如果在Profiler下的GPU中顯示的RenderTexture.SetActive()占用率很高的話,那么可能是因為你同時打開了編輯窗口的原因,而不是U3D的BUG。
1、Shader着色器
(1)有些着色器可能是處理器密集型的,因此最好為材質指定移動設備專用的着色器。將着色器從Diffuse修改為Mobile/Diffuse。
(2)shader中用貼圖混合的方式去代替多重通道計算。
(3)shader中注意float/half/fixed的使用。
(4)shader中不要用復雜的計算pow,sin,cos,tan,log等。
(5)shader中越少Fragment越好。
(6)自己寫的shader請注意復雜操作符計算,類似pow,exp,log,cos,sin,tan等都是很耗時的計算,最多只用一次在每個像素點的計算。不推薦你自己寫normalize,dot,inversesqart操作符,內置的肯定比你寫的好。
(7)需要警醒的是alpha test,這個非常耗時。
(8)浮點類型運算:精度越低的浮點計算越快。
在CG/HLSL中:float :32位浮點格式,適合頂點變換運算,但比較慢。
half:16位浮點格式,適合貼圖和UV坐標計算,是highp類型計算的兩倍。
fixed: 10位浮點格式,適合顏色,光照,和其他。是highp格式計算的四倍。
2、光源
(1)最好使用平行光,點光源和聚光燈消耗資源比較大
(2)限制燈光使用數量,盡可能不用燈光。動態燈光更加不要了。
(3)Lightmapping烘焙燈光,為場景添加光源時要考慮一下,因為有渲染開銷。如果你以前做過着色器編程,你會知道為了支持動態光源的渲染,要付出額外的代價。每個光源都需要渲染對象,根據對象使用的着色器、材質計算最終的光源效果,這個計算開銷很大。盡可能的在渲染之前就將光源細節“烘焙(bake)” 到對象的紋理中。“烘焙”是使用靜態光源效果的渲染方式,可以實現相同的視覺效果,而無需額外的計算開銷。
(4)實時陰影技術非常棒,但消耗大量計算。為GPU和CPU都帶來了昂貴的負擔
(5)燈光的Shadow Type只對PC平台有效,也就是說在移動平台是沒有陰影效果的(親測),另外軟陰影更為昂貴,耗資源!!!
(6)light的Render Mode下的Auto是根據附近燈光的亮度和當前質量的設置(Edit ->Project Settings -> Quality )在運行時確定,Not Important為頂點渲染,Important為像素渲染(更耗資源),但是像素渲染能夠實現頂點渲染不能實現的效果,比如實時陰影,因此必須權衡前后照明質量和游戲速度。像素燈的實際數量可以在質量設置( Edit -> Project Settings -> Quality )中的進行設置。
3、碰撞器
(1)通常,碰撞器根據復雜度排序,對象越復雜,使用這個對象的性能開銷越大。有可能的話,用盒子或者球體(Box/Sphere)來封裝對象,這樣碰撞器的計算最少。不要用網格碰撞器(Mesh Collider)。
(2)注意碰撞體的碰撞層,不必要的碰撞檢測請舍去。
4、貼圖紋理
(1)可以把圖像紋理或者其它資源共享使用,盡量避免透明,可以使用填充黑色
(2)嘗試用壓縮貼圖格式,或用16位代替32位。圖片壓縮將降低你的圖片大小(更快地加載更小的內存跨度(footprint)),而且大大提高渲染表現。壓縮貼圖比起未壓縮的32位RGBA貼圖占用內存帶寬少得多。
(3)之前U3D會議還聽說過一個優化,貼圖盡量都用一個大小的格式(512 * 512 , 1024 * 1024),這樣在內存之中能得到更好的排序,而不會有內存之間空隙。
(4)MIPMAps,跟網頁上的略縮圖原理一樣,在3D游戲中我們為游戲的貼圖生成多重紋理貼圖,遠處顯示較小的物體用小的貼圖,顯示比較大的物體用精細的貼圖。這樣能更加有效的減少傳輸給GPU中的數據。但同時也會增加內存的大小,自己根據項目來權衡利弊
(5)如果你做了一個圖集是1024X1024的。此時你的界面上只用了圖集中的一張很小的圖,那么很抱歉1024X1024這張大圖都需要載入你的內存里面,1024就是4M的內存,如果你做了10個1024的圖集,你的界面上剛好都只用了每個圖集里面的一張小圖,那么再次抱歉你的內存直接飆40M。意思是任何一個4096的圖片,不管是圖集還是texture,他都占用4*4=16M?
(6)IOS平台使用PVRTC壓縮紋理。Adroid平台使用ETC1格式壓縮。均可以減至1/4的內存大小,優化非常明顯!目前主流的Android機型基本都支持ETC1格式壓縮。但ETC1只能支持非Alpha通道的圖片壓縮。所以一般把Alpha通道圖分離出來,繪制到GPU顯存時,a值從Alpha圖里獲取,無Alpha通道的圖就可以使用ETC1壓縮。
(7)設置不透明貼圖的壓縮格式為ETC 4bit,因為android市場的手機中的GPU有多種,每家的GPU支持不同的壓縮格式,但他們都兼容ETC格式
對於透明貼圖,我們只能選擇RGBA 16bit 或者RGBA 32bit。
(8)減少FPS,在ProjectSetting-> Quality中的VSync Count 參數會影響你的FPS,EveryVBlank相當於FPS=60,EverySecondVBlank = 30;
如果這兩種情況都不符合游戲的FPS的話,我們需要手動調整FPS,首先關閉垂直同步(VSync = Vertical Sync)這個功能,即設置VSync為Don't Sync
然后在代碼的Awake方法里手動設置FPS(Application.targetFrameRate = 45;)
降低FPS的好處:
1)省電,減少手機發熱的情況;
2)能都穩定游戲FPS,減少出現卡頓的情況。
(9)當我們設置了FPS后,再調整下Fixed timestep這個參數,這個參數在ProjectSetting->Time中,目的是減少物理計算的次數,來提高游戲性能。
(10)盡量少使用Update LateUpdate FixedUpdate,這樣也可以提升性能和節省電量。多使用事件(不是SendMessage,使用自己寫的,或者C#中的事件委托)。
(11)待機時,調整游戲的FPS為1,節省電量。
(12)圖集大小最好不要高於1024,否則游戲安裝之后、低端機直接崩潰、原因是手機系統版本低於2.2、超過1000的圖集無法讀取導致。2.2 以上沒有遇見這個情況。注意手機的RAM 與 ROM、小於 512M的手機、直接放棄機型適配。
(13)不同設備要使用不同的紋理大小,尤其是UI和大型背景中的紋理。《Shadow Blade》使用的是通用型模板,但如果在啟動時檢測到設備大小和分辨率,就會載入不同資產。
(14)遠處的物體繪制在skybox上
5、腳本
(1)如果你不需要運行某一個腳本,那么就禁用它。不管它多少的小,或者出現的很少,但每一個處理都需要占用時間。
(2)不要留着未實現的Update,FixedUpdate等方法,用不到就刪除,不然會執行,消耗時間!
(3)移除代碼中的任何字符串連接,因為這會給GC留下大量垃圾。使用StringBuilder鏈接字符串
(4)用簡單的“for”循環代替“foreach”循環。由於某些原因,每個“foreach”循環的每次迭代會生成24字節的垃圾內存。一個簡單的循環迭代10次就可以留下240字節的垃圾內存。
(5)更改我們檢查游戲對象標簽的方法。用“if (go.CompareTag (“Enemy”)”來代替“if (go.tag == “Enemy”)” 。在一個內部循環調用對象分配的標簽屬性以及拷貝額外內存,這是一個非常糟糕的做法。
(6)不使用LINQ命令,因為它們一般會分配中間緩器,而這很容易生成垃圾內存。
(7)修改代碼以免依賴“ControllerColliderHit” 回調函數。這證明這些回調函數處理得並不十分迅速。
(8)要謹慎評估觸發器的“onInside”回調函數,在我們的項目中,我們盡量在不依賴它們的情況下模擬邏輯。
(9)注意是否有多余的動畫腳本,模型自動導入到U3D會有動畫腳本,大量的話會嚴重影響消耗CPU計算。
(10)盡量避免每幀處理,可以每隔幾幀處理一次 void Update() {
if
(Time.frameCount % 5 == 0) { DoSomeThing(); } }
(11)盡量避免使用float,而使用int,特別是在手機游戲中,盡量少用復雜的數學函數,比如sin,cos等函數。改除法/為乘法,例如:使用x*0.5f而不是 x/2.0f 。
(12)避免使用for(int i=0;i<myArray.Length;i++),而應該這樣 int length=myArray.Length; for(int i=0;i<length;i++)
(13)少使用臨時變量,特別是在Update OnGUI等實時調用的函數中定義臨時變量。
(14)協同是一個好方法。可以使用協同程序來代替不必每幀都執行的方法。(還有InvokeRepeating方法也是一個好的取代Update的方法)。
(15)不要使用SendMessage之類的方法,他比直接調用方法慢了100倍,你可以直接調用或通過C#的委托來實現。
(16)操作transform.localPosition的時候請小心,移動GameObject是非常平常的一件事情,以下代碼看起來很簡單:
transform.localPosition += new Vector3 ( 10.0f * Time.deltaTime, 0.0f, 0.0f );
但是小心了,假設上面這個GameObject有一個parent, 並且這個parent GameObject的localScale是(2.0f,2.0f,2.0f)。你的GameObject將會移動20.0個單位/秒。
因為該 GameObject的world position等於:
Vector3 offset = new Vector3( my.localPosition.x * parent.lossyScale.x, my.localPosition.y * parent.lossyScale.y, my.localPosition.z * parent.lossyScale.z );
Vector3 worldPosition = parent.position + parent.rotation * offset;
換句話說,上面這種直接操作localPosition的方式是在沒有考慮scale計算的時候進行的,為了解決這個問題,unity3d提供了Translate函數,
所以正確的做法應該是:
transform.Translate ( 10.0f * Time.deltaTime, 0.0f, 0.0f );
(17)減少固定增量時間, 將固定增量時間值設定在0.04-0.067區間(即,每秒15-25幀)。您可以通過Edit->Project Settings->Time來改變這個值。這樣做降低了FixedUpdate函數被調用的頻率以及物理引擎執行碰撞檢測與剛 體更新的頻率。如果您使用了較低的固定增量時間,並且在主角身上使用了剛體部件,那么您可以啟用插值辦法來平滑剛體組件。
(18)減少GetComponent的調用使用,GetComponent或內置組件訪問器(transform)會產生明顯的開銷。您可以通過一次獲取組件的引用來避免開銷,並將該引用分配給一個變量(有時稱為"緩存"的引用)。
Transform myTransform ; void Awake () { myTransform = transform; }
(19)同時,在某些可能的情況下,您也可以使用結構(struct)來代替類(class)。這是因為,結構變量主要存放在棧區而非堆區。因為棧的分配較快,並且不調用垃圾回收操作,所以當結構變量比較小時可以提升程序的運行性能。但是當結構體較大時,雖然它仍可避免分配/回收的開銷,而它由於"傳值"操作也會導致單獨的開銷,實際上它可能比等效對象類的效率還要低。
(20)使用GUILayout 函數可以很方便地將GUI元素進行自動布局。然而,這種自動化自然也附帶着一定的處理開銷。您可以通過手動的GUI功能布局來避免這種開銷。此外,您也可以設置一個腳本的useGUILayout變量為 false來完全禁用GUI布局:
void Awake () { useGUILayout = false; }
(21)最小化碰撞檢測請求(例如ray casts和sphere checks),盡量從每次檢查中獲得更多信息。
(22)在edit->project setting->time中調大FixedTimestep(真實物理的幀率)來減少cpu損耗
(23)盡量不要動態的instantiate和destroy object,使用object pool
(24)盡量不要再update函數中做復雜計算,如有需要,可以隔N幀計算一次
(25)不要使用內置的onGUii函數處理gui,使用其他方案,如NGUI
6、組件
(1)盡可能的使用簡單組件—如果你不需求功能較多的組件,那么就自己去實現它避免一起使用大量系統組件。比如,CharacterController是一個很廢資源的組件,那么最好使用剛體來定義自己的解決方案。
(2)面對性能更弱的設備,要用skinned mesh代替physics cloth。cloth參數在運行表現中發揮重要作用,如果你肯花些時間找到美學與運行表現之間的平衡點,就可以獲得理想的結果。
(3)在物理模擬過程中不要使用ragdolls( 布娃娃系統),只有在必要時才讓它生效。
(4)真實的物理(剛體)很消耗,不要輕易使用,盡量使用自己的代碼模仿假的物理
7、NGUI
(1)NGUI中所有Panel都有一個Depth值影響着他下面的所有掛件。如果你正在創建一個使用多個窗口的復雜UI,通常最好的做法是每個窗口有一個UIPanel。請確認你的panel不會擁有相同的depth值。如果這個值是一樣的,為了保證繪制順序,draw call將會開始頻繁分割,這將導致產生比平常更多的draw call。
8、頂點數
(1)盡量減少頂點數
9、材質
(1)盡可能共用材質。這樣便可以減少DrawCall,引擎可以進行其批處理!
(2)如果你需要通過腳本來控制單個材質屬性,需要注意改變Renderer.material將會造成一份材質的拷貝。因此,你應該使用Renderer.sharedMaterial來保證材質的共享狀態。
(3)有一個合並模型材質不錯的插件叫Mesh Baker
10、特效
(1)如果不需要別用霧效(fog)
(2)要找到美學/性能之間的平衡,就免不了許多粒子效果的迭代。減少發射器數量並盡量減少透明度需求也是一大挑戰。
11、模型物體
(1)不要有不必要的三角面。面片數最好控制在300~2000面片
(2)UV貼圖中的接縫和硬邊越少越好。
需要注意的是,圖形硬件需要處理頂點數和硬件報告說的並不一樣。不是硬件說能渲染幾個點就是幾個點。模型處理應用通常展示的是幾何頂點數量。例如,一個由一些不同頂點構成的模型。在顯卡中,一些集合頂點將會被分離(split)成兩個或者更多邏輯頂點用作渲染。如果有法線、UV坐標、頂點色的話,這個頂點必須會被分離。所以在游戲中處理的實際數量顯然要多很多。
(3)LOD (Level Of Detail) 是很常用的3D游戲技術了,其功能理解起來則是相當於多重紋理貼圖。在以在屏幕中顯示模型大小的比例來判斷使用高或低層次的模型來減少對GPU的傳輸數據,和減少GPU所需要的頂點計算。
(4)攝像機分層距離剔除(Per-Layer Cull Distances):為小物體標識層次,然后根據其距離主攝像機的距離判斷是否需要顯示。
(5)遮擋剔除(Occlusion Culling)其實就是當某個物體在攝像機前被另外一個物體完全擋住的情況,擋住就不發送給GPU渲染,從而直接降低DRAW CALL。不過有些時候在CPU中計算其是否被擋住則會很耗計算,反而得不償失。
(6)將不需要移動的物體設為Static,讓引擎可以進行其批處理。
(7)用單個蒙皮渲染、盡量少用材質、少用骨骼節點、移動設備上角色多邊形保持在300~1500內(當然還要看具體的需求)、PC平台上1500~4000內(當然還要看具體的需求)。
角色的面數一般不要超過1500,骨骼數量少於30就好,越多的骨骼就會越多的帶來CPU消耗,角色Material數量一般1~2個為最佳。
(8)導入 3D 模型之后,在不影響顯示效果的前提下,最好打開 Mesh Compression。Off, Low, Medium, High 這幾個選項,可酌情選取。
(9)避免大量使用unity自帶的 Sphere 等內建 Mesh,Unity 內建的 Mesh,多邊形的數量比較大,如果物體不要求特別圓滑,可導入其他的簡單3D模型代替。
(10)每個角色盡量使用一個Skinned Mesh Renderer,這是因為當角色僅有一個 Skinned Mesh Renderer 時,Unity 會使用視錐型可見性裁剪和多邊形網格包圍體更新的方法來優化角色的運動,而這種優化只有在角色僅含有一個 Skinned Mesh Renderer時才會啟動。
(11)對於靜態物體頂點數要求少於500,UV的取值范圍不要超過(0,1)區間,這對於紋理的拼合優化很有幫助。
(12)不需要的Animation組件就刪掉
12、粒子系統
(1)粒子系統運行在iPhone上時很慢,怎么辦?因為iPhone擁有相對較低的fillrate 。如果您的粒子效果覆蓋大部分的屏幕,而且是multiple layers的,這樣即使最簡單的shader,也能讓iPhone傻眼。我們建議把您的粒子效果baking成紋理序列圖。然后在運行時可以使用1-2個粒子,通過動畫紋理來顯示它們。這種方式可以取得很好的效果,以最小的代價。
自帶地形:地形高度圖尺寸小於257,盡量使用少的混合紋理數目,盡量不要超過4個,Unity自帶的地形時十分占資源的,強烈建議不要使用,自己制作地形,盡量一張貼圖搞定
- drawcall是啥?draw:繪制,call:調用,其實就是對底層圖形程序(比如:OpenGL ES)接口的調用,以在屏幕上畫出東西。那么,是誰去調用這些接口呢?CPU。
- fragment是啥?經常有人說vf啥的,vertex我們都知道是頂點,那fragment是啥呢?說它之前需要先說一下像素,像素各位應該都知道吧?像素是構成數碼影像的基本單元呀。那fragment呢?是有可能成為像素的東西。啥叫有可能?就是最終會不會被畫出來不一定,是潛在的像素。這會涉及到誰呢?GPU。
- batching是啥?都知道批處理是干嘛的吧?沒錯,將批處理之前需要很多次調用(drawcall)的物體合並,之后只需要調用一次底層圖形程序的接口就行。聽上去這簡直就是優化的終極方案啊!但是,理想是美好的,世界是殘酷的,一些不足之后我們再細聊。
- 內存的分配:記住,除了Unity3D自己的內存損耗。我們可是還帶着Mono呢啊,還有托管的那一套東西呢。更別說你一激動,又引入了自己的幾個dll。這些都是內存開銷上需要考慮到的。
- CPU方面
上文中說了,drawcall影響的是CPU的效率,而且也是最知名的一個優化點。但是除了drawcall之外,還有哪些因素也會影響到CPU的效率呢?讓我們一一列出暫時能想得到的:
(1)DrawCalls
(2)物理組件(Physics)
(3)GC(什么?GC不是處理內存問題的嘛?匹夫你不要騙我啊!不過,匹夫也要提醒一句,GC是用來處理內存的,但是是誰使用GC去處理內存的呢?)
(4)當然,還有代碼質量
DrawCalls:
前面說過了,DrawCall是CPU調用底層圖形接口。比如有上千個物體,每一個的渲染都需要去調用一次底層接口,而每一次的調用CPU都需要做很多工作,那么CPU必然不堪重負。但是對於GPU來說,圖形處理的工作量是一樣的。所以對DrawCall的優化,主要就是為了盡量解放CPU在調用圖形接口上的開銷。所以針對drawcall我們主要的思路就是每個物體盡量減少渲染次數,多個物體最好一起渲染。所以,按照這個思路就有了以下幾個方案:
- 使用Draw Call Batching,也就是描繪調用批處理。Unity在運行時可以將一些物體進行合並,從而用一個描繪調用來渲染他們。具體下面會介紹。
- 通過把紋理打包成圖集來盡量減少材質的使用。
- 盡量少的使用反光啦,陰影啦之類的,因為那會使物體多次渲染。
Draw Call Batching
首先我們要先理解為何2個沒有使用相同材質的物體即使使用批處理,也無法實現Draw Call數量的下降和性能上的提升。
因為被“批處理”的2個物體的網格模型需要使用相同材質的目的,在於其紋理是相同的,這樣才可以實現同時渲染的目的。因而保證材質相同,是為了保證被渲染的紋理相同。
因此,為了將2個紋理不同的材質合二為一,我們就需要進行上面列出的第二步,將紋理打包成圖集。具體到合二為一這種情況,就是將2個紋理合成一個紋理。這樣我們就可以只用一個材質來代替之前的2個材質了。
而Draw Call Batching本身,也還會細分為2種。
Static Batching 靜態批處理
看名字,猜使用的情景。
靜態?那就是不動的咯。還有呢?額,聽上去狀態也不會改變,沒有“生命”,比如山山石石,樓房校舍啥的。那和什么比較類似呢?嗯,聰明的各位一定覺得和場景的屬性很像吧!所以我們的場景似乎就可以采用這種方式來減少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的動態批處理。
Dynamic Batching 動態批處理
有陰就有陽,有靜就有動,所以聊完了靜態批處理,肯定跟着就要說說動態批處理了。首先要明確一點,Unity3D的draw call動態批處理機制是引擎自動進行的,無需像靜態批處理那樣手動設置static。我們舉一個動態實例化prefab的例子,如果動態物體共享相同的材質,則引擎會自動對draw call優化,也就是使用批處理。首先,我們將一個cube做成prefab,然后再實例化50次,看看draw call的數量。
for(int i = 0; i < 50; i++) { GameObject cube; cube = GameObject.Instantiate(prefab) as GameObject; }
draw call的數量:
可以看到draw call的數量為1,而 saved by batching的數量是49。而這個過程中,我們除了實例化創建物體之外什么都沒做。不錯,unity3d引擎為我們自動處理了這種情況。
但是有很多童靴也遇到這種情況,就是我也是從prefab實例化創建的物體,為何我的draw call依然很高呢?這就是匹夫上文說的,draw call的動態批處理存在着很多約束。下面匹夫就演示一下,針對cube這樣一個簡單的物體的創建,如果稍有不慎就會造成draw call飛漲的情況吧。
我們同樣是創建50個物體,不同的是其中的10個物體,每個物體的大小都不同,也就是Scale不同。
for(int i = 0; i < 50; i++) { GameObject cube; cube = GameObject.Instantiate(prefab) as GameObject; if(i / 10 == 0) { cube.transform.localScale = new Vector3(2 + i, 2 + i, 2 + i); } }
我們看到draw call的數量上升到了11次,而saved by batching的數量也下降到了39。各位看官可以看到,僅僅是一個簡單的cube的創建,如果scale不同,竟然也不會去做批處理優化。這僅僅是動態批處理機制的一種約束,那我們總結一下動態批處理的約束,各位也許也能從中找到為何動態批處理在自己的項目中不起作用的原因:
- 批處理動態物體需要在每個頂點上進行一定的開銷,所以動態批處理僅支持小於900頂點的網格物體。
- 如果你的着色器使用頂點位置,法線和UV值三種屬性,那么你只能批處理300頂點以下的物體;如果你的着色器需要使用頂點位置,法線,UV0,UV1和切向量,那你只能批處理180頂點以下的物體。
- 不要使用縮放。分別擁有縮放大小(1,1,1) 和(2,2,2)的兩個物體將不會進行批處理。
- 統一縮放的物體不會與非統一縮放的物體進行批處理。
- 使用縮放尺度(1,1,1) 和 (1,2,1)的兩個物體將不會進行批處理,但是使用縮放尺度(1,2,1) 和(1,3,1)的兩個物體將可以進行批處理。
- 使用不同材質的實例化物體(instance)將會導致批處理失敗。
- 擁有lightmap的物體含有額外(隱藏)的材質屬性,比如:lightmap的偏移和縮放系數等。所以,擁有lightmap的物體將不會進行批處理(除非他們指向lightmap的同一部分)。
- 多通道的shader會妨礙批處理操作。比如,幾乎unity中所有的着色器在前向渲染中都支持多個光源,並為它們有效地開辟多個通道。
- 預設體的實例會自動地使用相同的網格模型和材質。
所以,盡量使用靜態的批處理。
物理組件
1.設置一個合適的Fixed Timestep。設置的位置:Edit → Project Settings → Time
那何謂“合適”呢?首先我們要搞明白Fixed Timestep和物理組件的關系。物理組件,或者說游戲中模擬各種物理效果的組件,最重要的是什么呢?計算啊。對,需要通過計算才能將真實的物理效果展現在虛擬的游戲中。那么Fixed Timestep這貨就是和物理計算有關的啦。所以,若計算的頻率太高,自然會影響到CPU的開銷。同時,若計算頻率達不到游戲設計時的要求,有會影響到功能的實現,所以如何抉擇需要各位具體分析,選擇一個合適的值。
2.就是不要使用網格碰撞器(mesh collider):為啥?因為實在是太復雜了。網格碰撞器利用一個網格資源並在其上構建碰撞器。對於復雜網狀模型上的碰撞檢測,它要比應用原型碰撞器精確的多。標記為凸起的(Convex )的網格碰撞器才能夠和其他網格碰撞器發生碰撞。各位上網搜一下mesh collider的圖片,自然就會明白了。我們的手機游戲自然無需這種性價比不高的東西。
當然,從性能優化的角度考慮,物理組件能少用還是少用為好。
處理內存,卻讓CPU受傷的GC
在CPU的部分聊GC,感覺是不是怪怪的?其實小匹夫不這么覺得,雖然GC是用來處理內存的,但的確增加的是CPU的開銷。因此它的確能達到釋放內存的效果,但代價更加沉重,會加重CPU的負擔,因此對於GC的優化目標就是盡量少的觸發GC。
首先我們要明確所謂的GC是Mono運行時的機制,而非Unity3D游戲引擎的機制,所以GC也主要是針對Mono的對象來說的,而它管理的也是Mono的托管堆。 搞清楚這一點,你也就明白了GC不是用來處理引擎的assets(紋理啦,音效啦等等)的內存釋放的,因為U3D引擎也有自己的內存堆而不是和Mono一起使用所謂的托管堆。
其次我們要搞清楚什么東西會被分配到托管堆上?不錯咯,就是引用類型咯。比如類的實例,字符串,數組等等。而作為int,float,包括結構體struct其實都是值類型,它們會被分配在堆棧上而非堆上。所以我們關注的對象無外乎就是類實例,字符串,數組這些了。
那么GC什么時候會觸發呢?兩種情況:
- 首先當然是我們的堆的內存不足時,會自動調用GC。
- 其次呢,作為編程人員,我們自己也可以手動的調用GC。
所以為了達到優化CPU的目的,我們就不能頻繁的觸發GC。而上文也說了GC處理的是托管堆,而不是Unity3D引擎的那些資源,所以GC的優化說白了也就是代碼的優化。那么匹夫覺得有以下幾點是需要注意的:
- 字符串連接的處理。因為將兩個字符串連接的過程,其實是生成一個新的字符串的過程。而之前的舊的字符串自然而然就成為了垃圾。而作為引用類型的字符串,其空間是在堆上分配的,被棄置的舊的字符串的空間會被GC當做垃圾回收。
- 盡量不要使用foreach,而是使用for。foreach其實會涉及到迭代器的使用,而據傳說每一次循環所產生的迭代器會帶來24 Bytes的垃圾。那么循環10次就是240Bytes。
- 不要直接訪問gameobject的tag屬性。比如if (go.tag == “human”)最好換成if (go.CompareTag (“human”))。因為訪問物體的tag屬性會在堆上額外的分配空間。如果在循環中這么處理,留下的垃圾就可想而知了。
- 使用“池”,以實現空間的重復利用。
- 最好不用LINQ的命令,因為它們會分配臨時的空間,同樣也是GC收集的目標。而且我很討厭LINQ的一點就是它有可能在某些情況下無法很好的進行AOT編譯。比如“OrderBy”會生成內部的泛型類“OrderedEnumerable”。這在AOT編譯時是無法進行的,因為它只是在OrderBy的方法中才使用。所以如果你使用了OrderBy,那么在IOS平台上也許會報錯。
代碼?腳本?
聊到代碼這個話題,也許有人會覺得匹夫多此一舉。因為代碼質量因人而異,很難像上面提到的幾點,有一個明確的評判標准。也是,公寫公有理,婆寫婆有理。但是匹夫這里要提到的所謂代碼質量是基於一個前提的:Unity3D是用C++寫的,而我們的代碼是用C#作為腳本來寫的,那么問題就來了~腳本和底層的交互開銷是否需要考慮呢?也就是說,我們用Unity3D寫游戲的“游戲腳本語言”,也就是C#是由mono運行時托管的。而功能是底層引擎的C++實現的,“游戲腳本”中的功能實現都離不開對底層代碼的調用。那么這部分的開銷,我們應該如何優化呢?
- 以物體的Transform組件為例,我們應該只訪問一次,之后就將它的引用保留,而非每次使用都去訪問。這里有人做過一個小實驗,就是對比通過方法GetComponent<Transform>()獲取Transform組件, 通過MonoBehavor的transform屬性去取,以及保留引用之后再去訪問所需要的時間:
- GetComponent = 619ms
- Monobehaviour = 60ms
- CachedMB = 8ms
- Manual Cache = 3ms
2.如上所述,最好不要頻繁使用GetComponent,尤其是在循環中。
3.善於使用OnBecameVisible()和OnBecameVisible(),來控制物體的update()函數的執行以減少開銷。
4.使用內建的數組,比如用Vector3.zero而不是new Vector(0, 0, 0);
5.對於方法的參數的優化:善於使用ref關鍵字。值類型的參數,是通過將實參的值復制到形參,來實現按值傳遞到方法,也就是我們通常說的按值傳遞。復制嘛,總會讓人感覺很笨重。比如Matrix4x4這樣比較復雜的值類型,如果直接復制一份新的,反而不如將值類型的引用傳遞給方法作為參數。
好啦,CPU的部分匹夫覺得到此就介紹的差不多了。下面就簡單聊聊其實匹夫並不是十分熟悉的部分,GPU的優化。
GPU的優化
GPU與CPU不同,所以側重點自然也不一樣。GPU的瓶頸主要存在在如下的方面:
- 填充率,可以簡單的理解為圖形處理單元每秒渲染的像素數量。
- 像素的復雜度,比如動態陰影,光照,復雜的shader等等
- 幾何體的復雜度(頂點數量)
- 當然還有GPU的顯存帶寬
那么針對以上4點,其實仔細分析我們就可以發現,影響的GPU性能的無非就是2大方面,一方面是頂點數量過多,像素計算過於復雜。另一方面就是GPU的顯存帶寬。那么針鋒相對的兩方面舉措也就十分明顯了。
減少繪制的數目
那么第一個方面的優化也就是減少頂點數量,簡化復雜度,具體的舉措就總結如下了:
- 保持材質的數目盡可能少。這使得Unity更容易進行批處理。
- 使用紋理圖集(一張大貼圖里包含了很多子貼圖)來代替一系列單獨的小貼圖。它們可以更快地被加載,具有很少的狀態轉換,而且批處理更友好。
- 如果使用了紋理圖集和共享材質,使用Renderer.sharedMaterial 來代替Renderer.material 。
- 使用光照紋理(lightmap)而非實時燈光。
- 使用LOD,好處就是對那些離得遠,看不清的物體的細節可以忽略。
- 遮擋剔除(Occlusion culling)
- 使用mobile版的shader。因為簡單。
優化顯存帶寬
第二個方向呢?壓縮圖片,減小顯存帶寬的壓力。
- OpenGL ES 2.0使用ETC1格式壓縮等等,在打包設置那里都有。
- 使用mipmap。
上面是一個mipmap 如何儲存的例子,左邊的主圖伴有一系列逐層縮小的備份小圖
是不是很一目了然呢?Mipmap中每一個層級的小圖都是主圖的一個特定比例的縮小細節的復制品。因為存了主圖和它的那些縮小的復制品,所以內存占用會比之前大。但是為何又優化了顯存帶寬呢?因為可以根據實際情況,選擇適合的小圖來渲染。所以,雖然會消耗一些內存,但是為了圖片渲染的質量(比壓縮要好),這種方式也是推薦的。
內存的優化
既然要聊Unity3D運行時候的內存優化,那我們自然首先要知道Unity3D游戲引擎是如何分配內存的。大概可以分成三大部分:
- Unity3D內部的內存
- Mono的托管內存
- 若干我們自己引入的DLL或者第三方DLL所需要的內存。
第3類不是我們關注的重點,所以接下來我們會分別來看一下Unity3D內部內存和Mono托管內存,最后還將分析一個官網上Assetbundle的案例來說明內存的管理。
Unity3D內部內存
Unity3D的內部內存都會存放一些什么呢?各位想一想,除了用代碼來驅動邏輯,一個游戲還需要什么呢?對,各種資源。所以簡單總結一下Unity3D內部內存存放的東西吧:
- 資源:紋理、網格、音頻等等
- GameObject和各種組件。
- 引擎內部邏輯需要的內存:渲染器,物理系統,粒子系統等等
Mono托管內存
因為我們的游戲腳本是用C#寫的,同時還要跨平台,所以帶着一個Mono的托管環境顯然必須的。那么Mono的托管內存自然就不得不放到內存的優化范疇中進行考慮。那么我們所說的Mono托管內存中存放的東西和Unity3D內部內存中存放的東西究竟有何不同呢?其實Mono的內存分配就是很傳統的運行時內存的分配了:
- 值類型:int型啦,float型啦,結構體struct啦,bool啦之類的。它們都存放在堆棧上(注意額,不是堆所以不涉及GC)。
- 引用類型:其實可以狹義的理解為各種類的實例。比如游戲腳本中對游戲引擎各種控件的封裝。其實很好理解,C#中肯定要有對應的類去對應游戲引擎中的控件。那么這部分就是C#中的封裝。由於是在堆上分配,所以會涉及到GC。
而Mono托管堆中的那些封裝的對象,除了在在Mono托管堆上分配封裝類實例化之后所需要的內存之外,還會牽扯到其背后對應的游戲引擎內部控件在Unity3D內部內存上的分配。
舉一個例子:
一個在.cs腳本中聲明的WWW類型的對象www,Mono會在Mono托管堆上為www分配它所需要的內存。同時,這個實例對象背后的所代表的引擎資源所需要的內存也需要被分配。
一個WWW實例背后的資源:
- 壓縮的文件
- 解壓縮所需的緩存
- 解壓縮之后的文件
如圖:
那么下面就舉一個AssetBundle的例子:
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) } }
內存分配的三個部分匹夫已經在代碼中標識了出來:
- Web Stream:包括了壓縮的文件,解壓所需的緩存,以及解壓后的文件。
- AssetBundle:Web Stream中的文件的映射,或者說引用。
- 實例化之后的對象:就是引擎的各種資源文件了,會在內存中創建出來。
那就分別解析一下:
WWW www = WWW.LoadFromCacheOrDownload (BundleURL, version)
- 將壓縮的文件讀入內存中
- 創建解壓所需的緩存
- 將文件解壓,解壓后的文件進入內存
- 關閉掉為解壓創建的緩存
AssetBundle bundle = www.assetBundle;
- AssetBundle此時相當於一個橋梁,從Web Stream解壓后的文件到最后實例化創建的對象之間的橋梁。
- 所以AssetBundle實質上是Web Stream解壓后的文件中各個對象的映射。而非真實的對象。
- 實際的資源還存在Web Stream中,所以此時要保留Web Stream。
Instantiate(bundle.mainAsset);
- 通過AssetBundle獲取資源,實例化對象
最后各位可能看到了官網中的這個例子使用了:
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);
盡可能地減少 Drawcall 的數量。
減少的方法主要有如下幾種: Frustum Culling ,Occlusion Culling , Texture Packing 。
Frustum Culling 是 Unity 內建的,我們需要做的就是尋求一個合適的遠裁剪平面;
Occlusion Culling ,遮擋剔除, Unity 內嵌了 Umbra ,一個非常好 OC 庫。
但 Occlusion Culling 也並不是放之四海而皆准的,有時候進行 OC 反而比不進行還要慢,
建議在 OC 之前先確定自己的場景是否適合利用 OC 來優化; Texture Packing ,或者叫 Texture Atlasing ,
是將同種 shader 的紋理進行拼合,根據 Unity 的 static batching 的特性來減少 draw call 。
建議使用,但也有弊端,那就是一定要將場景中距離相近的實體紋理進行拼合,否則,拼合后很可能會增加每幀渲染所需的紋理大小,加大內存帶寬的負擔。這也就是為什么會出現“ DrawCall 降了,渲染速度也變慢了”的原因。