1. 程式人生 > >MonoBehaviour 與 GameObject 的關係

MonoBehaviour 與 GameObject 的關係

一、MonoBehaviour 的生命週期

MonoBehaviour 是 Unity 中所有指令碼的基類,如果你使用JS的話,指令碼會自動繼承MonoBehaviour。如果使用C#的話,你需要顯式繼承MonoBehaviour。

在我們使用MonoBehaviour的時候,尤其需要注意的是它有哪些可重寫函式,這些可重寫函式會在遊戲中發生某些事件的時候被呼叫。我們在Unity中最常用到的幾個可重寫函式是這幾個:

  • Awake:當一個指令碼被例項化時,Awake 被呼叫。我們大多在這個類中完成成員變數的初始化。

  • Start:僅在 Update 函式第一次被呼叫前呼叫。因為它是在 Awake 之後被呼叫的,我們可以把一些需要依賴 Awake 的變數放在Start裡面初始化。 同時我們還大多在這個類中執行 StartCoroutine 進行一些協程的觸發。要注意在用C#寫指令碼時,必須使用 StartCoroutine 開始一個協程,但是如果使用的是 JavaScript,則不需要這麼做。

  • Update:當開始播放遊戲幀時(此時,GameObject 已例項化完畢),其 Update 在 每一幀 被呼叫。

  • LateUpdate:LateUpdate 是在所有 Update 函式呼叫後被呼叫。

  • FixedUpdate:當 MonoBehaviour 啟用時,其 FixedUpdate 在每一固定幀被呼叫。

  • OnEnable:當物件變為可用或啟用狀態時此函式被呼叫。

  • OnDisable:當物件變為不可用或非啟用狀態時此函式被呼叫。

  • OnDestroy:當 MonoBehaviour 將被銷燬時,這個函式被呼叫。

下面用一張圖來更形象地說明一下這幾個類的在MonoBehaviour的生命週期中是如何被呼叫的:


這裡寫圖片描述 

二、MonoBehaviour 的那些坑

  • 私有(private)和保護(protected)變數只能在專家模式中顯示。屬性不被序列化或顯示在檢視面板.

  • 不要使用名稱空間(namespace)

  • 記得使用 快取元件查詢, 即在MonoBehaviour的長遠方法中經常被訪問的元件最好在把它當作一個私有成員變數儲存起來。

  • 在遊戲裡經常出現需要檢測敵人和我方距離的問題,這時如果要尋找所有的敵人,顯然要消耗的運算量太大了,所以最好的辦法是將攻擊範圍使用Collider表示,然後將Collider的isTrigger設定為True。最後使用OnTriggerEnter來做攻擊範圍內的距離檢測,這樣會極大提升程式效能。

三、Monobehaviour 常用方法

不可重寫函式:

  • Invoke

    function Invoke (methodName : string, time : float) : void 
    在 time 秒之後,呼叫 methodName 方法;

    
    public class example : MonoBehaviour {
    public Rigidbody projectile;
    void LaunchProjectile() {
        Rigidbody instance = Instantiate(projectile);
        instance.velocity = Random.insideUnitSphere * 5;
    }
    public void Awake() {
        Invoke("LaunchProjectile", 2);
    }
    }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
  • InvokeRepeating

    function InvokeRepeating (methodName : string, time : float, repeatRate : float) : void 
    從第一次呼叫開始,每隔repeatRate時間呼叫一次.

  • CancelInvoke

    function CancelInvoke () : void 
    取消這個MonoBehaviour上的所有呼叫Invoke。

  • IsInvoking

    function IsInvoking (methodName : string) : bool 
    某指定函式是否在等候呼叫。

  • StartCoroutine

    function StartCoroutine (routine : IEnumerator) : Coroutine 
    一個協同程式在執行過程中,可以在任意位置使用 yield 語句。yield 的返回值控制何時恢復協同程式向下執行。協同程式在物件自有幀執行過程中堪稱優秀。協同程式在效能上沒有更多的開銷。StartCoroutine函式是立刻返回的,但是yield可以延遲結果。直到協同程式執行完畢。

  • StopCoroutine / StopAllCoroutines

可重寫函式:

  • Update

    當 MonoBehaviour 例項化完成之後,Update 在每一幀被呼叫。

  • LateUpdate

    LateUpdate 是在所有 Update 函式呼叫後被呼叫。這可用於調整指令碼執行順序。例如:當物體在Update裡移動時,跟隨物體的相機可以在LateUpdate裡實現。

  • FixedUpdate

    處理 Rigidbody 時,需要用FixedUpdate代替Update。例如:給剛體加一個作用力時,你必須應用作用力在FixedUpdate裡的固定幀,而不是Update中的幀。(兩者幀長不同)

  • Awake

    Awake 用於在遊戲開始之前初始化變數或遊戲狀態。在指令碼整個生命週期內它僅被呼叫一次。Awake 在所有物件被初始化之後呼叫,所以你可以安全的與其他物件對話或用諸如 GameObject.FindWithTag 這樣的函式搜尋它們。每個遊戲物體上的Awke以隨機的順序被呼叫。因此,你應該用Awake來設定指令碼間的引用,並用Start來傳遞資訊Awake總是在Start之前被呼叫。它不能用來執行協同程式。

    C#和Boo使用者注意:Awake 不同於建構函式,物體被構造時並沒有定義元件的序列化狀態。Awake像建構函式一樣只被呼叫一次。

  • Start

    Start在behaviour的生命週期中只被呼叫一次。它和 Awake 的不同是,Start 只在指令碼例項被啟用時呼叫。你可以按需調整延遲初始化程式碼。Awake 總是在Start之前執行。

  • OnMouseEnter /OnMouseOver / OnMouseExit / OnMouseDown / OnMouseUp / OnMouseDrag

    當滑鼠進入 / 懸浮 / 移出 / 點選 / 釋放 / 拖拽GUIElement(GUI元素)或Collider(碰撞體)中時呼叫OnMouseEnter。

  • OnTriggerEnter / OnTriggerExit / OnTriggerStay

    當Collider(碰撞體)進入 / 退出 / 停留在 trigger(觸發器)時呼叫OnTriggerEnter。OnTriggerStay 將會在每一幀被呼叫。

  • OnCollisionEnter / OnCollisionExit / OnCollisionStay

    當此collider/rigidbody觸發另一個rigidbody/collider時,被呼叫。OnCollisionStay 將會在每一幀被呼叫。


