Unity游戲項目性能優(yōu)化總結(jié)

概述

本文就Unity游戲項目性能優(yōu)化作出了總結(jié)。包括Profile工具、Unity使用、機制設計、腳本編寫等方面內(nèi)容。
本文的測試機型皆為iPhone6。為方便找出瓶頸目標幀率先提高為60fps,后面再看實際情況是否限幀30fps。
本文的Unity版本為5.5.0f3或更新版本。

本文將持續(xù)更新。

Profiler工具

在Unity項目中,可能使用到的Profiler工具分3種:

  • 長期性能數(shù)據(jù)監(jiān)控工具
  • Unity Profiler
  • XCode和Instruments

長期性能數(shù)據(jù)監(jiān)控工具會至少每天都對游戲單局、或游戲資源進行自動化性能測試,并上報結(jié)果到服務器。能從“整體”去對比不同時段、不同版本間的性能差別。

游戲資源長期性能監(jiān)控工具報表

Unity Profiler能定量地找到C#的GC Alloc問題;其Timeline視圖也能從地整體(但不太定量)找到CPU瓶頸。
Unity Timeline Profiler

XCode的GPU Report視圖能從整體(但不太定量)找到游戲的瓶頸階段。當Frame Time中CPU大于GPU時,表示CPU是瓶頸,否則表示GPU是瓶頸;當Utilization中的TILER比RENDERER高時,表示頂點處理是GPU的瓶頸,否則表示像素處理是GPU的瓶頸。
幀率受限于瓶頸,應優(yōu)先優(yōu)化瓶頸階段,非瓶頸階段優(yōu)化得再快都無法提高幀率。
GPU Report

XCode的Capture GPU frame功能能高效且定量地定位到GPU中shader的消耗。

XCode Capture GPU frame

Instruments的TimeProfiler能高效且定量地定位C#腳本(IL2CPP后的C++代碼)的CPU占用,甚至包括部分Unity引擎代碼的CPU占用函數(shù)消耗,而不必麻煩地添加BeginSample()EndSample()
Instruments Time Profiler

Unity使用/機制優(yōu)化小結(jié)

GameObject的SpawnPool應支持“移出屏幕”功能

GameObject(比如特效)可能會被頻繁的在“使用中”、“不使用”的狀態(tài)間切換。我們的SpawnPool不應過快地把“剛剛不使用”的GameObject立刻Deactivate掉,否則會引起不必要的Deactivate/Activate的性能消耗。應有一個“從熱變冷”的過程:“剛剛不使用”只是移出屏幕;只有“不使用一段時間”的GameObject,才會得以Deactivate。可能的實現(xiàn)方式如下:

/// On each timer, we try to make parts of "hot" items to be "cool" by deactivating them.
internal void OnTimer()
{
    if(teleportCache.items.Count > 0) {
        if(Time.realtimeSinceStartup - teleportCache.lastSpawnTime < SpawnPool.TeleportThenDeactivateDuration &&
            teleportCache.items.Count <= 3) {
            this.MoreLogInfo("this prefab is recently spawned, and the teleport cache is not too large, we don't deactivate these remaining items");
        }
        else {
            int deactivateCount = Mathf.Max(1, teleportCache.items.Count / 3);

            SpawnIdentity oneId;
            while(deactivateCount > 0) {
                --deactivateCount;
                oneId = teleportCache.items.Pop();
                if(null != oneId) {
                    this.MoreLogInfo("Deactivate from teleportCache:" + oneId);
                    oneId.gameObject.MoreSetActive(false, this);
                    oneId.gameObject.transform.SetParent(null, true);
                    deactivateCache.Push(oneId);

                    SpawnPoolProfiler.AddDeactivateCount(oneId.gameObject);
                }
            }
        }
    }
}

Transform的孩子不應過多

當Transform包含不該有的孩子Transform或其他組件時,為該Transform進行position、rotation賦值,會引起消耗,特別是包含粒子系統(tǒng)的時候。


對Transform進行rotation賦值時,由于其孩子包含粒子系統(tǒng)所產(chǎn)生的消耗

