본문 바로가기
[Unity]

[Unity] EditorGUI를 활용한 커스텀 에디터 만들기

by 김기승 2023. 4. 25.

커스텀 에디터를 제작하면 단순 반복 작업을 일괄처리 할 수도 있고, 필요로 하는 기능을 추가할 수도 있으며,

더 나아가 난잡해진 Inspector 창을 깔끔하게 정리할 수도 있다.

public class Inspector : MonoBehaviour
{
    public bool showValue = true;
    public int val = 100;
}

예를 들어 위와 같은 스크립트를 작성하면,

원래 이러한 Inspector 창을 생성하지만,

이렇게 만들 수도 있다는 것이다.

 

대부분 두 가지로 구현하는데,

첫 번째는 위에서 예시로 든 것과 같이 MonoBehavior를 상속한 클래스의 Inspector를 조작하는 것이고.

두 번째는 Lighting, Project Settings, Preference 및 Propiler와 같은 EditorWindow를 생성하는 것이다.

 

일단 각 방식을 구성해보고, 공통으로 사용되는 GUILayout에 대해 알아보겠다.


Inspector

public class CustomClass : MonoBehaviour
{
    public bool boolVar = true;
    public int intVar = 100;
    public float floatVar = 0.1f;
    public int[] arrayVar;
    public CustomStruct structVar;
    public struct CustomStruct
    {
        public string stringVar;
    }
}

CustomClass에는 다양한 자료형의 변수들을 두었다. 이 클래스의 Inspector를 제작하기 위해서는

/* 에디터에서만 사용한다는 뜻으로, #endif 까지의 구문들은 빌드에 포함되지 않는다 */
#if UNITY_EDITOR
[CustomEditor(typeof(CustomClass))] //CustomClass는 커스텀 Inspector로 표시할 것
public class CustomInspector : Editor
{
    /* Inspector를 그리는 함수 */
    public override void OnInspectorGUI()
    {
    
    }
}
#endif

이러한 형식을 갖춘 클래스가 필요한데, [CustomEditor(typeof("클래스명"))] 으로 커스텀할 클래스를 지정했고,

OnInspectorGUI 함수에 작성된 구문들이 실제 Inspector에 표시되는 GUI가 된다.

※ 해당 클래스를 따로 스크립트를 생성해서 작성하는 것이 일반적이나, 같은 스크립트에 작성해도 된다.

 

이제 CustomClass의 있는 변수들을 참조할 수 있어야 한다.

유니티에서는 초기화 가능한 변수들을 감싸서 직렬화한 오브젝트를 제공하는데, 그것이 SerializedObject 이다.

현재 에디터에 표시되고 있는 클래스의 SerializedObject를 상속(Editor)받은 클래스에 제공하고 있어

즉시 사용이 가능하다.

초기화 가능한 변수는 접근 지정자 public으로 지정하거나, [SerializeField]로 선언한 것이다.

CustomClass에서는 그냥 public으로만 선언했다.

    /* Inspector를 그리는 함수 */
    public override void OnInspectorGUI()
    {
        SerializedProperty boolVar =  serializedObject.FindProperty("boolVar"); //Bool
        Debug.Log(boolVar.boolValue);

        SerializedProperty intVar = serializedObject.FindProperty("intVar"); //Int
        Debug.Log(intVar.intValue);

        SerializedProperty floatVar = serializedObject.FindProperty("floatVar"); //Float
        Debug.Log(floatVar.floatValue);

        SerializedProperty arrayVar = serializedObject.FindProperty("arrayVar"); //배열
        for (int i = 0; i < arrayVar.arraySize; ++i)
        {
            SerializedProperty arrayElementVar = arrayVar.GetArrayElementAtIndex(i);
            Debug.Log(arrayElementVar.intValue);
        }

        SerializedProperty structVar = serializedObject.FindProperty("structVar"); //구조체
    }

바로 serializedObject 프로퍼티가 위에서 언급한 것이고,

FindProperty 함수를 통해 변수의 이름으로 SerializedProperty 클래스를 참조할 수 있다.

이 클래스는 찾은 변수의 값을 지니고 있다.

단, SerializedProperty를 활용하여 변수를 참조할 때에는 각 자료형에 맞는 접근이 필요하다.