四、指令碼與GameObject的關係

被顯式新增到 Hierarchy 中的 GameObject 會被最先例項化,GameObject 被例項化的順序是從下往上。GameObject 被例項化的同時,載入其元件 component 並例項化,如果掛載了指令碼元件,則例項化指令碼元件時,將呼叫指令碼的 Awake 方法,元件的例項化順序是也是從下往上。在所有顯式的 GameObject 及其元件被例項化完成之前,遊戲不會開始播放幀。

當 GameObject 例項化工作完成之後,將開始播放遊戲幀。每個指令碼的第一幀都是呼叫 Start 方法,其後每一幀呼叫 Update,而且每個指令碼在每一幀中的呼叫順序是從下往上。

總結:被掛載到 GameObject 下面的指令碼會被例項化成 GameObject 的一個成員。

4.1 指令碼變數的引用

在指令碼中宣告另一個指令碼的變數。在 ClassA 中建立一個 public 的變數型別是 ClassB。

// class A
public class classA : MonoBehaviour {

    public classB b;

    // Use this for initialization
    void Start () {

    }

    // Update is called once per frame
    void Update () {

    }
}

// class B
public class classB : MonoBehaviour {

    public classA a;
    // Use this for initialization
    void Start () {

    }

    // Update is called once per frame
    void Update () {

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

4.1.1 非同一個 GameObject 的指令碼引用

情況如下: 


這裡寫圖片描述 
這裡寫圖片描述 

此時,如果 classA 中的成員 B 想要引用由 GameObjectB new 出來的 classB 物件,只需要將 GameObjectB 拖拽到 GameObjectA 中 classA 指令碼即可。

4.1.2 同一個 GameObject 中互相引用

情況如下:


這裡寫圖片描述 

此時,發現沒法通過拖拽的方式建立 classA 和 classB 的引用。因為 Unity 編輯器裡面的拖拽繫結方式是 GameObject 級別的。

那麼此時如何解決互相引用的問題呢?此時,需要用到 gameObject 這個變數。

被掛載到 GameObject 中的指令碼,被例項化時,其內部繼承自 Monobehavior 的 gameObject 成員會繫結所掛載的 GameObject 物件。可以注意到,在本例中,classA 和 classB 都是同一個 GameObject 下的元件,所以通過 GetComponent 便可以獲得另一個指令碼變數的引用。

// class A
public class classA : MonoBehaviour {

    public classB b;

    // Use this for initialization
    void Start () {
        b = gameObject.GetComponent ("ClassB") as ClassB;
    }

    // Update is called once per frame
    void Update () {

    }
}

// class B
public class classB : MonoBehaviour {

    public classA a;
    // Use this for initialization
    void Start () {
       a = gameObject.GetComponent ("ClassA") as ClassA;
    }

    // Update is called once per frame
    void Update () {

    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

4.1.3 父子關係的 GameObject 中引用

把問題引申一步,還是那兩個指令碼ClassA,ClassB,不過這回不是綁在同一個GameObject上面,而是分辨繫結在兩個GameObject:Parent(ClassA),Child(ClassB)


這裡寫圖片描述 

首先還是來嘗試拖拽,雖然無法在Unity的編輯器中通過拖拽互相引用指令碼(Componet),不過繫結 GameObject 是可以.所以只需要建立兩個public的變數,然後型別都是 GameObject,在Unity裡面互相拖拽引用,最後在 Start 函式時候通過已經繫結好的 gameObject 呼叫其 GetComponent 方法即可。

的確,這個方法是可行,不過有個更好的方法就是使用 Transform。Transform 是一個很特殊的Component,其內部保留著 GameObject 之間的顯示樹結構.所以就上面的例子來說,當要從 Child 訪問到 Parent,只需要在 Child 對應的腳本里面寫 transform.parent.gameObject.GetComponent() 即可

返過來就相對麻煩一點,因為無法保證一個parent只有一個child,所以無法簡單的使用 transform.child.gameObject這樣訪問, 但是Unity給我們提供了一個很方便的函式,那就是Find。

需要注意的是Find只能查詢其Child,舉個複雜點的例子

Parent->ChildA->ChildB->ChildC

當在 Patent 中想要找到 ChildC中的一個Component時候,呼叫 transform.Find(“ChildA/ChildB/ChildC”).gameObject;