본문 바로가기

유니티 공부

유니티에 유용한 기능 에디터 프로그래밍 - 응용편

컴포넌트 GUI 커스터 마이징

 

 

유니티에서 인스팩터창에 있는 컴포넌트에 GUI를 커스터 마이징 할 수 있습니다.

커스터 마이징한 컴포넌트 GUI

 

만드는 방법은 커스터 마이징 할 스크립트 파일을 만들고 MonoBehaviour를 상속 받는 대신 Editor를 상속 시키고, 어트리뷰트로 [CustomEditor(커스텀할 클래스)] 작성해주면 됩니다.

[CustomEditor(typeof(CustomScript))]
public class CustromEditor : Editor
{
	...
}

 

그리고 커스터마이징할 클래스 타입을 지정한 뒤, 그 클래스에서 수정할 필드들을 SerializedProperty로 받을 변수를 선언해줍니다.

[CustomEditor(typeof(CustomScript))]
public class CustromEditor : Editor
{
    CustomScript targetRef;

    SerializedProperty targetObjectProp;
    SerializedProperty nameProp;
    SerializedProperty hpProp;
}

 

다음으로 OnEnable() 메서드에서는 게임 오브젝트가 활성화될 때 실행됩니다.
이때 serializedObject.FindProperty()를 사용하여 씬에 있는 CustomScript 컴포넌트의 필드(otherObject, myName, myHP)를 찾아 각각의 SerializedProperty 변수에 할당합니다.
마지막으로 target을 CustomScript 타입으로 캐스팅해 targetRef 변수에 저장합니다.

    private void OnEnable()
    {
        targetObjectProp = serializedObject.FindProperty($"{nameof(CustomScript.otherObject)}");
        nameProp = serializedObject.FindProperty($"{nameof(CustomScript.myName)}");
        hpProp = serializedObject.FindProperty($"{nameof(CustomScript.myHP)}");


        targetRef = (CustomScript)base.target;
    }

 

마지막으로 OnInspectorGUI() 메서드에서, 이전에 OnGUI()에서 작성했던 것처럼 인스펙터 창에 표시될 GUI를 재정의합니다.


먼저 serializedObject.Update();를 호출하여 변경된 값을 동기화해주고, 인스펙터 내에서 값이 수정된 후에는 serializedObject.ApplyModifiedProperties();를 호출해 변경 내용을 실제 객체에 반영합니다.


이렇게 하면 다른 곳에서 해당 값을 사용할 때도 데이터가 일치하게 유지됩니다.

    public override void OnInspectorGUI()
    {
        //base.OnInspectorGUI();
        
        serializedObject.Update();

        if (hpProp.intValue < 500)
        {
            GUI.color = Color.red;
        }
        else
        {
            GUI.color = Color.green;
        }

        hpProp.intValue = EditorGUILayout.IntSlider("HP 값", hpProp.intValue, 0, 10000);

        EditorGUILayout.BeginHorizontal();
        {
            GUI.color = Color.blue;
            EditorGUILayout.PrefixLabel("이름");
            GUI.color = Color.white;
            nameProp.stringValue = EditorGUILayout.TextArea(nameProp.stringValue);
        }

        EditorGUILayout.EndHorizontal();

        EditorGUILayout.PropertyField(targetObjectProp);

        serializedObject.ApplyModifiedProperties();
    }

 

여기서 중요한 용어

  • Editor
  • CustomEditor
  • SerializedProperty
  • SerializedObject

EditorWindow에 응용

 

 

이번에는 위에서 커스터마이징한 컴포넌트의 GUI를 EditorWindow에 연결해보겠습니다.
먼저, SerializedObject를 키로 하고 SerializedProperty 리스트를 값으로 가지는 Dictionary를 선언합니다.

Dictionary<SerializedObject, List<SerializedProperty>> Targets = new Dictionary<SerializedObject, List<SerializedProperty>>();

 

 

그다음 OnGUI() 메서드에서 CustomScript 컴포넌트를 가진 모든 오브젝트를 찾아, 각 오브젝트의 필드(otherObject, myName, myHP)를 SerializedProperty로 가져와 Targets 딕셔너리에 추가합니다.

 

 

            var allCustom = FindObjectsOfType<CustomScript>();

            if (allCustom != null)
            {
                for (int i = 0; i < allCustom.Length; i++)
                {
                    var so = new SerializedObject(allCustom[i]);
                    var props = new List<SerializedProperty>()
                    {
                        so.FindProperty(nameof(CustomScript.otherObject)),
                        so.FindProperty(nameof(CustomScript.myName)),
                        so.FindProperty(nameof(CustomScript.myHP)),

                    };

                    Targets.Add(so, props);
                }
            }

 

