在上一篇文章中有提到,我們可以將 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 資料夾
- 「Standard Assets」和「Pro Standard Assets」和「Plugins」這三個資料夾內的程式碼
- 「Standard Assets」和「Pro Standard Assets」和「Plugins」這三個資料夾下的「Editor」內的程式碼
- 資料夾「Editor」以外的程式碼
- 資料夾「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 尚未位移前的狀態
// RecordObject(即將修改的目標物件, 在 Undo 選單中顯示的標題文字)
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( )。(但不會像 Update 一樣每個 Frame 都更新)
- 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 也會不停的被重畫,我們便可以在編輯時看到即時的效果。