2015年6月11日 星期四

Unity:認識 Tag 與 Layer 的差異與應用

相信會使用 Unity 的人都知道 GameObject 有 tag 以及 layer 這兩個變數值(Variables),而在 Inspector view 的遊戲物件名稱下方緊接著就可以找到設定 Tag 及 Layer 的下拉選單欄位,所以 Tag 及 Layer 對 Unity 的使用者來說並不陌生,而通常直覺就是將 Tag 及 Layer 做為遊戲物件的標記分類,但是,它們之間的差異在哪兒呢?什麼用途適合 Tag,什麼用途適合 Layer?
Tag 及 Layer 的下拉選單欄位
我們可以從官方文件很清楚地得知 Tags 與 Layers 基本的特徵與用途。

Tag:
  • 為遊戲物件分類。
  • 方便查找遊戲物件。
  • 與其他遊戲物件碰觸時的判斷。
  • 在 Tag Manager(Unity 5 稱為 Tags & Layers)沒有限定所定義的數量。
Layer:
  • 為遊戲物件分類。
  • 讓 Camera 指定哪些物件要被畫出來。
  • 讓 Light 指定哪些物件要被照明。
  • 讓物理射線確認哪些物件要被偵測到。
  • 在 Tag Manager 有限定最多設定 32 個,且前 8 個預設不可讓使用者變更。
Layer 限定 32 個
以上,似乎已經很清楚地表明 Tag 與 Layer 之間的不同,同樣都是為物件分類,我們可以依照其特徵來選擇適當的使用時機,但是在寫程式和一些設計上,光這樣的認知是不夠的。

我們從 Inspector view 展開 Tag 以及 Layer 的選單來看,好像沒什麼不同,每個遊戲物件只能設定一個 Tag,同樣的,每個遊戲物件也只能設定一個 Layer,但如果從 Camera 及 Light 的 Culling Mask 欄位來看,Layer 選單不只多了 Nothing 及 Everything 可以選擇,而且是可以一次選擇多個 Layer 項目,從這個部分來看,可以得知 Layer 就比 Tag 複雜許多。

展開的 Tag 選單
展開的 Layer 選單
Camera 的 Culling Mask 欄位
Light 的 Culling Mask 欄位
首先,Tag 使用字串來定義,並直接從 Inspector view 的下拉選單來設定物件本身的 Tag,在使用上,Unity 內建並沒有任何 Component 有暴露出的欄位用來指定使用 Tag,所以通常是在我們自己寫的程式碼中才會使用到,而使用的方式則是直接給予 Tag 定義時的字串值,所以較單純些。常見的用法如下:

在場景中找出一個 Tag 為 Respawn 的 GameObject:

GameObject respawn = GameObject.FindWithTag("respawn");

在場景中找出全部的 Tag 為 Respawn 的 GameObject:

GameObject[] respawns = GameObject.FindGameObjectsWithTag("respawn");

有勾選 Is Trigger 的 Collider 在碰觸到其他物件時,判斷該物件的 Tag 是否為 Player:

void OnTriggerEnter(Collider other) {

    if(other.tag == "Player")){
        //....
    }
}

未勾選 Is Trigger 的 Collider 在碰撞到其他物件時,判斷該物件的 Tag 是否為 Player:

void OnCollisionEnter(Collision collision) {

    if(collision.gameObject.tag == "Player"){
        //....
    }
}

取得 Main Camera(Tag 為 MainCamera 的 Camera):

Camera mainCamera = Camera.main;

在此先講個題外話,寫程式應該要養成一種習慣,避免讓程式碼包含「不可思議的值」,這是什麼意思呢?舉例來說,如果程式裡寫了一個 if(val > 12.5f),另一個地方又寫了 if(val < 5.0f),也許剛寫完程式的時候,還能記得 12.5 跟 5 分別代表什麼,但是幾個月之後呢?或是轉交給他人維護時呢?還能清楚的知道這些值的意義分別是什麼嗎?不是要從相關的程式碼慢慢推算,就是要有詳細註解才行,但是如果換成 if(val > speedMax),是不是就清楚許多,另外一個相關的習慣是,應該避免在程式碼中直接寫明值,假設前述的 12.5f 沒有使用 speedMax 代替,如果有一天該值要改成 25.78f,那麼所有相關的地方都要依序手動改程式碼,這樣很容易發生遺漏而產生未知的 Bug。

為什麼要特別說明這個部分呢?Tag 在設定時以及在程式碼中的使用,都是很清楚的字串,可能不會發生「不可思議的值」這種問題,但是它畢竟是個字串值,而不是統一定義的變數或常數,如果前面常用例子裡面的 Tag 名稱 "Player" 在很多 Script 都有使用到,只要有任何地方不小心打錯一個字母,就會出現未知的 Bug,而且 IDE 無法幫我們發現這個問題,所以,可以在程式碼中宣告一個 public 變數欄位,統一在 Inspector view 來輸入這個字串值,那麼在同一個 Script 裡面就可以大大降低這種問題發生的風險,不過,在 Tag Manager 與我們撰寫的程式之間,還是要個別定義相同的字串值,如果在 Tag 應用廣泛的情況下,打錯字的風險還是有的,更何況有時候還是會有為了表達更明確而變更命名的情況,幸好 Unity 可以自行製作自定義編輯器來避免這種狀況發生。



接下來,來看看關於 Layer 的部分,在程式碼裡面只要宣告一個型別為 LayerMask 的 public 變數欄位,在 Inspector view 就可以像 Camera 或 Light component 一樣,出現可複選的 layer 選單欄位,隨著 Tag Manager 中的 Layers 設定或者命名的不同,這個選單也會跟著改變,所以就沒有前面所述像 Tag 那樣的問題,在設置欄位值上更為方便,而且不易出錯,不過 Layer 在一般程式判斷上的使用,倒是沒有 Tag 那麼簡單。

