2016年10月23日 星期日

Unity 插件開發 (四) - Custom Inspector 實作範例

本章要來實做一些 Custom Inspector 的範例,我們會針對可序列化陣列的 Inspector 做出更人性化的呈現。



Serializable 可序列化
序列化 (Serialization) 是 Unity Editor 裡非常核心的程式。當你在使用 Unity 很多方便的功能時,它幾乎都是架構在序列化程式之上的。由其你的程式碼是繼承自 MonoBehaviour ,那些能夠在 Inspector 裡看到的可調整欄位,都是序列化的功勞啊!在 Unity 中,設為 Public 的變數通常都會被設為可序列化的。但有的時候,我們自行設計的類別,因為 Unity 也不知道我們寫了些什麼,它當然就無法將它做序列化的動作。例如,下面這段程式, List<BrickType> 就算設成 Public,Unity 也不會把它做序列化。它在 Inspector 視窗中當然就看不到可調整欄位的身影啦。但如果在類別前加上 [System.Serializable],告訴 Unity 這個類別是可以做序列化的。那麼,它就會在 Inspector 視窗中出現一個陽春版的可調整欄位。
using UnityEngine;
using System.Collections;

//如果此類別有被拿來做成 Public Array 或 Public 變數
//[System.Serializable] 會叫 Unity 去對此類別做序列化
[System.Serializable]
public class BrickType
{
    public string Name;
    public Color HitColor;
}

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

public class BlockController : MonoBehaviour
{
    public List<BrickType> Types = new List<BrickType>();
    public string typeName;
    public LayerMask whatIsPlayer;   

    private BoxCollider2D m_boxCollider2D;

    // 以下省略......
}


修改可序列化陣列
其實本篇的重點不在可序列化,而是這個可序列化陣列List<BrickType>的 Inspector 欄位。因為它說實在的不太好用。如果你有使用過它,應該知道它必須先填寫 List Size 的大小,填完後才會長出相對應個數的參數集合給你填,使用上很不直覺。而且你如果不小心手殘把已經設定好的 List Size 改成 0,你原本參數集合裡的值通通會消失不見,必須要一個個再把它填回來。那真的是很崩潰!所以,本篇的任務就是要利用 Editor Script 來修改可序列化陣列呈現的方式。
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Reflection;

[CustomEditor(typeof(BlockController))]
public class BlockControllerEditor : Editor {
    BlockController m_Target;

    public override void OnInspectorGUI()
    {
        m_Target = (BlockController)target;

        // DrawDefaultInspector() 會將原本 Inspector 上有的東西先畫出來。
        // 這麼一來可能會造成 Inspector 中的 Types 欄位被畫兩次(Default一次,下面 Editor Script 又會在畫一次)
        // 因此,你可以在原先 public List<BrickType> Types 的欄位前加入 [HideInInspector],把 Default Inspector 關掉
        DrawDefaultInspector();
        DrawTypesInspector();
    }
 
    void DrawTypesInspector()
    {
        GUILayout.Space(5);
        GUILayout.Label("State", EditorStyles.boldLabel);

        for(int i=0; i< m_Target.Types.Count; i++)
        {
            DrawType(i);
        }

        DrawAddTypeButton();
    }

    void DrawType(int index)
    {
        if (index < 0 || index >= m_Target.Types.Count)
            return;
        
        GUILayout.BeginHorizontal();
        {
            GUILayout.Label("Name", EditorStyles.label, GUILayout.Width(50));

            // BeginChangeCheck() 用來檢查在 BeginChangeCheck() 和 EndChangeCheck() 之間是否有 Inspector 變數改變
            EditorGUI.BeginChangeCheck();
            string newName = GUILayout.TextField(m_Target.Types[index].Name, GUILayout.Width(120));
            Color newColor = EditorGUILayout.ColorField(m_Target.Types[index].HitColor);
            
            m_Target.Types[index].Name = newName;
            m_Target.Types[index].HitColor = newColor;

            // 如果 Inspector 變數有改變,EndChangeCheck() 會回傳 True,才有必要去做變數存取
            if (EditorGUI.EndChangeCheck())
            {
                // 在修改之前建立 Undo/Redo 記錄步驟
                Undo.RecordObject(m_Target, "Modify Types");

                m_Target.Types[index].Name = newName;
                m_Target.Types[index].HitColor = newColor;

                // 每當直接修改 Inspector 變數,而不是使用 serializedObject 修改時,必須要告訴 Unity 這個 Compoent 已經修改過了
                // 在下一次存檔時,必須要儲存這個變數
                EditorUtility.SetDirty(m_Target);
            }

            if (GUILayout.Button("Remove"))
            {
                // 系統會 "登" 一聲
                EditorApplication.Beep();
                                
                // 顯示對話框功能(帶有 OK 和 Cancel 兩個按鈕)
                if (EditorUtility.DisplayDialog("Really?", "Do you really want to remove the state '" + m_Target.Types[index].Name + "'?", "Yes", "No") == true)
                {
                    m_Target.Types.RemoveAt(index);
                    EditorUtility.SetDirty(m_Target);
                }

            }
        }
        GUILayout.EndHorizontal();
    }

    void DrawAddTypeButton()
    {
        if (GUILayout.Button("Add new State", GUILayout.Height(30)))
        {
            Undo.RecordObject(m_Target, "Add new Type");

            m_Target.Types.Add(new BrickType { Name = "New State" });
            EditorUtility.SetDirty(m_Target);
        }
    }
}


修改結果


參考資料

沒有留言:

張貼留言