在最近,使用U3D開發的遊戲核心部分功能即將完成,中間由於各種歷史原因,導致項目存在比較大的問題,這些問題在最後,恐怕只能通過一次徹底的重構來解決
現在的遊戲跑起來會有接近130-170個左右的DrawCall,遊戲運行起來明顯感覺到卡,而經過一天的優化,DrawCall成功縮減到30-70個,這個效果是非常顯著的,並且這個優化並沒有通過將現有的資源打包圖集來實現,圖集都是原有的圖集,如果從全局的角度對圖集再進行一次優化,那麼DrawCall還可以再減少十幾個
本次優化的重點包括:層級關係和特效
對於U3D,我是一個菜鳥,對於U3D的一些東西是一知半解,例如DrawCall,我得到的是一些並不完全正確的信息,例如將N個紋理打包成一個圖集,這個圖集就只會產生一個DrawCall,如果不打成圖集,那麼就會有N個DrawCall,這個觀點在很多人的認識裏都是正確的,因爲可以通過簡單的操作來驗證,但嚴格來說,這個觀點是錯誤的,因爲它還受層級關係影響!
渲染順序
U3D的渲染是有順序的,U3D的渲染順序是由我們控制的,控制好U3D的渲染順序,你才能控制好DrawCall
一個DrawCall,表示U3D使用這個材質/紋理,來進行一次渲染,那麼這次渲染假設有3個對象,那麼當3個對象都使用這一個材質/紋理的時候,就會產生一次DrawCall,可以理解爲一次將紋理輸送到屏幕上的過程,(實際上引擎大多會使用如雙緩衝,緩存這類的手段來優化這個過程,但在這裏我們只需要這樣子認識就可以了),假設3個對象使用不同的材質/紋理,那麼無疑會產生3個DrawCall
接下來我們的3個對象使用2個材質,A和B使用材質1,C使用材質2,這時候來看,應該是有2個DrawCall,或者3個DrawCall。應該是2個DrawCall啊,爲什麼會有3個DrawCall???而且是有時候2個,有時候3個。我們按照上面的DrawCall分析流程來分析一下:
1.渲染A,使用材質1
2.渲染B,使用材質1
3.渲染C,使用材質2
在這種情況下是2個DrawCall,在下面這種情況下,則是3個DrawCall
1.渲染A,使用材質1
2.渲染C,使用材質2
3.渲染B,使用材質1
因爲我們沒有控制好渲染順序(或者說沒有去特意控制),所以導致了額外的DrawCall,因爲A和B不是一次性渲染完的,而是被C打斷了,所以導致材質1被分爲兩次渲染
那麼是什麼在控制這個渲染順序呢?首先在多個相機的情況下,U3D會根據相機的深度順序進行渲染,在每個相機中,它會根據你距離相機的距離,由遠到近進行渲染,在UI相機中,還會根據你UI對象的深度進行渲染
那麼我們要做的就是,對要渲染的對象進行一次規劃,正確地排列好它們,規則是,按照Z軸或者深度,對空間進行劃分,然後確定好每個對象的Z軸和深度,讓使用同一個材質的東西,儘量保持在這個空間內,不要讓其他材質的對象進入這個空間,否則就會打斷這個空間的渲染順序
在這個基礎上,更細的規則有:
- 場景中的東西,我們使用Z軸來進行空間的劃分,例如背景層,特效層1,人物層,特效層2
- NGUI中的東西,我們統一使用Depth來進行空間的劃分
- 人物模型,當人物模型只是用一個材質,DrawCall只有1,但是用了2個以上的材質,DrawCall就會暴增(或許對材質的RenderQueue進行規劃也可以使DrawCall只有2個,但這個要拆分好才行),3D人物處於複雜3D場景中的時候,我們的空間規則難免被破壞,這只能在設計的時候儘量去避免這種情況了
- 使用了多個材質的特效,在動畫的過程中,往往會引起DrawCall的波動,在視覺效果可以接受的範圍內,可以將特效也進行空間劃分,假設這個特效是2D顯示,那麼可以使用Z軸來劃分空間
打包圖集
每個材質/紋理的渲染一定是會產生DrawCall的,這個DrawCall只能通過打包圖集來進行優化
製作圖集一般遵循幾個規則:
- 從功能角度進行劃分,例如UI可以劃分爲公共部分,以及每個具體的界面,功能上,顯示上密切相關的圖片打包到一起
- 不要一股腦把所有東西打包到一個圖集裏,特別是那些不可能同時出現的東西,它們就不應該在一個圖集裏,這樣的圖集意義不大,減少不了DrawCall,並且一個你不需要顯示的圖片,會一直佔用你的內存,這讓我非常不爽
- 注意控制圖集的大小,不要讓圖集太大,一個超級大圖集的DrawCall消耗或許頂的上十幾個小圖集的消耗
字符圖集,在使用BMFont或者其他工具生成圖片字的時候,我們往往是直接導入一大串文字,然後直接生成圖片,但實際上這上面的操作也有優化空間,例如BMFont生成的圖片大小,是可以設置的,有兩個規則,一個規則是導出的圖片儘量小,另一個是導出的圖片儘量少,默認的大小應該是512×512,假設你生成的圖片256×256就可以容納,那麼多做一個操作你可以節省這麼多空間,另外當你輸入多幾個字,就導致增加一張圖片時,例如1024變成2048,那麼你可以考慮使用3張512的圖片,這樣也會節省空間
經過精心劃分的圖集在加上精心規劃的渲染順序,DrawCall會有一個質的優化
特效清理
U3D提供了非常便捷的方法讓我們很輕易地使用美術給過來的特效,懶惰的U3D程序猿會直接放入U3D,甚至不去看這是個什麼特效,我們的特效一般都是一瞬間的事情,例如技能特效,或者其他什麼特效,那麼特效播放完,這個特效我們就看不到了,但假設這個特效在播放結束的時候,沒有將自身的Active屬性設置爲false,那麼它就會繼續佔用你的DrawCall,消耗你設備的計算能力,所以程序需要保證當一個特效播放完之後,能夠被消耗,或者設置爲非激活的狀態,可以使用一些公共方法來完成特效播放完之後的清理工作(自己實現2個靜態函數,一個播放完銷燬,一個播放完設置未激活)
unity3D 對於移動平臺的支持無可厚非,但是也有時候用Unity3D 開發出來的應用、遊戲在移動終端上的運行有着明顯的效率問題,比如卡、畫質等各種問題。自己在做遊戲開發的時候偶有所得。對於主要影響性能的因素做個總結。
主要因素有:
1. Saved by batching 值過大 ---- > 這個值主要是針對Mesh的批處理,這個值越高,應用就越卡
2. Drawcall 值過大 ---- > Drawcall 值過大,所需要的 GPU 的處理性能較高,從而導致CPU的計算時間過長,於是就卡了
3. 點、面過多 ---- > 點、面過多,GPU 根據不同面的效果展開計算,並且CPU計算的數據也多,所以效果出來了,但是卡巴斯基
由於 Saved by batching 和 Drawcall 值過大所引起的卡的問題我所做的優化方式有:
1. 對於模型 :Mesh 合併,有個不錯的插件(DrawCall Minimizer ---> 直接上Asset Store 下載即可,免費的,而且有文檔,很容易上手)
2. 對於UI : 儘量避免使用Unity3D自帶的 GUI 換用 NGUI或者EZGUI;因爲這兩個UI插件對於UI中的圖片處理是將UI圖片放置在一個 Atlas 中,一個 Atlas 對應一個Drawcall
3. 對於燈光: 可以使用 Unity3D 自帶的 Lightmapping 插件來烘焙場景中的燈光效果到物體材質上
4. 對於場景: 可以使用 Unity3D 自帶的 Occlusion Culling 插件把靜止不動的場景元素烘焙出來
4. 對於特效:儘量把材質紋理合並
對於Unity3D 在移動終端上支持的Drawcall 數到底多少,主要是跟機子性能有關的,當然也不是說值小性能就一定沒問題(本人親測,也有17就卡的,主要是模型材質紋理過大所引起的),目前我做的是70左右的,還OK,挺正常的
由於點、面過多所導致的性能問題,最好用簡模,用四面體來做複雜的模型,但是面、點也別太多,至於Unity3D 到底支持多少點、面的說法各異,我也搞不懂,總之少些肯定OK
檢測方式:
一,Unity3D 渲染統計窗口
Game視窗的Stats去查看渲染統計的信息:
1、FPS
fps其實就是 frames per second,也就是每一秒遊戲執行的幀數,這個數值越小,說明遊戲越卡。
2、Draw calls
batching之後渲染mesh的數量,和當前渲染到的網格的材質球數量有關。
渲染的批處理數量,這是引擎將多個對象的繪製進行合併從而減少GPU的開銷;
很多GUI插件的一個好處就是合併多個對象的渲染,從而降低DrawCalls ,保證遊戲幀數。
4、Tris 當前繪製的三角面數
5、Verts 當前繪製的頂點數
6、Used Textures 當前幀用於渲染的圖片佔用內存大小
7、Render Textures 渲染的圖片佔用內存大小,也就是當然渲染的物體的材質上的紋理總內存佔用
8、VRAM usage 顯存的使用情況,VRAM總大小取決於你的顯卡的顯存
9、VBO Total 渲染過程中上載到圖形卡的網格的數量,這裏注意一點就是縮放的物體可能需要額外的開銷。
10、Visible Skinned Meshes 蒙皮網格的渲染數量
11、Animations 播放動畫的數量
注意事項:
1,運行時儘量減少 Tris 和 Draw Calls
預覽的時候,可點開 Stats,查看圖形渲染的開銷情況。特別注意 Tris 和 Draw Calls 這兩個參數。
一般來說,要做到:
Tris 保持在 7.5k 以下,有待考證。
Draw Calls 保持在 20 以下,有待考證。
2,FPS,每一秒遊戲執行的幀數,這個數值越小,說明遊戲越卡。
3,Render Textures 渲染的圖片佔用內存大小。
4,VRAM usage 顯存的使用情況,VRAM總大小取決於你的顯卡的顯存。
二,代碼優化
1. 儘量避免每幀處理
比如:
function Update() { DoSomeThing(); }
可改爲每5幀處理一次:
function Update() { if(Time.frameCount % 5 == 0) { DoSomeThing(); } }
2. 定時重複處理用 InvokeRepeating 函數實現
比如,啓動0.5秒後每隔1秒執行一次 DoSomeThing 函數:
function Start() { InvokeRepeating("DoSomeThing", 0.5, 1.0); }
3. 優化 Update, FixedUpdate, LateUpdate 等每幀處理的函數
函數裏面的變量儘量在頭部聲明。
比如:
function Update() { var pos: Vector3 = transform.position; }
可改爲
private var pos: Vector3; function Update(){ pos = transform.position; }
4. 主動回收垃圾
給某個 GameObject 綁上以下的代碼:
function Update() { if(Time.frameCount % 50 == 0) { System.GC.Collect(); } }
5. 優化數學計算
比如,如果可以避免使用浮點型(float),儘量使用整形(int),儘量少用複雜的數學函數比如 Sin 和 Cos 等等
6,減少固定增量時間
將固定增量時間值設定在0.04-0.067區間(即,每秒15-25幀)。您可以通過Edit->Project Settings->Time來改變這個值。這樣做降低了FixedUpdate函數被調用的頻率以及物理引擎執行碰撞檢測與剛體更新的頻率。如果您使用了較低的固定增量時間,並且在主角身上使用了剛體部件,那麼您可以啓用插值辦法來平滑剛體組件。
7,減少GetComponent的調用
使用 GetComponent或內置組件訪問器會產生明顯的開銷。您可以通過一次獲取組件的引用來避免開銷,並將該引用分配給一個變量(有時稱爲"緩存"的引用)。例如,如果您使用如下的代碼:
function Update () {
transform.Translate(0, 1, 0);
}
通過下面的更改您將獲得更好的性能:
var myTransform : Transform;
function Awake () {
myTransform = transform;
}
function Update () {
myTransform.Translate(0, 1, 0);
}
8,避免分配內存
您應該避免分配新對象,除非你真的需要,因爲他們不再在使用時,會增加垃圾回收系統的開銷。您可以經常重複使用數組和其他對象,而不是分配新的數組或對象。這樣做好處則是儘量減少垃圾的回收工作。同時,在某些可能的情況下,您也可以使用結構(struct)來代替類(class)。這是因爲,結構變量主要存放在棧區而非堆區。因爲棧的分配較快,並且不調用垃圾回收操作,所以當結構變量比較小時可以提升程序的運行性能。但是當結構體較大時,雖然它仍可避免分配/回收的開銷,而它由於"傳值"操作也會導致單獨的開銷,實際上它可能比等效對象類的效率還要低。
9,使用iOS腳本調用優化功能
UnityEngine 命名空間中的函數的大多數是在 C/c + +中實現的。從Mono的腳本調用 C/C++函數也存在着一定的性能開銷。您可以使用iOS腳本調用優化功能(菜單:Edit->Project Settings->Player)讓每幀節省1-4毫秒。此設置的選項有:
Slow and Safe – Mono內部默認的處理異常的調用
Fast and Exceptions Unsupported –一個快速執行的Mono內部調用。不過,它並不支持異常,因此應謹慎使用。它對於不需要顯式地處理異常(也不需要對異常進行處理)的應用程序來說,是一個理想的候選項。
10,
優化垃圾回收
如上文所述,您應該儘量避免分配操作。但是,考慮到它們是不能完全杜絕的,所以我們提供兩種方法來讓您儘量減少它們在遊戲運行時的使用:
如果堆比較小,則進行快速而頻繁的垃圾回收
這一策略比較適合運行時間較長的遊戲,其中幀率是否平滑過渡是主要的考慮因素。像這樣的遊戲通常會頻繁地分配小塊內存,但這些小塊內存只是暫時地被使用。如果在iOS系統上使用該策略,那麼一個典型的堆大小是大約 200 KB,這樣在iPhone 3G設備上,垃圾回收操作將耗時大約 5毫秒。如果堆大小增加到1 MB時,該回收操作將耗時大約 7ms。因此,在普通幀的間隔期進行垃圾回收有時候是一個不錯的選擇。通常,這種做法會讓回收操作執行的更加頻繁(有些回收操作並不是嚴格必須進行的),但它們可以快速處理並且對遊戲的影響很小:
if (Time.frameCount % 30 == 0)
{
System.GC.Collect();
}
但是,您應該小心地使用這種技術,並且通過檢查Profiler來確保這種操作確實可以降低您遊戲的垃圾回收時間
如果堆比較大,則進行緩慢且不頻繁的垃圾回收
這一策略適合於那些內存分配 (和回收)相對不頻繁,並且可以在遊戲停頓期間進行處理的遊戲。如果堆足夠大,但還沒有大到被系統關掉的話,這種方法是比較適用的。但是,Mono運行時會儘可能地避免堆的自動擴大。因此,您需要通過在啓動過程中預分配一些空間來手動擴展堆(ie,你實例化一個純粹影響內存管理器分配的"無用"對象):
function Start() {
var tmp = new System.Object[1024];
// make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks
for (var i : int = 0; i < 1024; i++)
tmp[i] = new byte[1024];
// release reference
tmp = null;
}
遊戲中的暫停是用來對堆內存進行回收,而一個足夠大的堆應該不會在遊戲的暫停與暫停之間被完全佔滿。所以,當這種遊戲暫停發生時,您可以顯式請求一次垃圾回收:
System.GC.Collect();
另外,您應該謹慎地使用這一策略並時刻關注Profiler的統計結果,而不是假定它已經達到了您想要的效果。
三,模型
1,壓縮 Mesh
導入 3D 模型之後,在不影響顯示效果的前提下,最好打開 Mesh Compression。
Off, Low, Medium, High 這幾個選項,可酌情選取。
2,避免大量使用 Unity 自帶的 Sphere 等內建 Mesh
Unity 內建的 Mesh,多邊形的數量比較大,如果物體不要求特別圓滑,可導入其他的簡單3D模型代替。