溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Unity3D優(yōu)化總結(jié)篇

發(fā)布時間:2020-08-29 06:49:31 來源:網(wǎng)絡(luò) 閱讀:249 作者:GuangYao_Li 欄目:游戲開發(fā)

此總結(jié)由自己經(jīng)驗及網(wǎng)上收集整理優(yōu)化內(nèi)容 包括:
1.代碼方面;
2.函數(shù)使用方面;
3.ngui注意方面;
4.數(shù)學(xué)運算方面;
5.內(nèi)存方面;
6.垃圾回收方面 等等...

總結(jié)如下:

  1. 盡量避免每幀處理,可以每隔幾幀處理一次比如:

function Update()

{

DoSomeThing();

}

可改為每5幀處理一次:

function Update()

{

   if(Time.frameCount % 5 == 0)

   {

       DoSomeThing();

    }

}

  1. 定時重復(fù)處理用InvokeRepeating 函數(shù)實現(xiàn)
    比如,啟動0.5秒后每隔1秒執(zhí)行一次 DoSomeThing 函數(shù):

function Start()

{

    InvokeRepeating("DoSomeThing", 0.5, 1.0);

}
CancelInvoke("你調(diào)用的方法"); 停止InvokeRepeating

  1. 優(yōu)化 Update,FixedUpdate, LateUpdate 等每幀處理的函數(shù),函數(shù)里面的變量盡量在頭部聲明。

比如:

function Update()

{

var pos: Vector3 = transform.position;

}

可改為

private var pos: Vector3;

function Update()

{

   pos = transform.position;

}

  1. 主動回收垃圾給某個 GameObject 綁上以下的代碼:

function Update()

{

   if(Time.frameCount % 50 == 0)

  {

           System.GC.Collect();

   }

}

  1. 運行時盡量減少 Tris 和 Draw Calls
    預(yù)覽的時候,可點開 Stats,查看圖形渲染的開銷情況。特別注意 Tris 和 Draw Calls 這兩個參數(shù)。一般來說,要做到:
    Tris 保持在 7.5k 以下
    Draw Calls 保持在 35 以下

  2. 壓縮 Mesh
    導(dǎo)入 3D 模型之后,在不影響顯示效果的前提下,最好打開 Mesh Compression。
    Off, Low, Medium, High 這幾個選項,可酌情選取。對于單個Mesh最好使用一個材質(zhì)。

  3. 避免大量使用 Unity 自帶的 Sphere 等內(nèi)建 Mesh
    Unity 內(nèi)建的 Mesh,多邊形的數(shù)量比較大,如果物體不要求特別圓滑,可導(dǎo)入其他的簡單3D模型代替。

  4. 優(yōu)化數(shù)學(xué)計算盡量避免使用float,而使用int,特別是在手機游戲中,盡量少用復(fù)雜的數(shù)學(xué)函數(shù),比如sin,cos等函數(shù)。改除法/為乘法,例如:使用x*0.5f而不是 x/2.0f 。

9.如果你做了一個圖集是1024X1024的。此時你的界面上只用了圖集中的一張很小的圖,那么很抱歉1024X1024這張大圖都需要載入你的內(nèi)存里面,1024就是4M的內(nèi)存,如果你做了10個1024的圖集,你的界面上剛好都只用了每個圖集里面的一張小圖,那么再次抱歉你的內(nèi)存直接飆40M。意思是任何一個4096的圖片,不管是圖集還是texture,他都占用4*4=16M?

=====================分割線=====================================================
1、在使用數(shù)組或ArrayList對象時應(yīng)當(dāng)注意

length=myArray.Length;
for(int i=0;i<length;i++)
{

}

for(int i=0;i<myArray.Length;i++)
{

}

2、少使用臨時變量,特別是在Update OnGUI等實時調(diào)用的函數(shù)中。

void Update()
{
Vector3 pos;
pos=transform.position;
}
可以改為:

private Vector3 pos;
void Update()
{
pos=transform.position;
}

