Unity全面入門筆記15-生命週期

幀與時間

  • 幀長

在Unity中,一幀是執行兩次渲染的間隔時間,Unity會在兩次渲染間進行邏輯運算。

默認情況下,幀率是50幀每秒,也就是每0.02秒一幀。一幀的幀長還可以在主界面菜單欄的"Edit"->“Project Settings”->“Time”->"Fixed Timestep"進行修改。

幀的存在相當於給了遊戲邏輯一個硬性的規定:要想保證遊戲的流暢,每一個幀循環的時間內允許進行的計算是有限的。如果在一幀中的計算量過高,我們必須對這個計算進行處理,將其簡化或使其延續在多幀內執行,否則這一幀就會產生卡頓。如果連續多幀的計算量都很高,我們必須對遊戲邏輯算法進行優化,否則遊戲的運行會極大的受影響。

在腳本中可以使用Time.fixedDeltaTime來訪問或修改這個數值。

public static Time.fixedDeltaTime{get;set;}

生命週期

  • 介紹生命週期

對每一個組件,從它被實例化到被銷燬,遵循一定的生命週期:

生命週期圖

生命週期由兩重循環組成:外層的激活循環和內層的幀循環。通過自定義生命週期中不同部分的運作邏輯,我們可以實現多樣的效果,這就是通過Unity編程的核心。

  • 激活循環

一個組件的初始化有三種情況:

附加在佈置在場景中的GameObject上的組件,在場景被加載時與GameObject一起被初始化。

被附加在Prefab上,在使用Instantiate動態實例化這個Prefab時被初始化。

被使用AddComponent動態附加到GameObject上時被初始化。

在組件的初始化過程中會經歷Awake->OnEnable->Start三個生命週期過程。

一個組件的銷燬有三種情況:

組件被Destroy時被銷燬。

組件所附加的GameObject被Destroy時被銷燬。

退出當前場景時被銷燬。

在組件銷燬過程中會經歷OnDisable->OnDestroy兩個生命週期過程。

當將一個GameObject的activeSelf設置爲false時,其上的Component也會進入OnDisable狀態並等待,但此時Component並沒有被銷燬,重新將activeSelf設置爲true又會重新進入OnEnable狀態,這樣進入OnEnable狀態後組件會跳過Start階段直接進入後面的幀循環。

一個組件的Start階段在整個生命週期中有且只有一次,且這一次一定發生在OnEnable之後。Awake階段也有且只有一次,且這一次一定發生在OnEnable之前。

所以說,如果一個組件或它附着的GameObject在遊戲開始前就是不激活的,或者我們在Awake中將它設置爲不激活的,那麼在遊戲開始時這個組件只會進入Awake而不會進入Start階段,直到它被其它腳本動態激活。

除了直接修改組件所在GameObject的activeSelf屬性外,組件本身的活性也可以單獨修改。

public static Component.enabled{get; set;}

修改組件的enabled屬性會導致組件不再進行幀循環,對於每個組件來說,enabled被修改後的經歷的階段和修改activeSelf一致。

組件的銷燬或失活並非在使用銷燬或失活的語句中進行的,如調用Destroy(this)後,Unity底層會將組件加入待銷燬列表,等到經歷完剩餘的幀週期後,才進入OnDisable和OnDestroy過程,這時纔會正式將這個對象移除,從此之後的邏輯纔會停止運算。

  • 幀循環

在幀循環中,又可以分成物理層、輸入層、邏輯層和渲染層。

物理層包括FixedUpdate和碰撞、觸發事件。物理層的時間概念和邏輯層是不同的,在物理層中具有一個用於平衡物理時間和邏輯時間的子循環。

輸入層包括鼠標按下、拖動、進入、離開、擡起等事件,注意輸入層事件和Input類中的事件遵循兩套流程。輸入層的具體內容我們會在未來和Input類一起講解。

邏輯層從Update函數開始,再經歷協程的一系列調用,在LateUpdate結束。

渲染層會先渲染遊戲中的物體,然後在OnGUI中渲染UI物體。

Update和LateUpdate遵循邏輯時間,在每一幀中執行且只執行一次,LateUpdate的執行總在Update之後。

由於Unity框架遍歷各個GameObject的順序是開發者無法預計的,所以如果我們需要使兩個組件在每一幀都執行邏輯,但想讓一個組件的邏輯總在另一個之後,我們可以讓前者使用LateUpdate。

物理時間的一幀和邏輯時間的一幀不同,邏輯時間的一幀中FixedUpdate不一定執行一次,它可能執行很多次,也有可能不執行。物理時間的運算遵循以下原則:

在每一邏輯幀結束前計算物理時間,使其增加Time.deltaTime * Time.timeScale。物理時間的值在腳本中可以由Time.time獲得:

public static float Time.time{get; private set;}
public static float Time.deltaTime{get; private set;}
public static float Time.timeScale{get; private set;}

Time.deltaTime是上一幀經過的實際時間,如果上一幀運算量較少,沒有達到一幀的額定時間,那麼在補幀後Time.deltaTime恰好等於Time.fixedDeltaTime。如果上一幀產生了卡頓,那麼這個數值就會超過Time.fixedDeltaTime。

Time.timeScale是物理時間倍率,改變這個數值可以改變Time.time增長的速度。令Time.timeScale等於0時,Time.time就會停止增長,這也會導致遊戲的物理層循環暫停。

每當物理時間經過fixedDeltaTime,就會進行一次物理循環,這就導致物理循環在一邏輯幀不止進行一次。

  • 暫停

由於物理循環和FixedUpdate事件的這種特性,我們常利用Time.timeScale來實現暫停效果:

public class GameManager : MonoBehaviour
{
	private void Update()
	{
		if(Input.GetKeyDown(KeyCode.Space))
		{
			Time.timeScale = Time.timeScale == 0 ? 1 : 0;
		}
	}
}

我們在Update而非FixedUpdate中檢測鍵盤輸入並暫停遊戲,是因爲Update不在物理層循環中,不會因爲遊戲暫停而停止接受用於恢復暫停的輸入信息。

根據Time.timeScale隻影響物理層循環的特性可以得知,切換Time.timeScale的值具有以下特徵:

timeScale不影響Update和LateUpdate,影響FixedUpdate。

timeScale不影響Time.realtimeSinceStartup,會影響Time.timeSinceLevelLoad和Time.time。

timeScale不影響Time.fixedDeltaTime和Time.unscaleDeltaTime。

timeScale影響異步方法,包括Invoke和Coroutine。

timeScale也影響所有的動畫和粒子特效。

deltaTime的獲取是基於time的,所以在FixedUpdate中調用deltaTime的值是不會隨timeScale改變的,而Update和LateUpdate中反而會受timeScale影響。

藉此我們也總結出關於遊戲暫停的規律:

將UI等不希望被暫停的邏輯放在Update或LateUpdate,將希望被暫停的邏輯放在FixedUpdate。

對不想被暫停的特效需要做特殊處理。

雖然timeScale不能影響Update和LateUpdate的執行速率,但是如果開發者想要改變它們的速率,可以修改Time.captureFramerate這個屬性來修改每秒的幀率(默認爲50):

uint Time.captureFramerate{get; set;}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章