bool 자료형인데 boolValue 참조가 아닌 floatValue 참조 시 null 에러 발생

    /* Inspector를 그리는 함수 */
    public override void OnInspectorGUI()
    {
        SerializedProperty intVar = serializedObject.FindProperty("intVar");
        ++intVar.intValue; //값 변경 (그저 예시일 뿐입니다)

        serializedObject.ApplyModifiedProperties(); //변경된 프로퍼티 값을 적용
    }

 

변경된 프로퍼티 값을 실제 변수를 적용하기 위해서는 ApplyModifiedProperties 함수를 실행해주어야 한다.

※ 참고로 CustomInspector 클래스에서 선언한 변수는 유니티에서 저장해주지 않는다.


EditorWindow

유니티에서는 특정 기능을 담은 이러한 Window들을 열 수 있는데, 우리도 직접 만들어본다.

/* 에디터에서만 사용한다는 뜻으로, #endif 까지의 구문들은 빌드에 포함되지 않는다 */
#if UNITY_EDITOR
using UnityEditor;

public class CustomEditorWindow : EditorWindow
{
    [MenuItem("Custom/EditorWindow")] //해당 버튼을 누르면 Init 함수가 실행
    private static void Init()
    {
        CustomEditorWindow editorWindow = (CustomEditorWindow)GetWindow(typeof(CustomEditorWindow)); //Window 생성
        editorWindow.Show(); //Window 열기
    }

    /* GUI를 생성하는 함수 */
    private void OnGUI()
    {

    }
}
#endif

아까와 구조는 비슷하나, 메뉴 아이템을 클릭했을 때 실행되는 함수를 설정해주어야 한다.

여기에서는 Init 함수이고, 창을 생성한 후 여는 과정이 포함되어 있다.

OnGUI에 작성된 구문이 실제로 나타나는 GUI이다.

아직 GUILayout이 없기 때문에 허전하지만, 창은 잘 나타나는 것을 볼 수 있다.

    /* GUI를 생성하는 함수 */
    private void OnGUI()
    {
        Debug.Log(Selection.activeGameObject);
        Debug.Log(Selection.gameObjects.Length);
        Debug.Log(Selection.activeObject);
        Debug.Log(Selection.objects);
    }

EditorWindow에서는 Selection 클래스를 자주 참조하게 될 것이다.

선택된 오브젝트를 반환해주는 유용한 클래스이기 때문이다.

※ activeGameObject(복수:gameObjects)는 Hierarchy에서 선택된 게임 오브젝트고,

activeObject(복수:objects)는 Hierarchy 뿐만 아니라 Asset에 있는 모든 오브젝트다.

    /* GUI를 생성하는 함수 */
    private void OnGUI()
    {
        CustomClass customClass = Selection.activeGameObject.GetComponent<CustomClass>(); //CustomClass 클래스 참조
        SerializedObject serializedObject = new SerializedObject(customClass); //SerializedObject로 변환
        serializedObject.FindProperty("intVar").intValue = 1004; //프로퍼티로 참조

        serializedObject.ApplyModifiedProperties(); //프로퍼티 값 변경을 적용
    }

이를 활용하여 선택된 오브젝트 클래스의 변수를 참조할 수도 있다.

이전에 설명했던 SerializedObject로 변환하는 방식이다.

※ EditorWindow는 초기화 가능한 클래스와 연관되지 않기 때문에 자체 SerializedObject가 없다.

 

 

CustomEditorWindow 클래스에서 선언한 변수도 유니티에 저장되지 않는다.

그러나, EditorWindow 지정한 값들은 저장이 필요할 수도 있기 때문에

Xml이나 Json 방식 등으로 파싱하여 OnEnable 및 OnDisable 함수에서 로드 및 저장할 수도 있다.


GUILayout

본격적으로 GUI에 나타내보자.

단, 모든 예제는 상황에 맞게 바꿔야 한다.

① LabelField

EditorGUILayout.LabelField("My Label");
EditorGUILayout.LabelField("My BoldLabel", EditorStyles.boldLabel);

라벨을 나타내기 위한 Field이다.

boldLabel을 조작하여 색상 변경도 가능하다.

② ObjectField

gameObjectVar = (GameObject)EditorGUILayout.ObjectField("My GameObject", gameObjectVar, typeof(GameObject), true);

GameObject 뿐만 아니라 원하는 타입의 컴포넌트를 지정하기 위한 Field이다.

GUI에 표시되기를 원하는 변수를 넣어주고, 타입을 인자로 넣는다.

GUI를 그릴 때마다 값을 반환한다.

