호우동의 개발일지

Today :

article thumbnail

목표

https://howudong.tistory.com/149

 

[Unity 3D] GUI를 이용한 인스펙터(Inspector) 커스터마이징

목표 오브젝트에 스크립트를 넣어 인스펙터 뷰를 커스터마이징 직렬화(Serialization) - 해당 필드를 유니티가 인식할 수 있는 상태로 만드는 것 - 유니티가 처리할 수 있는 형태로 만드는 것 - Inspect

howudong.tistory.com

해당 포스팅에서 했던 GUI를 이용한 인스펙터 커스터마이징을 응용하여,
자신이 만든 에디터 창(Editor Window)과 인스펙터(Inspector)를 연결하겠다.

연결한다는 말은 에디터 창에서 프로퍼티의 값을 조절하면
인스펙터에도 적용되고, 그 반대도 적용되는 것을 말한다.

 

 


프로퍼티 값 초기화(가져오기/불러오기)


사전 준비

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

public class CustomScript : MonoBehaviour
{
    public GameObject otherObject;
    public string myName;
    public int myHp;
}

이는 인스펙터와 에디터 창에서 조절하고 싶은 프로퍼티를 가진 스크립트이다.
여기 안에 있는 오브젝트 이름과 타입만 기억하고 있으면 된다.

 


Dictionary 사용 이해

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

Dictinary란 (Key-Value) 쌍을 사용하는 자료구조인데,
리스트에는 Index가 있듯이,
Dictionary에는 Index 대신 Key가 존재한다.

키에는 다양한 데이터 타입이 가능하다.
Dictionary에서는 Key를 이용하여 Value를 얻는다.


여기서는 Key에 SerializedObject, Value에 List가 들어갔는데,

이 이유는 각 게임 오브젝트에 붙어있는 조정하고자 하는 프로퍼티를 
가진 스크립트(SerializedObject) 안에 있는
프로퍼티들을 List들에 추가하여 관리하기 위해서이다.

 


Button을 통한 프로퍼티 갱신(Dictionary 초기화)

private void OnGUI()
{
    if (GUILayout.Button("Refresh"))
    {
        Targets.Clear();

        // Scene에서 CustomScript를 가진 오브젝트를 모두 가져옴
        var allCustoms = FindObjectsOfType<CustomScript>();

        if(allCustoms != null)
        {
            foreach(CustomScript custom in allCustoms)
            {    
            // 대상 오브젝트를 직렬화하여 반환
                var so = new SerializedObject(custom);

                // 프로퍼티 리스트 생성
                var props = new List<SerializedProperty>()
                {
                    // 직렬화된 오브젝트에서 해당 이름의 프로퍼티를 가져옴
                    so.FindProperty(nameof(CustomScript.otherObject)),
                    so.FindProperty(nameof(CustomScript.myHp)),
                    so.FindProperty(nameof(CustomScript.myName)),
                };

                // 직렬화된 오브젝트(Key)와 프로퍼티들(Value)를 Dictionary로 추가
                Targets.Add(so, props);
            }
        }
    }
}

OnGUI()에서 버튼을 생성하면서 눌렀을 때의 동작을 추가할 것이다.

일단 초기화이기 때문에 Targets안에 있는 모든 것을 없앤다.

이후, FindObjectsofType()로 Scene에 있는 모든 오브젝트를 검사한다.
그중, CustomScript를 가지고 있는 모든 오브젝트를 가져와 CustomScript 배열로 담는다.

하나라도 있는 경우 오브젝트는 직렬화해서 Key로 쓰고
그 안에 있는 프로퍼티들은 리스트에 담아 Value로 만든다.

그리고 Key-value 쌍으로 Dictionary로 Targets에 담는다.

이렇게 하면 직렬화된 오브젝트와 프로퍼티를 갱신하는 과정이 끝난다.

 

 


인스펙터(Inspector)와 통신


인스펙터 값 계속 업데이트받기

bool isFocused;

private void Update()
{
    // 포커스를 잃어도 항시 업데이트를 하기 위해 
    if (!isFocused)
    {
        // 계속해서 업데이트
        foreach(var item in Targets) item.Key.Update();
        Repaint();
    }
}
// 포커스를 받는 순간(창에 들어오는 순간) 호출
private void OnFocus()
{
    isFocused = true;
    foreach (var item in Targets) item.Key.Update();
}

// 포커스를 잃는 순간(창에서 나가는 순간) 호출
private void OnLostFocus()
{
    isFocused = false;
}

값을 업데이트하는 것은 SerializedObject.Update() 함수를 이용한다.
여기서 isFocused 변수의 역할은 에디터 창의 포커스를 확인하는 것이다.

포커스를 왜 확인해야 할까?

OnGUI()는 에디터 창이 포커스를 받는 상태에서만 작동한다.


즉, 에디터 창에 포커스가 없는 상태에서 인스펙터에서
프로퍼티 값을 조절해도 에디터 창에는 업데이트되지 않는다.

그래서 isFocused를 사용하여 포커스를 잃을 시 isFocused = falsed를 하여 
Updated 문구 안에 item.key.update()와 Repaint()를 계속 호출하게 했다.