그리고 다음과 같이  foreach문을 사용해 EditorWindow에 그려줍니다. 여기서 중요한 점은 각 키에 들어 있는 필드가 그려지고 나면 마지막에 반드시 ApplyModifiedProperties(); 로 동기화 해주어야 합니다.

 

        foreach (var pair  in Targets)
        {
            EditorGUI.BeginChangeCheck();
            {
                EditorGUILayout.LabelField(pair.Key.targetObject.name, EditorStyles.boldLabel);

                EditorGUI.indentLevel++;
                {
                    foreach (var prop in pair.Value)
                    {
                        EditorGUILayout.PropertyField(prop);
                      
                    }
                }
                EditorGUI.indentLevel--;

                EditorGUILayout.LabelField("",GUI.skin.horizontalSlider);
            }
            if (EditorGUI.EndChangeCheck())
            {
                pair.Key.ApplyModifiedProperties();
            }
        }

 

EditorWindow에서 수정한 내용과 Inspector에서 수정한 내용이 항상 일치하도록 만들어야 합니다. 이를 위해 Update() 메서드에서, EditorWindow에 포커스가 없더라도 Targets에 등록된 SerializedObject를 갱신하여 EditorWindow와 Inspector의 값이 동일하게 유지되도록 합니다.

 

 

    private void Update()
    {
        if (isFocused == false)
        {
            foreach (var item in Targets)
            {
                item.Key.Update();
            }
        }

        Repaint();
    }


    private void OnFocus()
    {
       isFocused = true;

        foreach (var item in Targets)
        {
            item.Key.Update();
        }
    }

프로퍼티 GUI 재정의하기

 

위에서는 클래스 안에 필드에 대한 GUI를 수정했습니다. 이번에는 커스텀 클래스 안에 있는 필드에 GUI를 수정하는 방법에 대해 알아보겠습니다. 

커스텀 클래스에 PalyerData에 GUI 수정한 그림

 

먼저, 커스텀 클래스의 필드를 인스펙터에서 접근할 수 있도록 [Serializable]로 선언해줍니다.

[Serializable]
public class PlayerData
{
    public int hp;
    public string name;
    public PlayerClass thisClass;
}

 

그리고 인스펙터 창에서 PlayerData의 표시 방식을 커스터마이징하기 위해, PropertyDrawer를 상속받는 클래스를 새로 만들고,
[CustomPropertyDrawer(typeof(PlayerData))] 어트리뷰트를 사용해 해당 클래스를 PlayerData에 연결합니다.

 

[CustomPropertyDrawer(typeof(PlayerData))]
public class PlayerDataDrawer : PropertyDrawer
{

}

 

PropertyDrawer는 Unity가 “필드 단위로 그릴 때” 좌표(Rect)프로퍼티 정보(SerializedProperty) 를 함께 전달하기 때문에
일반적인 OnGUI() 대신 다음과 같이 정해진 시그니처(Rect, SerializedProperty, GUIContent)를 반드시 사용해야 합니다.

 

그리고 ProeperttyDrawer: OnGUI()에서는 GUILayout/EditorGUILayout 류의 자동 레이아웃 클래스 지원 안합니다.

  public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
   {
   }

SceneView에 응용

 

씬뷰에서 커스텀 에디터를 구현 할 수 있습니다. 하는 방법은 씬 뷰에 다가 드롱잉을 할 수 있는 콜백 함수를 연결 시켜주면 됩니다.

// OnSceneGUI는 로컬 함수 
 SceneView.duringSceneGui += OnSceneGUI;

 

그리고 Handles 클래스를 사용하면 씬(Scene) 뷰에서 라인, 텍스트, 버튼 등을 GUI처럼 원하는 형태로 그릴 수 있습니다.

 

    private void OnSceneGUI(SceneView obj)
    {
        // Handles는 씬 뷰에 어떤 것을 드로잉 하기 위해서 사용하는 클래스
        Handles.Label(targetRef.transform.position, $"I am : {targetRef.gameObject.name}");

        var otherObjs = FindObjectsOfType<CustomScript>();

        for (int i = 0; i < otherObjs.Length; i++)
        {
            if (this.targetRef != otherObjs[i])
            {
                var pos = otherObjs[i].transform.position;
                Handles.DrawLine(this.targetRef.transform.position, pos);

                Handles.color = Color.red;
                Handles.DrawWireCube(pos, Vector3.one);
                Handles.color = Color.white;
            }
        }

        Handles.DrawWireCube(targetRef.transform.position, new Vector3(2,3,2));

        Handles.BeginGUI();
        {
            if (GUILayout.Button("Move Right!"))
            {
                targetRef.transform.position += Vector3.right;
            }

            if (GUILayout.Button("Move Left!"))
            {
                targetRef.transform.position += Vector3.left;
            }
        }
        Handles.EndGUI();

    }