跟 Camera 及 Layer 一樣的 layer 選單
不同於 Tag 直接就是字串值,Layer 限定只能設定 32 個也是有原因的,因為 Layer 本身是個 32 位元的值,我們可以把它視為 00000000000000000000000000000000 這樣的值,從 Unity 預設已經定義的 Layers 有 Default、TransparentFX、Ignore Raycast、Water、UI 這幾個,如果我們在 Inspector view 將 Component 的 Layer 欄位勾選 Water 及 UI,那麼這個欄位值就可以視為 00000000000000000000000000110000,由此可知,Layers 所定義的其實是二進制的 32 個開關,所以如果將只勾選 Water 及 UI 這個 Layer 欄位值轉型為 int,將會獲得 48 的結果。

現在,既然已經瞭解到 Layer 值的原理,以下就來舉例相同狀況時 Tag 與 Layer 在程式碼上面的差異。
假設我們將敵人分類為 5 種,然後只對碰撞到其中三種敵人做出反應,那麼我們在 Tag Manager 中可以個別為 Tags 及 Layers 定義五個敵人種類名稱。

Tags 和 Layers 都定義好名稱

Tag

使用 Tag 來做的話,單純利用陣列判斷字串是否相符。

public string[] enemyTags;

void OnTriggerEnter(Collider other) {

    if(Array.IndexOf<string>(enemyTags , other.tag) != -1){

        //....
    }
}

而在 Inspector view 則需要將預計做出反應的 Tag 名稱準確的一個一個輸入到 enemyTags 陣列欄位中,如果輸入字串不相符的話,就會判斷錯誤。

準確的輸入 Tag 名稱

Layer

使用 Layer 來做的話,則是利用位運算子來使 layer 欄位值往右移位並與 1 進行邏輯運算來判斷 layer 是否相符。

public LayerMask enemyLayer;

void OnTriggerEnter(Collider other) {

    if((enemyLayer >> other.gameObject.layer & 1) == 1){
        //....
    }
}

而在 Inspector view 則是直接在 enemyLayer 欄位的選單中直接勾選出預計做出反應的 layer。

Layer 設定可以直接勾選
以上的差異不難看出,Layer 要更方便些、安全些,只是對於很少或不曾使用位運算子的人來說,可能會較難理解一些。

另外,Layer 在 Unity 中還有另外一個用途,即是做為物理射線判斷哪些東西要被偵測到。例如 Physics.Linecast 的最後一個參數,我們可以指定射線的起點到終點有哪些 layer 的物件會被偵測到。

public Vector3 start;
public Vector3 end;
public LayerMask enemyLayer;

void FixedUpdate(){

    if(Physics.Linecast(start , end , enemyLayer)){

        //....
    }
}

Physics 以及 Physics2D 裡有很多 Functions 都會使用到,另外 PhysicsPhysics2D 也都有預先定義了 DefaultRaycastLayers 以及 IgnoreRaycastLayer 來提供給 layerMask 參數使用,因為 layerMask 參數的預設值為 DefaultRaycastLayers,所以一般情況下,直接省略這個參數,只要是 Layer 設定為 Default 的遊戲物件都會被偵測到。

public Vector3 start;
public Vector3 end;

void FixedUpdate(){

    if(Physics.Linecast(start , end)){

        //....
    }
}

那麼 IgnoreRaycastLayer 呢?從名稱上來看是要被忽略、不要被偵測到的,所以就不能直接將它給予 layerMask 參數,而是要再多加個反轉運算子。那麼只要是 Layer 設定為 Ignore Raycast 的遊戲物件都會被忽略,未被忽略的則都會被偵測到。

public Vector3 start;
public Vector3 end;

void FixedUpdate(){

    if(Physics.Linecast(start , end , ~IgnoreRaycastLayer)){

        //....
    }
}

因此,我們也可以自行定義哪幾個 layer 的遊戲物件是要被忽略、不被偵測到的。

public Vector3 start;
public Vector3 end;
public LayerMask ignoreLayer;

void FixedUpdate(){

    if(Physics.Linecast(start , end , ~ignoreLayer)){

        //....
    }
}

再拿前面比較 Tag 與 Layer 差異的例子來說,我們也可以反過來設置哪幾種敵人碰觸時不做任何反應,其餘種類的敵人碰觸時才有動作。

public LayerMask ignoreEnemy;

void OnTriggerEnter(Collider other) {

    if((~ignoreEnemy >> other.gameObject.layer & 1) == 1){
        //....
    }
}

以上,對 Layer 以及 Tag 的差異有更詳細的瞭解之後,在遊戲的實作設計及應用上應該就能更加彈性,只要能好好運用 Tag 以及 Layer,對整體的設計製作上會有很大的幫助,而不是都用程式寫死的方式或是總是以物件名稱去做判斷,導致程式碼過於攏長及複雜。

最後,再補充一下設定 Layer 在 Unity 編輯器使用上的好處,對於放置很多物件的場景,我們有時候可能會因為場景的東西太多而造成觀看以及編輯上的不便,如果有好好的把每個遊戲物件分類並設置好 Layer 的話,在編輯器右上方的 Layers 選單可以調整哪些 Layer 的遊戲物件可以在 Scene view 看到,但是並不影響到 Game view 的內容,如果能善用這個功能,在工作上會方便很多。


P.S. 目前使用 Unity 版本為 5.1.0f3。