WinCNT

Unity의 EditorWindow를 한 번 알아보자 그 첫번째! 덤으로 Reflection과 PropertyDrawer로 List 요소만 표시해보자! 본문

Unity/Unity 관련

Unity의 EditorWindow를 한 번 알아보자 그 첫번째! 덤으로 Reflection과 PropertyDrawer로 List 요소만 표시해보자!

WinCNT_SSS 2024. 3. 25. 16:03

서론

전임자(필자는 만난 적 없음)가 전 프로젝트(필자는 참가한 적 없음)에서 만들었던 툴을 현재 담당하는 프로젝트에 마이그레이션하는 업무를 담당하게 되었다

아쉽게도 무지성으로 스크립트를 복사하는 방법은 당연히 불가능했다…따흐흑

살짝 살펴보니 EditorWindow에 대한 지식이 없으면 옮길 것도 못 옮길 것 같아서 우선 EditorWindow에 대해서 알아보기로 하였다


Hello, World!

역시 Unity, 공식 문서에 튜토리얼이 있었다

커스텀 에디터 창 생성 - Unity 매뉴얼

 

커스텀 에디터 창 생성 - Unity 매뉴얼

커스텀 에디터 창을 사용하면 직접 에디터와 워크플로를 만들어 Unity를 확장할 수 있습니다. 이 가이드는 코드를 사용하여 에디터 창을 만들고, 사용자 입력에 응답하고, UI의 크기를 조절할 수

docs.unity3d.com

 

위를 참고해서 일단 기본 중 기본인 Hello, World!를 출력해봤다(namespace는 필자가 좀 바꿨다)

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;

namespace Editor.EditorWindows
{
    public class MyCustomEditor : EditorWindow
    {
        [MenuItem("Custom Tools/My Custom Editor")]
        public static void ShowMyEditor()
        {
            EditorWindow wnd = GetWindow<MyCustomEditor>();
            wnd.titleContent = new GUIContent("My Custom Editor");
        }

        public void CreateGUI()
        {
            rootVisualElement.Add(new Label("Hello, World!"));
        }
    }
}

 

중요한 것은 EditorWindow를 상속받아서 만든 스크립트가 Editor 폴더 하부에 있어야 한다는 점이다

어…음 사실 asmdef를 제대로 설정하면 다른 폴더여도 상관 없을 것 같지만 그쪽은 일단 생략하자

또한 MenuItem를 설정한 함수는 Static으로 선언해야 한다


CreateGUI()와 OnGUI()

위에서는 CreateGUI()로 Hello, World!를 출력했지만, 사실 사용 빈도가 높은 함수는 OnGUI() 쪽일 것이다

CreateGUI는 에디터를 열 때 한 번만 호출되지만, OnGUI는 반복되서 호출된다

매번 호출되는 것은 아니고, 아마 특정 이벤트(마우스 호버링 등)일 때 호출되는 것으로 보인다

 

초기화 설정 같은 것은 CreateGUI()서 하고 갱신되는 렌더링 처리는 OnGUI()에 구현하면 될 것 같다

물론 대부분의 경우는 CreateGUI() 대신에 OnEnable()을 사용해도 별 문제가 없을 것으로 보인다


List를 표시하기

다른 여러 기능들은 다른 사람들의 글에도 잘 정리되어 있으니 생략하도록 하고!

애초에 필자도 그런 글들 보면서 구현한다…

찾아도 잘 안 나와서 필자가 고생하면서 구현했던 List를 EditorWindow에 표시하는 방법을 정리해보고자 한다

 

라고는 했지만 그냥 표시하고 싶을 뿐이라면 PropertyField()라는 API를 사용하는 것이 제일 편하다

Editor: How to do PropertyField for List elements?

 

Editor: How to do PropertyField for List elements?

Hi there, I finally got a array handling with serialized properties working, but I need the same for generic lists. It comes down to to the question of “How to access List.Count and List[dataindex] when using FindProperty()” ??? Here’s my working arr

discussions.unity.com

 

물론 PropertyField()를 사용하기 위해서는 SerializedProperty가 필요하고, SerializedProperty를 사용하기 위해서는 SerializedObject하긴 하다

 

SerializedObject나 SerializedProperty에 대해서는 아래의 참고 사이트가 잘 정리되어 있으니 궁금한 분들은 읽어보는 것을 추천한다!

[에디터 확장 입문] 번역 5장 SerializedObject에 대해서

 