但考慮到切換Transform的parent本身也會有消耗,因此,我們對此也應有“從熱變冷”的過程:“剛剛不使用”依然保留在父親Transform里;只有“不使用一段時間”的GameObject,才從父親Transform移出。

應減少粒子系統(tǒng)的Play()的調(diào)用次數(shù)

每次調(diào)用ParticleSystem.Play()都會有消耗,如果粒子系統(tǒng)本身沒有明顯“前搖”階段,應先檢查ParticleSystem.isPlaying,例子如下:

ParticleSystem ps;
for (int i = 0; i < num; ++i) {
    //m_particleSystemLst[i].Stop();
    ps = m_particleSystemLst[i];
    /// CAUTION! WE SHOULD CHECK isPlaying before calling Play()! OR, IT WILL AFFECT PERFORMANCE!
    if(!ps.isPlaying) {
        ps.Play();
    }
}

應減少每幀Material.GetXX()/Material.SetXX()的次數(shù)

每次調(diào)用Material.GetXX()Material.SetXX()都會有消耗,應減少調(diào)用該API的頻率。比如使用C#對象變量來記錄Material的變量狀態(tài),從而規(guī)避Material.GetXX();在Shader里把多個uniform half變量合并為uniform half 4,從而把4個Material.SetXX()調(diào)用合并為1個Material.SetXX()

應使用支持Conditional的日志輸出機制

簡單使用Debug.Log(ToString() + "hello " + "world");,其實參會造成CPU消耗及GC。使用支持Conditional的日志輸出機制,則無此問題,只需在構(gòu)建時,取消對應的編譯參數(shù)即可。

/// MoreDebug.cs,帶Conditional條件編譯的日志輸出機制
[Conditional("MORE_DEBUG_INFO")]
public static void MoreLogInfo(this object caller) { DoMoreLog(MoreLogLevel.Info, false, caller); }

/// 用戶代碼.cs,調(diào)用方簡單正常調(diào)用即可。正式構(gòu)建時,取消MORE_DEBUG_INFO編譯參數(shù)。
this.MoreLogInfo("writerSize=" + writer.Position, "channelId=" + channelId);

腳本優(yōu)化小結(jié)

依然需要減少GetComponent()的頻率

GetComponent()能成功找到組件,則無GC消耗,如找不到則會有一定的GC產(chǎn)生。有少量的CPU消耗。如有可能,我們依然需要規(guī)避冗余的GetComponent(),特別需要避免用其查找可能不存在的組件。另,自Unity5起,Unity已就.transform進行了cache,我們不需再為.transform擔心,見《UNITY 5: API CHANGES & AUTOMATIC SCRIPT UPDATING》最后一段。

應減少UnityEngine.Object的null比較

因為Unity overwrite掉了Object.Equals()《CUSTOM == OPERATOR, SHOULD WE KEEP IT?》也說過unityEngineObject==null事實上和GetComponent()的消耗類似,都涉及到Engine層面的機制調(diào)用,所以UnityEngine.Object的null比較,都會有少許的性能消耗。對于基礎功能、調(diào)用棧葉子節(jié)點邏輯、高頻功能,我們應少null比較,使用assertion來處理。只有在調(diào)用棧根節(jié)點邏輯,有必要的時候,才進行null比較。


上面C#代碼對應的IL2CPP代碼

而且,從代碼質(zhì)量來看,無腦的null保護也是不值得推崇的,因為其將錯誤隱藏到了更偏離錯誤根源的邏輯。理論上,當錯誤發(fā)生了,應盡早報錯,從而幫助開發(fā)者能更快速地定位錯誤根源。所以,多用assertion,少用null保護,無論是對代碼質(zhì)量,還是代碼性能,都是不錯的實踐。

應減少不必要的Transform.position/rotation等訪問

每次訪問Transform.position/rotation都有相應的消耗。應能cache就cache其返回結(jié)果。

應盡量減少創(chuàng)建C#堆內(nèi)存對象