3、如果可能,將GameObject上不必要的腳本disable掉。
如果你有一個大的場景在你的游戲中,并且敵方的位置在數(shù)千米意外,
這時你可以disable你的敵方AI腳本直到它們接近攝像機為止。
一個好的途徑來開啟或關(guān)閉GameObject是使用SetActiveRecursively(false),并且球形或盒型碰撞器設(shè)為trigger。

4、刪除空的Update方法。
當(dāng)通過Assets目錄創(chuàng)建新的腳本時,腳本里會包括一個Update方法,當(dāng)你不使用時刪除它。

5、引用一個游戲?qū)ο蟮淖詈虾踹壿嫷慕M件。
有人可能會這樣寫someGameObject.transform,gameObject.rigidbody.transform.gameObject.rigidbody.transform,但是這樣做了一些不必要的工作,你可以在最開始的地方引用它,像這樣:
private Transform myTrans;
void Start()
{
myTrans=transform;
}

6、協(xié)同是一個好方法。
可以使用協(xié)同程序來代替不必每幀都執(zhí)行的方法。(還有InvokeRepeating方法也是一個好的取代Update的方法)。

7、盡可能不要再Update或FixedUpdate中使用搜索方法(例如GameObject.Find()),你可以像前面那樣在Start方法里獲得它。

8、不要使用SendMessage之類的方法,他比直接調(diào)用方法慢了100倍,你可以直接調(diào)用或通過C#的委托來實現(xiàn)。

9、使用javascript或Boo語言時,你最好確定變量的類型,不要使用動態(tài)類型,這樣會降低效率,
你可以在腳本開頭使用#pragmastrict 來檢查,這樣當(dāng)你編譯你的游戲時就不會出現(xiàn)莫名其妙的錯誤了。

==================================================分割線=====================================================

1、頂點性能
一般來說,如果您想在iPhone 3GS或更新的設(shè)備上每幀渲染不超過40,000可見點,那么對于一些配備 MBX GPU的舊設(shè)備(比如,原始的 iPhone,如 iPhone 3g和 iPod Touch第1和第2代)來說,你應(yīng)該保證每幀的渲染頂點在10000以下。

2、光照性能
像素的動態(tài)光照將對每個受影響的像素增加顯著的計算開銷,并可能導(dǎo)致物體會被渲染多次。為了避免這種情況的發(fā)生,您應(yīng)該避免對于任何單個物體都使用多個像素光照,并盡可能地使用方向光。
需要注意的是像素光源是一個渲染模式(Render Mode)設(shè)置為重要(Important)的光源。
像素的動態(tài)光照將對頂點變換增加顯著的開銷。所以,應(yīng)該盡量避免任何給定的物體被多個光源同時照亮的情況。
對于靜態(tài)物體,采用烘焙光照方法則是更為有效的方法。

3、角色
每個角色盡量使用一個Skinned Mesh Renderer,這是因為當(dāng)角色僅有一個 Skinned Mesh Renderer 時,
Unity 會使用可見性裁剪和包圍體更新的方法來優(yōu)化角色的運動,而這種優(yōu)化只有在角色僅含有一個 Skinned Mesh Renderer時才會啟動。
角色的面數(shù)一般不要超過1500,骨骼數(shù)量少于30就好,角色Material數(shù)量一般1~2個為最佳。

4、靜態(tài)物體
對于靜態(tài)物體定點數(shù)要求少于500,UV的取值范圍不要超過(0,1)區(qū)間,這對于紋理的拼合優(yōu)化很有幫助。不要在靜態(tài)物體上附加Animation組件,雖然加了對結(jié)果沒什么影響,但是會增加CPU開銷。

5、攝像機
將遠(yuǎn)平面設(shè)置成合適的距離,遠(yuǎn)平面過大會將一些不必要的物體加入渲染,降低效率。另外我們可以根據(jù)不同的物體來設(shè)置攝像機的遠(yuǎn)裁剪平面。Unity 提供了可以根據(jù)不同的 layer 來設(shè)置不同的 view distance ,
所以我們可以實現(xiàn)將物體進行分層,大物體層設(shè)置的可視距離大些,而小物體層可以設(shè)置地小些,
另外,一些開銷比較大的實體(如粒子系統(tǒng))可以設(shè)置得更小些等等。