[에디터 확장 입문] 번역 5장 SerializedObject에 대해서

<주의> 원문의 작성 시기는 2016년경으로, 코드나 일부 설명이 최신 유니티 버젼과 다소 맞지 않을 ...

blog.naver.com

 

아무튼 자세한 설명은 생략하고, 필자가 구현해본 코드는 다음과 같다

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

[Serializable]
public class SceneInfoData
{
    [SerializeField] private string assetPath = default;
    public SceneInfoData() { }
    public SceneInfoData(string assetPath) { this.assetPath = assetPath; }
}

public class MyCustomWindow : EditorWindow
{
    [MenuItem("Custom Tools/My Custom Window")]
    public static void ShowWindow()
    {
        GetWindow<MyCustomWindow>("My Custom Window");
    }

    [SerializeField] private List<SceneInfoData> sceneInfoList = new List<SceneInfoData>();
    SerializedObject targetObject;
    
    private void OnEnable()
    {
        targetObject = new SerializedObject(this);
        
        sceneInfoList.Clear();
        sceneInfoList.Add(new SceneInfoData("111"));
        sceneInfoList.Add(new SceneInfoData("333"));
    }

    private void OnGUI()
    {
        if (targetObject != null)
        {
            targetObject.Update();

            // sceneInfoList는 위에서 선언한 List의 변수명
            SerializedProperty prop = targetObject.FindProperty("sceneInfoList");
            EditorGUILayout.PropertyField(prop, new GUIContent(prop.displayName));

            targetObject.ApplyModifiedProperties();
        }
    }
}

 

그러면 다음과 같이 List가 잘 표시된다!


PropertyAttribute와 PropertyDrawer 사용하기!

EditorWindow에도 PropertyAttribute와 PropertyDrawer를 사용할 수 있다

PropertyAttribute와 PropertyDrawer를 MonoBehaviour의 스크립트에 사용하는 예는 많았지만, 신기하게도 EditorWindow에서 사용하는 예제는 찾지 못 했다

그래도 좀 고생해서 어떻게든 작동하는 코드를 구현해봤다!

 

아래는 커스텀 Disable을 구현해본 코드이다

Custom PropertyAttribute

public class CustomDisableAttribute : PropertyAttribute { }

Custom PropertyDrawer

[CustomPropertyDrawer(typeof(CustomDisableAttribute))]
public class DisableDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginDisabledGroup(true);
        
        EditorGUI.BeginProperty(position, label, property);
        EditorGUI.PropertyField(position, property, new GUIContent(property.displayName));
        EditorGUI.EndProperty();
        
        EditorGUI.EndDisabledGroup();
    }
}

 

그리고 위의 샘플 코드의 List에 [CustomDisable]를 추가해주면…!

public class MyCustomWindow : EditorWindow
{
    [MenuItem("Custom Tools/My Custom Window")]
    public static void ShowWindow()
    {
        GetWindow<MyCustomWindow>("My Custom Window");
    }

    [CustomDisable][SerializeField] private List<SceneInfoData> sceneInfoList = new List<SceneInfoData>();
    SerializedObject targetObject;

    // ...(생략)...
}

 

그러면 다음과 같이 List가 Disable이 된다!


샘플 코드 CustomDisable의 약점

샘플 코드의 CustomDisable는 List로 표시되는 요소들을 Disable로 할 뿐이고 List 자체를 Disable로 하지는 않는다

그게 무슨 소리지?라고 할 수 있는데 다음을 보면 어떤 느낌인지 이해가 잘 될 것이다

 

List 자체를 Disable로 하고 싶으면 List를 PropertyField하는 처리의 앞뒤로 Disable 설정을 해주면 된긴 한다

만약 그 때 List에 CustomDisable를 안 붙였으면 List의 요소들을 접거나 펼칠 수 있으면서, List의 추가나 교체는 안 되게 된다


List의 요소만 표시하기

예제에는 우연히 Asset Path의 값이 라벨로 표시되고 있지만, 이 부분은 PropertyField가 뭘 표시해야 할지 모르는 데이터면 Element0나 Element1와 같은 형태로 표시되기도 한다

 

그런데 필자는 더 나아가서 List의 요소들만 표시하고 싶었다

예를 들어 샘플 코드의 SceneInfoData의 Asset Path의 값만 표시하는 등등