建議使用成員變量,或者Pool來規(guī)避高頻創(chuàng)建C#堆內(nèi)存對象的創(chuàng)建。而且堆內(nèi)存對象創(chuàng)建本身就是個相對較慢的過程。

應為struct對象重載所有object函數(shù)

為了普適性,C#的struct的默認Equals()GetHashCode()ToString()都是較慢實現(xiàn),甚至涉及反射。用戶自定義的struct,都應重載上述3個函數(shù),手動實現(xiàn),比如:

public struct NetworkPredictId{
    int m_value;
    public override int GetHashCode(){
        return m_value;
    }

    public override bool Equals(object obj){
        return obj is NetworkPredictId && this == (NetworkPredictId)obj;
    }

    public override string ToString(){
        return m_value.ToString();
    }

    public static bool operator ==(NetworkPredictId c1, NetworkPredictId c2){
        return c1.m_value == c2.m_value;
    }

    public static bool operator !=(NetworkPredictId c1, NetworkPredictId c2){
        return c1.m_value != c2.m_value;
    }
}

如果可能,盡量用Queue/Stack來代替List

我們會習慣用List來實現(xiàn)數(shù)據(jù)集合的需求。但好一些情況下,我們事實上是不需對其進行隨機訪問,而僅僅是“增加”、“刪除”操作。此時,我們應該使用增刪復雜度都是O(1)的Queue或者Stack

注意List常用接口復雜度

Add()常為O(1)復雜度,但超過Capacity時,為O(n)復雜度。故我們應注意合理地設置容器的初始化Capacity。
Insert()為O(n)復雜度。
Remove()為O(n)復雜度。RemoveAt(index)為O(n)復雜度,n=(Count - index)。故建議移除時應優(yōu)先從尾部移除。當批量移除時,miloyip亦指出RemoveRange提高移除效率。

/// remove not exsiting items in O(n)
int oldCount = m_items.Count;
int newCount = 0;
Item oneItem;
for(int i = 0; i < oldCount; ++i){
    oneItem = m_items[i];
    if(CheckExisting(oneItem)){
        m_items[newCount] = oneItem;
        ++newCount;
    }
}
m_items.RemoveRange(newCount, oldCount - newCount);

應注意容器的初始化capacity

同理如上條目。另,Capacity增長時,除了O(n)的復雜度,也有GC消耗。

應盡量為類或函數(shù)聲明為sealed

IL2CPP就sealed的類或函數(shù)會有優(yōu)化,變虛函數(shù)調(diào)用為直接函數(shù)調(diào)用。詳見《IL2CPP OPTIMIZATIONS: DEVIRTUALIZATION》

C#/CPP interop時,不需為blittable的變量聲明為MarshalAs

某些數(shù)值類型,托管代碼和原生代碼的二進制表達方式一致,這些稱為blittable數(shù)值類型。blittable數(shù)值類型在interop時為高效的簡單內(nèi)存拷貝,故應值得推崇。C#中的blittable數(shù)值類型為byteintfloat等,但注意不包括常用的boolstring。僅有blittable數(shù)值類型組成的數(shù)組或struct,也為blittable。
blittable的變量不應聲明MarshalAs
比如下面代碼,

[DllImport(ApolloCommon.PluginName, CallingConvention = CallingConvention.Cdecl)]
private static extern ApolloResult apollo_connector_readUdpData(UInt64 objId, /*[MarshalAs(UnmanagedType.LPArray)]*/ byte[] buff, ref int size);

注釋前后的IL2CPP代碼如下圖,右側(cè)明顯避免了marhal的產(chǎn)生。


詳見《IL2CPP INTERNALS: P/INVOKE WRAPPERS》

減少Dictionary的冗余訪問

我們常習慣編寫這樣的代碼:

if(myDictionary.Contains(oneKey))
{
    MyValue myValue = myDictionary[oneKey];
   // ...
}

但其可減少冗余的哈希次數(shù),優(yōu)化為:

MyValue myValue;
if(myDictionary.TryGetValue(oneKey, out myValue))
{
    // ...
}

(TO BE CONTINUED...)

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內(nèi)容