6、DrawCall
盡可能地減少 Drawcall 的數(shù)量。 IOS 設(shè)備上建議不超過 100 。
減少的方法主要有如下幾種: Frustum Culling ,Occlusion Culling , Texture Packing 。 Frustum Culling 是 Unity 內(nèi)建的,我們需要做的就是尋求一個合適的遠(yuǎn)裁剪平面;
Occlusion Culling ,遮擋剔除, Unity 內(nèi)嵌了 Umbra ,一個非常好 OC 庫。
但 Occlusion Culling 也并不是放之四海而皆準(zhǔn)的,有時候進行 OC 反而比不進行還要慢,
建議在 OC 之前先確定自己的場景是否適合利用 OC 來優(yōu)化; Texture Packing ,或者叫 Texture Atlasing ,
是將同種 shader 的紋理進行拼合,根據(jù) Unity 的 static batching 的特性來減少 draw call 。
建議使用,但也有弊端,那就是一定要將場景中距離相近的實體紋理進行拼合,否則,拼合后很可能會增加每幀渲染所需的紋理大小,
加大內(nèi)存帶寬的負(fù)擔(dān)。這也就是為什么會出現(xiàn)“ DrawCall 降了,渲染速度也變慢了”的原因。

7.粒子系統(tǒng)運行在iPhone上時很慢,怎么辦?
答:iPhone擁有相對較低的fillrate 。
如果您的粒子效果覆蓋大部分的屏幕,而且是multiple layers的,這樣即使最簡單的shader,也能讓iPhone傻眼。
我們建議把您的粒子效果baking成紋理序列圖。
然后在運行時可以使用1-2個粒子,通過動畫紋理來顯示它們。這種方式可以取得很好的效果,以最小的代價。

===========================================分割線==============================

1.操作transform.localPosition的時候請小心
移動GameObject是非常平常的一件事情,以下代碼看起來很簡單:
transform.localPosition += new Vector3 ( 10.0f * Time.deltaTime, 0.0f, 0.0f );

但是小心了,假設(shè)上面這個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函數(shù),
所以正確的做法應(yīng)該是:
transform.Translate ( 10.0f * Time.deltaTime, 0.0f, 0.0f );

曝出在Inspector的變量同樣的也能被Animation View Editor所使用
有時候我們會想用Unity3D自帶的Animation View Editor來做一些簡單的動畫操作。而Animation Editor不僅可以操作Unity3D自身的component,
還可以操作我們自定義的MonoBehavior中的各個Property。所以加入 你有個float值需要用曲線操作,你可以簡單的將它曝出到成可以serialize的類型,如:
public float foobar = 1.0f;
這樣,這個變量不僅會在Inspector中出現(xiàn),還可以在animation view中進行操作,生成AnimationClip供我們通過AnimationComponent調(diào)用。
范例:
public class TestCurve : MonoBehaviour
{
public float foobar = 0.0f;
IEnumerator Start ()
{
yield return new WaitForSeconds (2.0f);
animation.Play("foobar_op");
InvokeRepeating ( "LogFoobar", 0.0f, 0.2f );
yield return new WaitForSeconds (animation["foobar_op"].length);
CancelInvoke ("LogFoobar");
}
void LogFoobar ()
{
Debug.Log("foobar = " + foobar);

   }

}

2.GetComopnent<T> 可以取父類類型
Unity3D 允許我們對MonoBehavior做派生,所以你可能有以下代碼:

public class foo : MonoBehaviour { ...}

public class bar : foo { ...}
假設(shè)我們現(xiàn)在有A,B兩個GameObject, A包含foo, B包含bar, 當(dāng)我們寫
foo comp1 = A.GetComponent<foo>();

bar comp2 = B.GetComponent<bar>();