하지만 이것이 헬 게이트의 시작인지 몰랐지…

 

수정 1) 우선 요소만 나오도록 바꿔보자!

할 수 있다! GetEnumerator라면…!

[CustomPropertyDrawer(typeof(CustomDisableAttribute))]
public class DisableDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginDisabledGroup(true);
        EditorGUI.BeginProperty(position, label, property);

        var enumerator = property.GetEnumerator();
        int depth = property.depth;
        while (enumerator.MoveNext())
        {
            var prop = enumerator.Current as SerializedProperty;
            if (prop == null || prop.depth > depth + 1) 
                continue;
            EditorGUI.PropertyField(position, prop, new GUIContent(prop.displayName));
        }

        EditorGUI.EndProperty();
        EditorGUI.EndDisabledGroup();
    }
}

그러면 위와 같은 결과가 나온다…는 사실 훼이크였다

 

만약 SceneInfoData에 다른 SerializeField가 있으면 UI가 망가진다

public class SceneInfoData
{
    [SerializeField] private string assetPath = default;
    [SerializeField] private string AAAAAAAAA = default;
    [SerializeField] private string BBBBBBBBB = default;

    public SceneInfoData(string assetPath)
    {
        this.assetPath = assetPath;
        this.AAAAAAAAA = "AAAAAAAAA";
        this.BBBBBBBBB = "BBBBBBBBB";
    }
}

 

수정 2) List의 요소마다 적절한 높이를 확보하기!

표시할 프로퍼티의 높이는 GetPropertyHeight란 메소드에 의해 정해지는 것 같았다

다시 말해 이 부분을 override해주면 List의 요소마다 적절한 높이를 구할 수 있다

public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
    float totalHeight = 0f;
    // 프로퍼티 한 개의 높이+ 적절한 패딩
    var singleLineHeight = base.GetPropertyHeight(property, label) + EditorGUIUtility.standardVerticalSpacing;
    // 표시할 프로퍼티의 수
    int propertyCount = 0;
    var enumerator = property.GetEnumerator();
    while (enumerator.MoveNext()) 
        propertyCount++;
    
    totalHeight = propertyCount * singleLineHeight;
    
    return totalHeight;
}

 

처음에는 프로퍼티의 개수를 구하기 위해 property.Copy().CountInProperty()를 써봤는데, 가끔 1을 반환하는 경우가 있어서 property.GetEnumerator()로 변경했다

 

수정 3) List의 요소의 프로퍼티를 겹치지 않게 표시하기!

다음은 시행착오를 거치며 적정한 position의 위치를 찾았다

override 하기 전의 GetPropertyHeight로 position.y와 position.height를 적절하게 조절하는 것이 포인트였다

[CustomPropertyDrawer(typeof(CustomDisableAttribute))]
public class DisableDrawer : PropertyDrawer
{
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        // ...생략...
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginDisabledGroup(true);
        EditorGUI.BeginProperty(position, label, property);

        position.y += EditorGUIUtility.standardVerticalSpacing;

        var enumerator = property.GetEnumerator();
        int depth = property.depth;
        while (enumerator.MoveNext())
        {
            var prop = enumerator.Current as SerializedProperty;
            if (prop == null || prop.depth > depth + 1) 
                continue;

            position.height = base.GetPropertyHeight(prop, null);
            EditorGUI.PropertyField(position, prop, new GUIContent(prop.displayName));
            position.y += base.GetPropertyHeight(prop, new GUIContent(prop.displayName)) + EditorGUIUtility.standardVerticalSpacing;
        }

        EditorGUI.EndProperty();
        EditorGUI.EndDisabledGroup();
    }
}

 

수정 4) 홀수행, 짝수행의 Background를 다른 색으로 표시하기

이 부분도 조금 고생했지만 EditorGUI.LabelField로 어떻게든 됐다

우선초기화는 한 번만 하고 싶으니 CustomDisableAttribute에 변수를 하나 추가했다

public class CustomDisableAttribute : PropertyAttribute
{
    public bool IsInitialized { get; set; }
}

 

그리고 초기화 처리와 배경색을 변경하는 처리를 추가했다!

