2016年10月14日 星期五

Unity 插件開發 (三) - Custom Inspector

在上一篇文章中有提到,我們可以將 C# Script 中的變數透過 Property Attribute 來改變它在 Inspector 中顯示的方式。但這樣的方法顯然只能做一些比較陽春的狀態改變。如果要在複雜一點的功能,例如:我們很常需要用到,當某個 Boolean 設為 True 時,才會開啟一些可控制的變數,或者是某個變數的控制會受其它變數值的影響。這些比較複雜的功能,就必須要撰寫自己的 Custom Inspector Script。其實從本篇開始,才是真正插件開發的重頭戲,前兩篇只是為了介紹好用的工具和鋪陳而已。




為何要寫插件工具?

當我們要製作比較龐大的遊戲專案時,開發團隊通常會在數十人以上的規模在合作。而負責實作遊戲邏輯的軟體工程師,通常會在遊戲規格開得差不多後,開始參予專案製作。由於遊戲軟體工程師很接近遊戲的核心,所以在製作的中後期會變得非常忙碌。大至模組的製作、小至細微參數的調整都要一手包辦,真的會忙得焦頭爛額啊。如果想要減輕一些負擔給別人,總不能叫遊戲企畫或美術來寫程式吧。如果軟體工程師在專案開啟的前期,能設計好一些工具供後期使用,不但可以省下很多時間作重複的事情,甚至可以請不懂程式設計的人也來幫忙哩。舉個例子,如果今天專案要製作的是音樂遊戲,譜面的編輯工具就變得很重要,而且編輯音樂譜面也是要交給專業人士編輯,總不能讓寫程式的工程師,在最後死線前還要負責編譜吧。
所以,好的工具可以減輕工程師的負擔,將工作交出去給美術或企畫做編輯和調整。但相對的,製作遊戲工具也是要花不少時間的,這也要作好拿捏的,並不是所有的遊戲開發都需要另外開發編輯器或工具。

用 Unity 寫客制化工具

Unity 其實可以讓你擴充編輯器功能,包括設計自己的 Inspectors 或建立新的客制化視窗。裡面的參數元件該如何呈現、位置該如何排列,都可以由開發者來定義。而本篇就要來先介紹客制化 Inspectors。
撰寫 Inspectors 客制化工具有三個步驟
  • 建立一個 Script。它是我們的目標類別,我們要將這隻 Script 的 Inspectors 呈現做修改
  • 建立另一個 Script 繼承自 Editor,這個 Editor Script 會負責如何呈現 Inspectors 內的元件
  • 在 Editor Script 中加入 CustomEditor 的 Attribute,便代入要參照的類別,也就是我們的目標 Script


Editor 資料夾
在正式撰寫程式之前,先來說明一下 Editor 這個資料夾特性。我們所撰寫的所有 Editor Script 都要放在 Editor 資料夾下。在 Editor 資料夾內的程式碼,都會在最後才被執行。另外,放在「Editor」內的程式碼,在最後編譯階段時是不會編譯出去的。以下是資料夾名稱的執行順序
  1. 「Standard Assets」和「Pro Standard Assets」和「Plugins」這三個資料夾內的程式碼
  2. 「Standard Assets」和「Pro Standard Assets」和「Plugins」這三個資料夾下的「Editor」內的程式碼
  3. 資料夾「Editor」以外的程式碼
  4. 資料夾「Editor」內的程式碼


實做 Custom Inspector
在 2D 橫向卷軸遊戲中,有一個很常用到的功能叫做「攝影機追蹤」。顧名思義,就是攝影機會根據角色的位置來移動。我們會設定一個範圍,這個範圍會比攝影機小。當玩家操控的角色超出了我們設定的範圍,就會把攝影機往玩家的位置移動,讓玩家角色再度回到適當範圍裡。在 Unity 的內建 2D Package 裡就有一個 CameraFollow 的程式,它寫的就是攝影機追蹤角色的功能。這隻程式雖然被我稍微做了一些修改,但意義上是一樣的。我們要用這隻程式來示範該如何撰寫自己的 Custom Inspector。