可以看到comp1, comp2都得到了應(yīng)得的Component。那如果我們對B的操作改成:
foo comp2 = B.GetComponent<foo>();

答案是comp2還是會返回bar Component并且轉(zhuǎn)換為foo類型。你同樣可以用向下轉(zhuǎn)換得到有效變量:
bar comp2_bar = comp2 as bar;
合理利用GetComponent<base_type>()可以讓我們設(shè)計Component的時候耦合性更低。

3.Invoke, yield 等函數(shù)會受 Time.timeScale 影響
Unity3D提供了一個十分方便的調(diào)節(jié)時間的函數(shù)Time.timeScale。對于初次使用Unity3D的使用者,會誤導(dǎo)性的認(rèn)為Time.timeScale同樣可以適用于游戲中的暫停(Pause)和開始(Resume)。
所以很多人有習(xí)慣寫:
Time.timeScale = 0.0f;

對于游戲的暫停/開始,是游戲系統(tǒng)設(shè)計的一部分,而Time.timeScale不不是用于這個部分的操作。
正確的做法應(yīng)該是搜集需要暫停的腳本或 GameObject,
通過設(shè)置他們的enabled = false 來停止他們的腳本活動或者通過特定函數(shù)來設(shè)置這些物件暫停時需要關(guān)閉那些操作。

Time.timeScale 更多的是用于游戲中慢鏡頭的播放等操作,在服務(wù)器端主導(dǎo)的游戲中更應(yīng)該避免此類操作。
值得一提的是,Unity3D的許多時間相關(guān)的函數(shù)都和 timeScale掛鉤,而timeScale = 0.0f將使這些函數(shù)或動畫處于完全停止的狀態(tài),這也是為什么它不適合做暫停操作的主要原因。

這里列出受timeScale影響的一些主要函數(shù)和Component:
MonoBehaviour.Invoke(…)
MonoBehaviour.InvokeRepeating(…)
yield WaitForSeconds(…)
GameObject.Destroy(…)
Animation Component
Time.time, Time.deltaTime

4.Coroutine 和 IEnumerator的關(guān)系
初寫Unity3D C#腳本的時候,我們經(jīng)常會犯的錯誤是調(diào)用Coroutine函數(shù)忘記使用StartCoroutine的方式。如:

IEnumerator CoLog ()

{

   yield return new WaitForSeconds (2.0f);

   Debug.Log("hello foobar");

}

當(dāng)我們用以下代碼去調(diào)用上述函數(shù):
TestCoroutine testCo = GetComponent<TestCoroutine>();

testCo.CoLog ();

testCo.StartCoroutine ( "CoLog" );
那么testCo.CoLog()的調(diào)用將不會起任何作用。

5.StartCoroutine, InvokeRepeating 和其調(diào)用者關(guān)聯(lián)
通常我們只在一份GameObject中去調(diào)用StartCoroutine或者InvokeRepeating,我們寫:
StartCoroutine ( Foobar() );

InvokeRepeating ( "Foobar", 0.0f, 0.1f );

所以如果這個GameObject被disable或者destroy了,這些coroutine和invokes將會被取消。就好比我們手動調(diào)用:
StopAllCoroutines ();

CancelInvoke ();

這看上去很美妙,對于AI來說,這就像告訴一個NPC你已經(jīng)死了,你自己的那些小動作就都聽下來吧。

但是注意了,假如這樣的代碼用在了一個Manager類型的控制AI上,他有可能去控制其他的AI, 也有可能通過Invoke, Coroutine去做一些微線程的操作,這個時候就要明確StartCoroutine或者InvokeRepeating的調(diào)用者的設(shè)計。討論之前我 們先要理解,StartCoroutine或InvokeRepeating的調(diào)用會在該MonoBehavior中開啟一份Thread State, 并將需要操作的函數(shù),變量以及計時器放入這份Stack中通過并在引擎每幀Update的最后,Renderer渲染之前統(tǒng)一做處理。所以如果這個 MonoBehavior被Destroy了,那么這份Thread State也就隨之消失,那么所有他存儲的調(diào)用也就失效了。