[CustomPropertyDrawer(typeof(CustomDisableAttribute))]
public class DisableDrawer : PropertyDrawer
{
    // 배경색 변경을 위해 추가한 변수들
    private static GUIStyle style = new GUIStyle(EditorStyles.label);
    private static Texture2D texture = new Texture2D(1, 1);
    private bool isEven = false;
    private CustomDisableAttribute attr { get { return (CustomDisableAttribute)attribute; } }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        // ...생략...
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        // 초기화 처리 추가
        if (!attr.IsInitialized)
        {
            isEven = false;
            if (texture != null)
            {
                texture.SetPixel(0, 0, Color.gray);
                texture.Apply();
            }
            attr.IsInitialized = true;
            return;
        }
        
        // 홀수, 짝수에 따라 배경색을 변경
        if (isEven)
            style.normal.background = texture;
        else
            style.normal.background = null;
        isEven = !isEven;
        
        position.y -= EditorGUIUtility.standardVerticalSpacing / 2;
        EditorGUI.LabelField(position, GUIContent.none, style);

        EditorGUI.BeginDisabledGroup(true);
        EditorGUI.BeginProperty(position, label, property);

        position.y += EditorGUIUtility.standardVerticalSpacing;

        var enumerator = property.GetEnumerator();
        int depth = property.depth;
        while (enumerator.MoveNext())
        {
            var prop = enumerator.Current as SerializedProperty;
            if (prop == null || prop.depth > depth + 1) 
                continue;

            position.height = base.GetPropertyHeight(prop, null);
            EditorGUI.PropertyField(position, prop, new GUIContent(prop.displayName));
            position.y += base.GetPropertyHeight(prop, new GUIContent(prop.displayName)) + EditorGUIUtility.standardVerticalSpacing;
        }

        EditorGUI.EndProperty();
        EditorGUI.EndDisabledGroup();
    }
}

짜잔~!


System.Reflection으로 SerializedObject.FindProperty 대체해보기

이대로 끝내도 됐지만 개인적으로 마음에 안 드는 부분이 아직 있었다

바로 SerializedObject.FindProperty에 변수명(샘플에서는 sceneInfoList)를 쓴 부분이었다

public class MyCustomWindow : EditorWindow
{
    // ...생략...
    [CustomDisable, SerializeField] private List<SceneInfoData> sceneInfoList = new List<SceneInfoData>();
    SerializedObject targetObject;
    
    private void OnGUI()
    {
        if (targetObject != null)
        {
            targetObject.Update();
            // sceneInfoList는 위에서 선언한 List의 변수명
            SerializedProperty prop = targetObject.FindProperty("sceneInfoList");
            EditorGUILayout.PropertyField(prop, new GUIContent(prop.displayName));
            targetObject.ApplyModifiedProperties();
        }
    }
}

 

그냥 어트리뷰트에 CustomDisable가 붙었으면 알아서 표시하도록 하고 싶었다

동적으로 변수에 대한 정보를 알아야 하는 거니 예의 System.Reflection라는 것을 쓰면 가능하지 않을까라고 생각해 한 번 해봤다

 

여기서 그만두면 좋았을 텐데…그래도 결과적으로는 구현이 가능했으니 다행이라고 치자

 

Reflection을 이용해서 CustomAttributes가 있는 SerializedProperty만 획득하기

이번에 구현했던 것을 샘플 코드로 정리해봤다

이 부분을 좀 더 추상화하면 MyCustomWindow가 더 깔끔해질 것이다(샘플 코드라 생략)

 

우선 CustomAttributes가 있는 SerializedProperty만 모아두는 List를 추가했다

public class MyCustomWindow : EditorWindow
{
    private readonly List<SerializedProperty> targetList = new List<SerializedProperty>();
    // ...생략...
}

 

그리고 위에서 구현한 CustomDisableAttribute가 설정된 SerializedProperty하고 List에 추가하는 메소드도 만들었다