1 建立目標類別

using System;
using UnityEngine;

public class CameraFollow : MonoBehaviour
{
    // X 軸方向的追蹤
    public bool isTrackingXAxis; // 是否開啟X 軸方向的追蹤
    public float xMargin = 1f;   // 角色離攝影機中心多遠以上會開始追蹤
    public float xSmooth = 8f;   // 攝影機追蹤的速度                                                                                                                                                                                                                                                                                                                                                                                                                                                    
    public Vector2 minMaxX;      // 攝影機追蹤的最小最大值 (最小值, 最大值)
    public float xPos;           // 攝影機實際的 X 位置

    // Y 軸方向的追蹤
    public bool isTrackingYAxis;
    public float yMargin = 1f;
    public float ySmooth = 8f;
    public Vector2 minMaxY;
    public float yPos;

    private Transform m_Player; // 玩家角色的 Transform
        
    private void Awake()
    {
        m_Player = GameObject.FindGameObjectWithTag("Player").transform;
    }

    // 檢查位置 X 是否超出範圍
    private bool CheckXMargin()
    {
        return Mathf.Abs(transform.position.x - m_Player.position.x) > xMargin;
    }

    // 檢查位置 Y 是否超出範圍
    private bool CheckYMargin()
    {
        return Mathf.Abs(transform.position.y - m_Player.position.y) > yMargin;
    }


    private void Update()
    {
        TrackPlayer();
    }
        
    private void TrackPlayer()
    {
        float targetX = transform.position.x;
        float targetY = transform.position.y;
            
        if (CheckXMargin())
            targetX = Mathf.Lerp(transform.position.x, m_Player.position.x, xSmooth*Time.deltaTime);
            
        if (CheckYMargin())
            targetY = Mathf.Lerp(transform.position.y, m_Player.position.y, ySmooth*Time.deltaTime);

        // 攝影機的位置必須要在最小和最大值之間
        targetX = Mathf.Clamp(targetX, minMaxX.x, minMaxX.y);
        targetY = Mathf.Clamp(targetY, minMaxY.x, minMaxY.y);

        // 設定攝影機位置
        transform.position = new Vector3(targetX, targetY, transform.position.z);
    }
}
將以上的程式碼附加到場景中的 Camera 上,我們在 Camera 的 Inspector 中會看到多一個 Camera Follow 的 Component 。而所有被設定成 public 的變數欄位(Field),都會變成一個可控制的欄位或是可以勾選的 CheckBox (Boolean的變數)。這是 Unity 內建的 Inspector 功能,方便開發者在遊戲場景中,可以隨時調整參數。
但內建的東西總是事與願違對吧!像是,我希望開發者如果沒有勾選「Is Tracking X Axis」,那下面四個和追蹤X軸相關的參數也不必開啟給開發者做設定吧。另外,參數「X Pos」代表攝影機的初始(或目前)位置,如果開發者輸入一個超出「Min Max X」設定值,那也不太合理啊!這時,我們就需要自己撰寫程式,來修改 Inspector 的畫面呈現囉!


2 建立 Editor 程式碼

我們需要建立另一個 Script 繼承自 Editor,這個 Editor Script 會負責如何呈現 Inspectors 內的元件
using UnityEngine;
using UnityEditor;
using System.Collections;

[CustomEditor(typeof(CameraFollow))]
public class CameraFollowerEditor : Editor
{
    CameraFollow m_Target;

    private bool _isTrackingXAxis;
    