포커스 받는 상태에서는 값을 조절할 수 있기 때문에 인스펙터의 값을 갱신받다 보면
이상해질 수 있으므로 Update()를 꺼두기 위해 isFoucsed를 다시 true로 만든다.

 


인스펙터에 프로퍼티 값 보내기

프로퍼티 값을 보내기 위해 EditorGUI.BeginChangeCheck();
~ if(EditorGUI.EndChangeCheck()를 사용한다.

EditorGUI.BeginChangeCheck();
{
   ...
   코드블록
   ...
}
if(EditorGUI.EndChangeCheck()
{
   코드블록이 변경되면 실행될 코드
}

이와 같은 형식으로 사용한다.

 

BeginChangeCheck 블록 안에 감시할 사항(관찰할 대상)을 넣고,
그 사항이 변경되면 EndChangeCheck() 안의 블록을 실행한다.

private void OnGUI()
{
	// 직렬화된 전체 오브젝트를 가져옴
    foreach(var pair in Targets)
    {
        EditorGUI.BeginChangeCheck();
        {
            EditorGUILayout.LabelField(pair.Key.targetObject.name, EditorStyles.boldLabel);
            EditorGUI.indentLevel++; // inspector 들여쓰기 느낌 // 그림으로 설명하겠음 
            {
            	// 각각의 프로퍼티를 필드로 만듦
                foreach (var prop in pair.Value) EditorGUILayout.PropertyField(prop);
            }
            EditorGUI.indentLevel--;
            // 밑선 추가
            EditorGUILayout.LabelField("", GUI.skin.horizontalSlider);
        }
        // 프로퍼티 변경 시 프로퍼티 변경 사항을 인스펙터에 알림
        if (EditorGUI.EndChangeCheck()) pair.Key.ApplyModifiedProperties();
    }
}

ChangeCheck()를 이용해서 프로퍼티의 값들을 감시한다.


값이 변경되면 바로 Apply.ModifiedProperties를 이용해
그 오브젝트의 인스펙터에 바로 알리게 된다.

여기서 indentLevel은 인스펙터에 글자의 위치 같은 것인데,
쉽게 말해서 들여 쓰기 같은 것이라고 보면 된다.


indentLevel이 높을수록 안쪽으로 들어온다.(들여 쓰기가 많이 된다.)

이제 밑에 전체 코드를 올리고 한번 결과를 보겠다.

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

public class TestEditWindow : EditorWindow
{
    // Dictionary : key - value 형식
    // 게임오브젝트 하나하나마다 붙어있는 SerializedObject(CustomScript) 안에 있는 프로퍼티들을 추가하여 관리하기위해 사용
    Dictionary<SerializedObject, List<SerializedProperty>> Targets = new Dictionary<SerializedObject, List<SerializedProperty>>();
    bool isFocused;

    private void Update()
    {
        // 포커스를 잃어도 항시 업데이트를 하기 위해 
        if (!isFocused)
        {
            // 계속해서 업데이트
            foreach(var item in Targets) item.Key.Update();
            Repaint();
        }
    }

    private void OnGUI()
    {
        if (GUILayout.Button("Refresh"))
        {
            Targets.Clear();

            var allCustoms = FindObjectsOfType<CustomScript>();

            if(allCustoms != null)
            {
                foreach(CustomScript custom in allCustoms)
                {
                    var so = new SerializedObject(custom);
                    var props = new List<SerializedProperty>()
                    {
                        so.FindProperty(nameof(CustomScript.otherObject)),
                        so.FindProperty(nameof(CustomScript.myHp)),
                        so.FindProperty(nameof(CustomScript.myName)),
                    };

                    Targets.Add(so, props);
                }
            }
        }
        foreach(var pair in Targets)
        {
            EditorGUI.BeginChangeCheck();
            {
                EditorGUILayout.LabelField(pair.Key.targetObject.name, EditorStyles.boldLabel);
                EditorGUI.indentLevel++; // inspector 들여쓰기 느낌 
                {
                    foreach (var prop in pair.Value) EditorGUILayout.PropertyField(prop);
                }
                EditorGUI.indentLevel--;
                EditorGUILayout.LabelField("", GUI.skin.horizontalSlider);
            }
            if (EditorGUI.EndChangeCheck()) pair.Key.ApplyModifiedProperties();
        }
    }
    private void OnFocus()
    {
        isFocused = true;
        foreach (var item in Targets) item.Key.Update();
    }
    private void OnLostFocus()
    {
        isFocused = false;
    }

    [MenuItem("MyTool/OpenTool %g")]
    static void Open()
    {
        var myWindow = GetWindow<TestEditWindow>();
        myWindow.titleContent = new GUIContent() { text = "MyTool" };
    }
}

버튼을 누르기 전
버튼을 누르기 전
버튼을 누른 후
버튼을 누른 후


버튼을 누르니 CustomScript를 가지고 있는
4개의 게임 오브젝트가 에디터 창에 뜨는 것을 확인할 수 있다.

색깔 바뀜

에디터 창에서 프로퍼티 My HP를 바꾸자
인스펙터에서도 정상적으로 바뀌는 것을 확인할 수 있다.

결과

반대로 인스펙터에서 값을 조정해도
포커스를 잃은 에디터창에서도 My Hp가 정상적으로 값이 바뀐다.