③ IntField, FloatField, Vector2Field, Vector3Field, ColorField

intVar = EditorGUILayout.IntField("My Int", intVar);
floatVar = EditorGUILayout.FloatField("My Float", floatVar);
vector2Var = EditorGUILayout.Vector2Field("My Vector2", vector2Var);
vector3Var = EditorGUILayout.Vector3Field("My Vector3", vector3Var);
colorVar = EditorGUILayout.ColorField("My Color", colorVar);

 

값을 지정하기 위한 Field이다.

표시하고자 하는 변수를 넣어준다. GUI를 그릴 때마다 값을 반환한다.

④ TextField, TextArea

stringVar = EditorGUILayout.TextField("My String", stringVar);
stringVar = EditorGUILayout.TextArea(stringVar, EditorStyles.textArea);
stringVar = EditorGUILayout.TextArea(stringVar, EditorStyles.textArea, GUILayout.Height(50f));

Text를 지정하기 위한 Field이다.TextField의 크기를 가변 또는 고정하는 TextArea를 활용할 수도 있다.

⑤ Toggle

boolVar = EditorGUILayout.Toggle("My Bool", boolVar);

Bool 타입 변수를 지정하기 위한 Toggle이다.

GUI를 그릴 때마다 값을 반환한다.

※ ToggleGroup으로 묶어서 사용가능

⑥ Button

if (GUILayout.Button("My Button")) Debug.Log("Click");

버튼이 클릭되는 순간 True를 반환한다.

※ 나머지는 False

⑦ EnumPopup

myEnum = (Number)EditorGUILayout.EnumPopup("My Enum", myEnum);

Enum 타입을 지정하는 Popup이다.

⑧ IntPopup

string[] displayedOptions = new string[] { "One", "Two", "Three", "Zero" };
int[] optionValues = new int[] { 1, 2, 3, 0 };
intVar = EditorGUILayout.IntPopup("My IntPopup", intVar, displayedOptions, optionValues);

Enum 타입을 만들지 않아도 string 배열로 만들 수 있는 Popup이다.

(optionValues 인자는 아직 필요성을 못 느낀다)

⑨ Toolbar

intVar = GUILayout.Toolbar(intVar, System.Enum.GetNames(typeof(Number)));

 

본 방식은 Enum을 사용했다. GUI를 그릴 때마다 값을 반환한다.

⑩ Foldout

boolVar = EditorGUILayout.Foldout(boolVar, "My FoldOut", true);

펼침을 나타낼 수 있으며, Label 클릭 시에도 토글될 것인지를 지정할 수 있다.

⑪ IndentLevel

EditorGUILayout.LabelField("My Label");
++EditorGUI.indentLevel;
EditorGUILayout.LabelField("My Label2");
++EditorGUI.indentLevel;
EditorGUILayout.LabelField("My Label3");
--EditorGUI.indentLevel;
EditorGUILayout.LabelField("My Label4");
--EditorGUI.indentLevel;
EditorGUILayout.LabelField("My Label5");

들여 쓰기를 나타낼 수 있다.

⑫ Space

GUILayout.Label("My Label");
EditorGUILayout.Space();
GUILayout.Label("My Label2");
EditorGUILayout.Space(20f);
GUILayout.Label("My Label3");

간격을 조절할 수 있다.

⑬ BeginHorizontal, EndHorizontal

GUILayout.BeginHorizontal();
GUILayout.Button("My Button1");
GUILayout.Label("My Label");
GUILayout.Button("My Button2");
GUILayout.EndHorizontal();

수평으로 나열할 수 있다.

⑭ FlexibleSpace

GUILayout.BeginHorizontal();
GUILayout.Button("My Button1");
GUILayout.Label("My Label");
GUILayout.Button("My Button2");
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();

유연하게 공간을 만들어낸다.


아직 설명하지 못한 종류가 많지만, 나머지는 응용 수준이므로

감을 쉽게 잡을 수 있을 것으로 본다.

조금이나마 도움이 되길 바라며 이만 글을 마친다.

 

※ 마우스를 올리기만 해도 실행되는 것이 OnInspectorGUI 함수와 OnGUI 함수이다.

그래서 조금만 화면이 복잡해져도 성능이 급격히 떨어진다. 최대한 간단하게 나타내야 한다.

※ 참고로 거의 모든 GUILayout 예제에 Label(My ...)을 넣었는데 안 넣어도 된다.

댓글