private void GetTargetProperty(SerializedObject serializedObject, SerializedProperty property)
{
    try
    {
        var type = serializedObject.targetObject.GetType();
        // BindingFlags는 매 중요!!
        FieldInfo field = type.GetField(property.propertyPath, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
        if (field != null)
        {
            object[] attributes = field.GetCustomAttributes(true);
            foreach (var attr in attributes)
            {
                // CustomDisableAttribute인지 체크하고 List에 추가
                if (attr.GetType() == typeof(CustomDisableAttribute))
                    targetList.Add(property.Copy());
            }
        }
    }
    catch (Exception e)
    {
        Debug.Log(e);
        throw;
    }
}

 

참고로 BindingFlags는 제대로 설정해야 한다

예를 들어 샘플 코드의 sceneInfoList는 private이므로 BindingFlags.NonPublic 플래그가 없으면 GetField로 가져올 수 없다

 

그리고 메소드는 OnEnable에서 다음과 같이 사용했다

private void OnEnable()
{
    targetObject = new SerializedObject(this);
    targetObject.Update();
    var iterator = targetObject.GetIterator();
    while (iterator.NextVisible(true))
    {
        GetTargetProperty(targetObject, iterator);
    }
    targetObject.ApplyModifiedProperties();

    // ...생략...
}

 

그리고 이제 변수명으로 획득할 필요가 없으니 지워주고, OnGUI에서 targetList의 요소들을 찾아서 그리는 것으로 변경!

private void OnGUI()
{
    if (targetObject != null)
    {
        targetObject.Update();

        // 이제 sceneInfoList를 찾을 필요 없음
        // SerializedProperty prop = targetObject.FindProperty("sceneInfoList");
        // EditorGUILayout.PropertyField(prop, new GUIContent(prop.displayName));

        EditorGUI.BeginDisabledGroup(true);
        foreach (var item in targetList)
        {
            EditorGUILayout.PropertyField(item, new GUIContent(item.displayName));
        }
        EditorGUI.EndDisabledGroup();

        targetObject.ApplyModifiedProperties();
    }
}

 

그러면 짜잔~!

겉모습은 변함 없이 제대로 나온다!

 

이제 다음과 같이 변수을 선언할 때 마음대로 정할 수 있게 되었다

public class MyCustomWindow : EditorWindow
{
    // ...생략...
    
    [CustomDisable, SerializeField] private List<SceneInfoData> sceneInfoList = new List<SceneInfoData>();
    [CustomDisable, SerializeField] private List<SceneInfoData> sceneInfoList2 = new List<SceneInfoData>();
    [CustomDisable, SerializeField] private List<SceneInfoData> sceneInfoList3 = new List<SceneInfoData>();

    // ...생략...
}


샘플 코드 전문

MyCustomWindow.cs

using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEditor;

[Serializable]
public class SceneInfoData
{
    [SerializeField] private string assetPath = default;
    [SerializeField] private string AAAAAAAAA = default;
    [SerializeField] private string BBBBBBBBB = default;

    public SceneInfoData(string assetPath)
    {
        this.assetPath = assetPath;
        this.AAAAAAAAA = "AAAAAAAAA";
        this.BBBBBBBBB = "BBBBBBBBB";
    }
}

public class MyCustomWindow : EditorWindow
{
    private readonly List<SerializedProperty> targetList = new List<SerializedProperty>();
    
    [MenuItem("Custom Tools/My Custom Window")]
    public static void ShowWindow()
    {
        GetWindow<MyCustomWindow>("My Custom Window");
    }

    [CustomDisable, SerializeField] private List<SceneInfoData> sceneInfoList = new List<SceneInfoData>();
    [CustomDisable][SerializeField] private List<SceneInfoData> sceneInfoList2 = new List<SceneInfoData>();
    [CustomDisable(IsInitialized = false), SerializeField] private List<SceneInfoData> sceneInfoList3 = new List<SceneInfoData>();
    SerializedObject targetObject;
    
    private void OnEnable()
    {
        targetObject = new SerializedObject(this);
        targetObject.Update();
        
        var iterator = targetObject.GetIterator();
        while (iterator.NextVisible(true))
        {
            GetTargetProperty(targetObject, iterator);
        }
        targetObject.ApplyModifiedProperties();
        
        sceneInfoList.Clear();
        sceneInfoList.Add(new SceneInfoData("111"));
        sceneInfoList.Add(new SceneInfoData("222"));
        sceneInfoList.Add(new SceneInfoData("333"));
        sceneInfoList.Add(new SceneInfoData("444"));
        sceneInfoList.Add(new SceneInfoData("555"));
        
        sceneInfoList2.Clear();
        sceneInfoList2.Add(new SceneInfoData("222"));
        sceneInfoList2.Add(new SceneInfoData("444"));
        sceneInfoList2.Add(new SceneInfoData("666"));
        
        sceneInfoList3.Clear();
        sceneInfoList3.Add(new SceneInfoData("111"));
        sceneInfoList3.Add(new SceneInfoData("333"));
        sceneInfoList3.Add(new SceneInfoData("555"));
    }

    private void OnGUI()
    {
        if (targetObject != null)
        {
            
            // // List
            // SerializedProperty prop = targetObject.FindProperty("sceneInfoList");
            //
            // EditorGUI.BeginDisabledGroup(true);
            // EditorGUILayout.PropertyField(prop, new GUIContent(prop.displayName));
            // EditorGUI.EndDisabledGroup();
            
            targetObject.Update();

            EditorGUI.BeginDisabledGroup(true);
            foreach (var item in targetList)
            {
                EditorGUILayout.PropertyField(item, new GUIContent(item.displayName));
            }
            EditorGUI.EndDisabledGroup();
            
            targetObject.ApplyModifiedProperties();
        }
    }
    
    private void GetTargetProperty(SerializedObject serializedObject, SerializedProperty property)
    {
        try
        {
            var type = serializedObject.targetObject.GetType();
            FieldInfo field = type.GetField(property.propertyPath, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
            if (field != null)
            {
                object[] attributes = field.GetCustomAttributes(true);
                foreach (var attr in attributes)
                {
                    if (attr.GetType() == typeof(CustomDisableAttribute))
                        targetList.Add(property.Copy());
                }
            }
        }
        catch (Exception e)
        {
            Debug.Log(e);
            throw;
        }
    }
}

 

CustomDisable.cs

using UnityEditor;
using UnityEngine;

public class CustomDisableAttribute : PropertyAttribute
{
    public bool IsInitialized { get; set; }
}

[CustomPropertyDrawer(typeof(CustomDisableAttribute))]
public class DisableDrawer : PropertyDrawer
{
    private static GUIStyle style = new GUIStyle(EditorStyles.label);
    private static Texture2D texture = new Texture2D(1, 1);
    private bool isEven = false;
    
    private CustomDisableAttribute attr { get { return (CustomDisableAttribute)attribute; } }

    
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        float totalHeight = 0f;
        var singleLineHeight = base.GetPropertyHeight(property, label) + EditorGUIUtility.standardVerticalSpacing;
        int propertyCount = 0;
        var enumerator = property.GetEnumerator();
        while (enumerator.MoveNext()) 
            propertyCount++;
        totalHeight = propertyCount * singleLineHeight;
        return totalHeight;
    }
    
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (!attr.IsInitialized)
        {
            isEven = false;
            if (texture != null)
            {
                texture.SetPixel(0, 0, Color.gray);
                texture.Apply();
            }
            attr.IsInitialized = true;
            return;
        }
        
        if (isEven)
            style.normal.background = texture;
        else
            style.normal.background = null;
        isEven = !isEven;
        
        position.y -= EditorGUIUtility.standardVerticalSpacing / 2;
        EditorGUI.LabelField(position, GUIContent.none, style);
        
        EditorGUI.BeginDisabledGroup(true);
        EditorGUI.BeginProperty(position, label, property);
        
        position.y += EditorGUIUtility.standardVerticalSpacing;
        
        var enumerator = property.GetEnumerator();
        int depth = property.depth;
        while (enumerator.MoveNext())
        {
            var prop = enumerator.Current as SerializedProperty;
            if (prop == null || prop.depth > depth + 1) 
                continue;

            position.height = base.GetPropertyHeight(prop, null);
            EditorGUI.PropertyField(position, prop, new GUIContent(prop.displayName));
            position.y += base.GetPropertyHeight(prop, new GUIContent(prop.displayName)) + EditorGUIUtility.standardVerticalSpacing;
        }
        EditorGUI.EndProperty();
        
        EditorGUI.EndDisabledGroup();
    }
}

마무리

사실 구현만 하자면 간단히 끝날 수 있는 부분이었지만, 여러 가지 고려하다보니 중요한 개념들에 대해서 배울 수 있게 되었다

나중에 다시 쓰일 테니 결코 시간 낭비한 게 아니라 믿는다…


참고 사이트

Generic List In Custom Editor Window

 

Generic List In Custom Editor Window

Hi: I am trying to make a custom editor window with a grneric list. the same as one in the inspector when you attach it on a gameobject. using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; [System.Serializable]

discussions.unity.com

[에디터 확장 입문] 번역 10장 PropertyDrawer

 

[에디터 확장 입문] 번역 10장 PropertyDrawer

<주의> 원문의 작성 시기는 2016년경으로, 코드나 일부 설명이 최신 유니티 버젼과 다소 맞지 않을 ...

blog.naver.com