如果有兩份GameObject A和B, 他們互相知道對方,假如A中通過StartCoroutine或InvokeRepeating去調(diào)用B的函數(shù)從而控制B,這個時候Thread State是存放在A里,當(dāng)A被disable或者destroy了,這些可能需要一段時間的控制函數(shù)也就失效了,這個時候B明明還沒死,也不會動了。更 好的做法是讓在A的函數(shù)中通過B.StartCoroutine ( … ) 讓這份Thread State存放于B中。

// class TestCortouine
public class TestCoroutine : MonoBehaviour
{
public IEnumerator CoLog ( string _name )
{
Debug.Log(_name + " hello foobar 01");
yield return new WaitForSeconds (2.0f);
Debug.Log(_name + " hello foobar 02"); }}
// component attached on GameObject A
public class A: MonoBehaviour
{
public GameObject B;

       void Start ()
         {

               TestCoroutine compB = B.GetComponent<TestCoroutine>();
                 // GOOD, thread state in B 
                 // same as: comp
                 B.StartCoroutine ( "CoLog", "B" ); 
                 compB.StartCoroutine ( compB.CoLog("B") );
                 // BAD, thread state in A 
                 StartCoroutine ( compB.CoLog("A") ); 
                 Debug.Log("Bye bye A, we'll miss you"); 
                 Destroy(gameObject); 
                 / / T_T I don't want to die...

       }

}

以上代碼,得到的結(jié)果將會是:
B hello foobar 01

A hello foobar 01

Bye bye A, we'll miss you

B hello foobar 02

6.如不需要Start, Update, LateUpdate函數(shù),請去掉他們
當(dāng)你的腳本里沒有任何Start, Update, LateUpdate函數(shù)的時候,Unity3D將不會將它們加入到他的Update List中,有利于腳本整體效率的提升。
們可以從這兩個腳本中看到區(qū)別:

Update_01.cs
public class Update_01 : MonoBehaviour

{

void Start () {}

void Update () {}

}

Update_02.cs
public class Update_02 : MonoBehaviour

{
}

===========================================分割線==============
1.減少固定增量時間
將固定增量時間值設(shè)定在0.04-0.067區(qū)間(即,每秒15-25幀)。您可以通過Edit->Project Settings->Time來改變這個值。這樣做降低了FixedUpdate函數(shù)被調(diào)用的頻率以及物理引擎執(zhí)行碰撞檢測與剛體更新的頻率。如果您使用了較低的固定增量時間,并且在主角身上使用了剛體部件,那么您可以啟用插值辦法來平滑剛體組件。

2.減少GetComponent的調(diào)用使用 GetComponent或內(nèi)置組件訪問器會產(chǎn)生明顯的開銷。您可以通過一次獲取組件的引用來避免開銷,并將該引用分配給一個變量(有時稱為"緩存"的引用)。
例如,如果您使用如下的代碼:
void Update ()
{
transform.Translate(0, 1, 0);
}

通過下面的更改您將獲得更好的性能:
Transform myTransform = null;
void Awake ()

{
myTransform = transform;
}

void Update ()

{
myTransform.Translate(0, 1, 0);
}

3.避免分配內(nèi)存
您應(yīng)該避免分配新對象,除非你真的需要,因為他們不再在使用時,會增加垃圾回收系統(tǒng)的開銷。
您可以經(jīng)常重復(fù)使用數(shù)組和其他對象,而不是分配新的數(shù)組或?qū)ο?。這樣做好處則是盡量減少垃圾的回收工作。
同時,在某些可能的情況下,您也可以使用結(jié)構(gòu)(struct)來代替類(class)。
這是因為,結(jié)構(gòu)變量主要存放在棧區(qū)而非堆區(qū)。因為棧的分配較快,并且不調(diào)用垃圾回收操作,所以當(dāng)結(jié)構(gòu)變量比較小時可以提升程序的運行性能。
但是當(dāng)結(jié)構(gòu)體較大時,雖然它仍可避免分配/回收的開銷,而它由于"傳值"操作也會導(dǎo)致單獨的開銷,實際上它可能比等效對象類的效率還要低。