에셋 파일 관리 및 조작하는 방법

 

  • AssetDatabase 클래스는 Unity 프로젝트 내 에셋 파일을 관리하고 조작하는 데 사용됩니다.
    주요 기능은 다음과 같습니다.
    • AssetDatabase.FindAssets("검색어")
      → 현재 프로젝트에서 검색어와 일치하는 모든 에셋의 GUID를 반환합니다.
      • 검색어
        • 타입기반 검색 ex) "t:Texture", "t:Material", "t:ScriptableObject"
        • 특정 문자열 ex) "Player"
        • 타입과 이름 조합 ex)"Player t:Texture"
        • 레이블기반 검색 ex) "l:UI"
    • AssetDatabase.GUIDToAssetPath("에셋GUID")
      → 해당 GUID에 연결된 에셋의 경로(Path)를 반환합니다.
    • AssetDatabase.CreateAsset(Object asset, string path)
      → 새로운 에셋을 지정한 경로에 생성합니다.

유저의 오브젝트 선택 직접 제어하기

 

Selection 클래스는 에디터에서 오브젝트를 선택하거나 현재 선택 상태를 제어할 수 있는 기능을 제공합니다.
예를 들어, EditorWindow에서 다음과 같은 작업이 가능합니다.

  • 프로젝트나 씬(Scene)에 있는 모든 오브젝트를 선택
  • 특정 타입의 오브젝트만 선택
  • 현재 선택된 오브젝트를 기반으로 핑(Ping) 표시
       if (GUILayout.Button("모든 Scene Obejct 선택하기"))
       {
           var targets = FindObjectsOfType<GameObject>(true);
           if (targets != null)
           {
               Selection.objects = targets;
           }
       }

       if (GUILayout.Button("모든 Text 선택하기"))
       {
           var targets = FindObjectsOfType<UnityEngine.UI.Text>();

           if (targets != null)
           {
               GameObject[] gos = new GameObject[targets.Length];
               for (int i = 0; i< targets.Length; i++)
               {
                   gos[i] = targets[i].gameObject;
               }

               Selection.objects = gos;
           }
       }

       if (GUILayout.Button("선택한 오브젝트 핑 찍기"))
       {
           if (Selection.objects !=  null && Selection.objects.Length == 1)
           {
               EditorGUIUtility.PingObject(Selection.objects[0]);
           }
       }

타 Editor 훔쳐오기

 

EditorWindow에서 선택한 오브젝트의 컴포넌트를 가져와 표시할 수 있습니다. 방법은 다음과 같습니다. 먼저 Selection 클래스를 사용하여 현재 선택된 오브젝트를 가져오고, 그 오브젝트를 기반으로 미리 선언한 Editor 변수에 Editor.CreateEditor()를 이용해 Editor 인스턴스를 생성하여 할당합니다.

   Editor duplicatedEditor;    
    
    if (Selection.objects != null && Selection.objects.Length == 1)
     {
         var target = Selection.objects[0];
         
         duplicatedEditor = Editor.CreateEditor(target);
     }

 

그 다음에 선택한 오브젝트에 모든 컴포넌트를 가져와서 미리 선언한 Editor[] 배열에  Editor.CreateEditor()를 이용해 Editor 인스턴스를 생성하여 할당합니다.

                    var allComps = go.GetComponents(typeof(Component));

                    if (allComps != null)
                    {
                        dupulicatedDetailEditor = new Editor[allComps.Length];
                        for (int i = 0; i < allComps.Length; i++)
                        {
                            detailFoldOut.Add(false);
                            dupulicatedDetailEditor[i] = Editor.CreateEditor(allComps[i]);
                        }

                    }

 

마지막으로 duplicatedEditor != null이 아니면 다음과 같이 EditorWindow에 해당 오브젝트에 GUI를 그려줍니다.

 

        if (duplicatedEditor != null)
        {
            duplicatedEditor.DrawHeader();
            duplicatedEditor.OnInspectorGUI();

            if (dupulicatedDetailEditor != null)
            {
                for (int i = 0; i < dupulicatedDetailEditor.Length; i++)
                {
                    detailFoldOut[i] = EditorGUILayout.Foldout(detailFoldOut[i], $"{dupulicatedDetailEditor[i].GetType()}");

                    if (detailFoldOut[i])
                    {
                        dupulicatedDetailEditor[i].OnInspectorGUI();
                    }
                }
            }
        }

에디터 전용 비휘발성 값 관리하기

 

  • EditorWindow에서는 값을 저장하고 불러올 수 있습니다.
    이때 EditorPrefs를 사용하면 PlayerPrefs와 유사하게 키-값(Key-Value) 형식으로 데이터를 관리할 수 있습니다.
    • 저장: EditorPrefs.SetXXX(key, value)
    • 불러오기: EditorPrefs.GetXXX(key)
    • 삭제: EditorPrefs.DeleteKey(key)

'유니티 공부' 카테고리의 다른 글

유니티에 유용한 기능 에디터 프로그래밍 - 기본편  (1) 2025.10.13
UIToolkit 튜토리얼  (0) 2025.07.04
UI Toolkit  (0) 2025.06.29
Object Pooling  (2) 2025.06.09
유니티 게임 WebGL 빌드 하기  (0) 2025.06.06