    public override void OnInspectorGUI()
    {
        DrawDefaultInspector();

        m_Target = (CameraFollow)target;

        // Toggle(標題, 預設值),勾選框元件
        _isTrackingXAxis = EditorGUILayout.Toggle("Tracking X Axis", m_Target.isTrackingXAxis);
        m_Target.isTrackingXAxis = _isTrackingXAxis;

        // 從 BeginDisabledGroup(Boolean) 到 EndDisabledGroup() 中間的範圍是否可以被選取
        // 取決於 BeginDisabledGroup 傳入的布林參數
        EditorGUI.BeginDisabledGroup(_isTrackingXAxis == false);

        // FloatField(標題, 預設值),浮點數輸入元件
        // 原本的目標物件(Camera)裡的變數都要設定為 Inspector 欄位中修改的數值
        m_Target.xMargin = EditorGUILayout.FloatField("Margin", m_Target.xMargin);
        m_Target.xSmooth = EditorGUILayout.FloatField("Smooth", m_Target.xSmooth);
        m_Target.minMaxX.x = EditorGUILayout.FloatField("Min position", m_Target.minMaxX.x);
        m_Target.minMaxX.y = EditorGUILayout.FloatField("Max positio", m_Target.minMaxX.y);

        // 我們要用 Slider 來控制攝影機的位置,在此要先取得目標物件(Camera)的 Position
        Vector3 cameraPosition = m_Target.transform.position;
        // Slider(標題, 預設值, 最小值, 最大值),滑桿元件
        cameraPosition.x = EditorGUILayout.Slider("Camera X Position", cameraPosition.x, m_Target.minMaxX.x, m_Target.minMaxX.y);
        EditorGUI.EndDisabledGroup();

        if (cameraPosition.x != m_Target.transform.position.x)
        {
            // 在修改目標物件(Camera)的位移前,先記錄到 Undo List 中。開發者可以藉由 Undo 的功能回到 Transform 尚未位移前的狀態
            Undo.RecordObject(m_Target.transform, "Change Camera X Position");
            // 原本的目標物件(Camera)裡的 position 都要設定為 Inspector 滑桿中修改的數值
            m_Target.transform.position = cameraPosition;            
        }

        // 每一次都重畫場景中的物件(為了處理 Gizmos)
        SceneView.RepaintAll();
    }
}

上面撰寫 Editor Script 有幾個重點要注意
  • Line 2 : Editor Script 需要用到「UnityEditor」的 Namespace,記得加入「using UnityEditor;」
  • Line 6 : Editor Script 需要繼承自 Editor
  • Line 5 : 必須宣告這個 Editor Script 是為了編輯哪個目標類別,因此在 class 的上方加入「[CustomEditor (typeof(CameraFollow))]
  • Line 12 : OnInspectorGUI()這個 Function 會在 Inspector 畫面有重畫或有任何滑鼠點擊事件時執行,相當於 Update()
  • Line 14 : target這個變數是目標類別的物件。將它轉型後(m_Target),便可以從裡面拿到目標類別裡的變數和方法
  • 以上程式只示範攝影機X軸方向的追蹤Y軸方向的追蹤可以以此類推,程式邏輯一模一樣


結果呈現如下圖,所有的設定都會在勾選了「Tracking X Axis」後才可以編輯。且「Camera X Position」也會根據「Min/Max Position」作範圍限制,並跟著移動鏡頭




加入 Gizmos
為了讓設定值可以更方便調整,我們想要將攝影機追蹤的安全範圍即時的畫在場景中。這就要用到第一章教過的 Gizmos 來畫出可視物件。我們將以下的程式碼 Function 加到 CameraFollow 的類別中
void OnDrawGizmos()
{
    Camera cam = Camera.main;
    float cHeight = 2f * cam.orthographicSize;
    
    Vector3 pos = this.transform.position - new Vector3(0.0f, 0.0f, 1.0f);
    float region_width = xMargin * 2.0f;

    if (xMargin < 0.0f)
        Gizmos.color = new Color(1.0f, 0.0f, 0.0f, 0.5f);
    else
        Gizmos.color = new Color(0.0f, 1.0f, 0.0f, 0.5f);
    Gizmos.DrawCube(pos, new Vector3(region_width, cHeight, 1.0f));
}


結果呈現如下圖。還記得我們在 CameraFollowerEditor 最後加的 SceneView.RepaintAll() 嗎?它會在我們編輯場景的過程中去重畫場景的物件。也就是說,即使不執行程式,Gizmos 也會不停的被重畫,我們便可以在編輯時看到即時的效果。


參考資料

沒有留言:

張貼留言