4.最小化GUI
使用GUILayout 函數(shù)可以很方便地將GUI元素進行自動布局。然而,這種自動化自然也附帶著一定的處理開銷。
您可以通過手動的GUI功能布局來避免這種開銷。
此外,您也可以設(shè)置一個腳本的useGUILayout變量為 false來完全禁用GUI布局:
void Awake ()

{
useGUILayout = false;
}

5.使用iOS腳本調(diào)用優(yōu)化功能
UnityEngine 命名空間中的函數(shù)的大多數(shù)是在 C/c + +中實現(xiàn)的。
從Mono的腳本調(diào)用 C/C++函數(shù)也存在著一定的性能開銷。
您可以使用iOS腳本調(diào)用優(yōu)化功能(菜單:Edit->Project Settings->Player)讓每幀節(jié)省1-4毫秒。
此設(shè)置的選項有:
Slow and Safe – Mono內(nèi)部默認(rèn)的處理異常的調(diào)用
Fast and Exceptions Unsupported –一個快速執(zhí)行的Mono內(nèi)部調(diào)用。
不過,它并不支持異常,因此應(yīng)謹(jǐn)慎使用。
它對于不需要顯式地處理異常(也不需要對異常進行處理)的應(yīng)用程序來說,是一個理想的候選項。

6.優(yōu)化垃圾回收
如上文所述,您應(yīng)該盡量避免分配操作。
但是,考慮到它們是不能完全杜絕的,所以我們提供兩種方法來讓您盡量減少它們在游戲運行時的使用:
如果堆比較小,則進行快速而頻繁的垃圾回收

這一策略比較適合運行時間較長的游戲,其中幀率是否平滑過渡是主要的考慮因素。像這樣的游戲通常會頻繁地分配小塊內(nèi)存,但這些小塊內(nèi)存只是暫時地被使用。如果在iOS系統(tǒng)上使用該策略,那么一個典型的堆大小是大約 200 KB,這樣在iPhone 3G設(shè)備上,垃圾回收操作將耗時大約 5毫秒。如果堆大小增加到1 MB時,該回收操作將耗時大約 7ms。
因此,在普通幀的間隔期進行垃圾回收有時候是一個不錯的選擇。
通常,這種做法會讓回收操作執(zhí)行的更加頻繁(有些回收操作并不是嚴(yán)格必須進行的),但它們可以快速處理并且對游戲的影響很小:
if (Time.frameCount % 30 == 0)
{
System.GC.Collect();
}

但是,您應(yīng)該小心地使用這種技術(shù),并且通過檢查Profiler來確保這種操作確實可以降低您游戲的垃圾回收時間
如果堆比較大,則進行緩慢且不頻繁的垃圾回收

這一策略適合于那些內(nèi)存分配 (和回收)相對不頻繁,并且可以在游戲停頓期間進行處理的游戲。
如果堆足夠大,但還沒有大到被系統(tǒng)關(guān)掉的話,這種方法是比較適用的。
但是,Mono運行時會盡可能地避免堆的自動擴大。
因此,您需要通過在啟動過程中預(yù)分配一些空間來手動擴展堆(ie,你實例化一個純粹影響內(nèi)存管理器分配的"無用"對象):

void 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 (int i = 0; i < 1024; i++)

   {
         tmp = new byte[1024];
         // release reference
         tmp = null;

}
游戲中的暫停是用來對堆內(nèi)存進行回收,而一個足夠大的堆應(yīng)該不會在游戲的暫停與暫停之間被完全占滿。所以,當(dāng)這種游戲暫停發(fā)生時,您可以顯式請求一次垃圾回收:
System.GC.Collect();
另外,您應(yīng)該謹(jǐn)慎地使用這一策略并時刻關(guān)注Profiler的統(tǒng)計結(jié)果,而不是假定它已經(jīng)達(dá)到了您想要的效果。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI