2016年10月25日 星期二

Unity 插件開發 (五) - Custom Inspector 實作「圖層管理工具」

我們在實作 2D 遊戲時,常常被圖層設定的問題所困擾。當我們想要在層與層之間新增另外一個圖層時,都要花上好一段時間來找每一張圖到底是在哪一層中。而且找到後,還需要挪出空間來給新圖層,幾乎要動到所有的圖層設定。所以,本章要來實做另一個 Custom Inspector 的範例,我們會利用 ReorderableList 來做一個圖層管理工具。





ReorderableList
ReorderableList 是一個可排序陣列的 Inspector 工具。這個工具其實你應該不陌生,你可以在 Unity 中 Project Settings / Tags and Layers 裡看到它。如果想要在Custom Inspector 裡使用 ReorderableList,必須要使用 UnityEditorInternal 的命名空間。UnityEditorInternal 這個函式庫雖然有開放給開發者,但官方文件中並沒有相關的資料。目前似乎是偏向 Unity 內部團隊在使用的,或者是未來會釋出的功能目前還在測試階段中。有興趣的可以去裡面挖掘好玩的東西。以下程式碼是建立 ReorderableList 基本雛形
LayerData.cs

using UnityEngine;
using System.Collections;

[System.Serializable]
public class LayerData
{
    public LayerData(GameObject _go, string _layerName, int _layerOrder)
    {
        go = _go;
        LayerName = _layerName;
        LayerOrder = _layerOrder;
        AddSpaceLayer = 1;
    }

    public GameObject go;
    public string LayerName;
    public int LayerOrder;
    public int AddSpaceLayer;
    public int tempLayerOrderBeforeChange;
}



LayerManager.cs

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class LayerManager : MonoBehaviour
{
    public List<LayerData> layerDataList = new List<LayerData>();
}



LayerManagerEditor.cs

using UnityEngine;  
using UnityEditor;  
using UnityEditorInternal;

[CustomEditor(typeof(LayerManager))]
public class LayerManagerEditor: Editor {  
    private ReorderableList list;

    private void OnEnable() {
        list = new ReorderableList(serializedObject, 
                serializedObject.FindProperty("layerDataList"), 
                true, true, true, true);
    }

    public override void OnInspectorGUI() {
        serializedObject.Update();
        list.DoLayoutList();
        serializedObject.ApplyModifiedProperties();
    }
}


這裡有幾點可以說明一下
  • LayerData 是需要做序列化的,因為我們會在 LayerManager 裡使用它,可以參考上一篇的序列化陣列
  • ReorderableList 其實有兩種建構子
    1. public ReorderableList(IList elements, Type elementType)
    2. public ReorderableList(SerializedObject serializedObject, SerializedProperty elements)
    推薦使用第二種 SerializedProperty 的,這會讓程式碼更簡潔,而且會支援較多 Unity 內建功能(例如:Undo System)
  • 後面四個 Boolean 參數分別代表
    1. 是否支援拖曳來交換 Element 排序
    2. 是否顯示標頭
    3. 是否支援新增按鈕
    4. 是否支援移除按鈕





畫出 List Item
ReorderableList 開出許多 Delegates Function 讓開發者可以客製化自己的 ReorderableList。第一個要介紹的就是 drawElementCallback。當 List 中的每一個物件要被畫出來時,系統會去 Call 這個 Function。加入以下的 Function 並在 OnEnalbe 中執行它。
LayerManagerEditor.cs

private void DrawElementCallback()
{
    // List 中的每一個元件要被畫時,會去 Call 以下的程式碼
    // [參數]
    //    rect : 原本每一個元件預設的位置和寬高
    //    index : 元件是 List 中的第幾個
    list.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
    {
        // 將元件的序列化參數資料提出來。
        // 若用序列化參數來做修改,可以直接改變原本 List 中的參數值,不需要再將欄位的值存回去。
        // 另外一個好處是,它可以支援 Undo 的功能
        var element = list.serializedProperty.GetArrayElementAtIndex(index);
        // 每一個元件都間隔 2px
        rect.y += 2;

        // [1] 輸入 GameObject 欄位。該欄位是使用 PropertyField 的序列化參數來做修改
        //     不需要再將欄位的值存回去,而且可以支援 Undo 的功能
        float _width = rect.width / 10.0f;
        EditorGUI.PropertyField(
            new Rect(rect.x, rect.y, _width * 3, EditorGUIUtility.singleLineHeight),
            element.FindPropertyRelative("go"),
            GUIContent.none
            );

        // [2] 輸入圖層欄位。
        //     GetSortingLayerNames() 可以取得目前專案使用的所有Layer字串
        //     因為這裡並不是使用序列化參數來做修改,所以需要再將欄位的值存回 m_Target
        //     而且它不支援 Undo 的功能
        string[] sorting_layer_ary = GetSortingLayerNames();
        int target_layer_name_index = System.Array.IndexOf(sorting_layer_ary, m_Target.layerDataList[index].LayerName);
        int _layer_index = EditorGUI.Popup(
        new Rect(rect.x + _width * 3, rect.y, _width * 3, EditorGUIUtility.singleLineHeight),
            target_layer_name_index,
            sorting_layer_ary
            );
        m_Target.layerDataList[index].LayerName = GetSortingLayerNames()[_layer_index];
        EditorUtility.SetDirty(m_Target);

        // [3] 顯示目前圖層
        EditorGUI.LabelField(
        new Rect(rect.x + _width * 7, rect.y, _width * 1, EditorGUIUtility.singleLineHeight),
            m_Target.layerDataList[index].LayerOrder.ToString()
            );

        // [4] 和前一個 Index element 的圖層相隔多少
        EditorGUI.LabelField(
            new Rect(rect.x + _width * 8, rect.y, 15, EditorGUIUtility.singleLineHeight),
            "+"
            );
        if (index == 0)
            m_Target.layerDataList[index].AddSpaceLayer = 0;
        EditorGUI.PropertyField(
            new Rect(rect.x + _width * 8 + 15, rect.y, _width - 15, EditorGUIUtility.singleLineHeight),
            element.FindPropertyRelative("AddSpaceLayer"),
            GUIContent.none
            );

        // [5] 修正完後的圖層
        int _finalLayer = (index == 0) ? init_layer_count + m_Target.layerDataList[index].AddSpaceLayer : m_Target.layerDataList[index-1].tempLayerOrderBeforeChange + m_Target.layerDataList[index].AddSpaceLayer;
        EditorGUI.LabelField(
            new Rect(rect.x + _width * 9 + 15, rect.y, _width * 1, EditorGUIUtility.singleLineHeight),
        _   finalLayer.ToString()
            );
        m_Target.layerDataList[index].tempLayerOrderBeforeChange = _finalLayer;
    };
}



LayerManagerEditor.cs - 取得目前專案使用的所有Layer字串

public string[] GetSortingLayerNames()
{
    System.Type internalEditorUtilityType = typeof(InternalEditorUtility);
    PropertyInfo sortingLayersProperty = internalEditorUtilityType.GetProperty(
       "sortingLayerNames",
       BindingFlags.Static | BindingFlags.NonPublic
       );
    return (string[])sortingLayersProperty.GetValue(null, new object[0]);
}


以上程式碼只擷取重要的部分,完整程式碼會放在參考資料連結裡。此段的 Editor 程式碼跑起來結果如下圖。

沒有留言:

張貼留言