WinCNT

Unity의 EditorWindow를 한 번 알아보자 그 두번째! 이번엔 Reflection과 Action으로 Method를 실행해보자! 본문

Unity/Unity 관련

Unity의 EditorWindow를 한 번 알아보자 그 두번째! 이번엔 Reflection과 Action으로 Method를 실행해보자!

WinCNT_SSS 2024. 3. 28. 09:49

서론

저번 글에 이어지는 EditorWindow 내용이다

https://wincnt-shim.tistory.com/406

 

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

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

wincnt-shim.tistory.com


테스트를 위해 선택한 GameObject를 Count하는 메소드 작성

의미 없는 메소드를 만들어도 슬플 뿐이니 무엇인가 기능을 하나 만들어서 테스트해보자

이번 대상은 선택한 GameObject를 Count하는 메소드이다(최적화는 나중에 생각하자…)

#region Selected GameObject Vertex-Count
private void DrawSelectionVertexCounter()
{
    _selectionGameObjects = Selection.gameObjects.ToList();
    EditorGUILayout.BeginVertical(GUI.skin.GetStyle("HelpBox"));
    EditorGUILayout.LabelField("Selected GameObject Vertex-Count", EditorStyles.boldLabel);
    EditorGUI.indentLevel++;
    {
        EditorGUILayout.IntField("Skin", CountMeshVertex(GetAllSkinnedMeshRenderers(_selectionGameObjects)));
        EditorGUILayout.IntField("mesh", CountMeshVertex(GetAllMeshFilter(_selectionGameObjects)));
    }
    EditorGUI.indentLevel--;
    EditorGUILayout.EndVertical();
    
    Repaint();
}
#endregion

#region Private Methods
private List<SkinnedMeshRenderer> GetAllSkinnedMeshRenderers(List<GameObject> rootGameObject)
{
    var meshes = new List<SkinnedMeshRenderer>();
    foreach (var gameObject in rootGameObject)
        meshes.AddRange(gameObject.GetComponentsInChildren<SkinnedMeshRenderer>(true));
    return meshes.OrderByDescending(x => x.sharedMesh.vertexCount).ToList();
}
private List<MeshFilter> GetAllMeshFilter(List<GameObject> rootGameObject)
{
    var meshes = new List<MeshFilter>();
    foreach (var gameObject in rootGameObject)
        meshes.AddRange(gameObject.GetComponentsInChildren<MeshFilter>(true));
    return meshes.OrderByDescending(x => x.sharedMesh.vertexCount).ToList();
}

private static int CountMeshVertex(List<SkinnedMeshRenderer> list)
{
    return list
        .Where(i => i is not null)
        .Where(i => i.gameObject.activeInHierarchy)
        .Sum(i => i.sharedMesh.vertices.Length);
}
private static int CountMeshVertex(List<MeshFilter> list)
{
    return list
        .Where(i => i is not null)
        .Where(i => i.gameObject.activeInHierarchy)
        .Sum(i => i.sharedMesh.vertices.Length);
}
#endregion

 

위의 메소드들을 만들고 OnGUI에서 실행하도록 추가하자!

private List<GameObject> _selectionGameObjects = new List<GameObject>();

private void OnGUI()
{
    DrawSelectionVertexCounter();

    //...생략...
}

 

짜잔~!


Custom Attribute 만들기

다음으로 Reflection로 찾을 Attribute를 만들었다

예전과 같이 PropertyAttribute를 사용하고 싶었지만 이번엔 Method가 대상이라 사용할 수 없었다

어쩔 수 없이 Attribute를 상속받은 OnInspectorGUIAttribute를 만들었다

using System;
public class OnInspectorGUIAttribute : Attribute { }

 

그리고 DrawSelectionVertexCounter() 위에 장착!

[OnInspectorGUI]
private void DrawSelectionVertexCounter()
{
    // 생략
}

Reflection로 Custom Attribute 찾기!

전에 구현했던 GetTargetProperty의 응용

이번에는 GetMethods()로 MethodInfo를 찾는다는 게 차이점이다

그리고 찾은 메소드는 나중에 실행하기 위해 Delegate.CreateDelegate로 Action를 만들어서 리스트에 따로 보관해뒀다

private List<Action> targetMethodList = new List<Action>();
SerializedObject targetObject;

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

private void GetTargetMethod(SerializedObject serializedObject, SerializedProperty property)
{
    try
    {
        var type = serializedObject.targetObject.GetType();
        var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
        if (methods.Length != 0)
        {
            foreach (var method in methods)
            {
                var attributes = method.GetCustomAttributes(true);
                foreach (var attr in attributes)
                {
                    if (attr.GetType() == typeof(OnInspectorGUIAttribute))
                    {
                        Action action = (Action) Delegate.CreateDelegate(typeof(Action), serializedObject.targetObject, method);
                        targetMethodList.Add(action);
                    }
                }
            }
        }
        // duplicates remove
        targetMethodList = targetMethodList.Distinct().ToList();
    }
    catch (Exception e)
    {
        Debug.Log(e);
        throw;
    }
}

나머지는 실행해줄 뿐!

이제 targetMethodList에 보관한 메소드를 실행시키면 끝!

private List<GameObject> _selectionGameObjects = new List<GameObject>();

private void OnGUI()
{
    // DrawSelectionVertexCounter();

    if (targetObject != null)
    {
        targetObject.Update();

        foreach (var method in targetMethodList)
        {
            method.Invoke();
        }
        
        // ...생략...
        
        targetObject.ApplyModifiedProperties();
    }
    //...생략...
}

 

짜잔


결과

다른 메소드에 붙여도 잘 되나 실험해보자

[OnInspectorGUI]
private void DrawSelectionVertexCounter()
{
    _selectionGameObjects = Selection.gameObjects.ToList();
    EditorGUILayout.BeginVertical(GUI.skin.GetStyle("HelpBox"));
    EditorGUILayout.LabelField("Selected GameObject Vertex-Count", EditorStyles.boldLabel);
    EditorGUI.indentLevel++;
    {
        EditorGUILayout.IntField("Skin", CountMeshVertex(GetAllSkinnedMeshRenderers(_selectionGameObjects)));
        EditorGUILayout.IntField("mesh", CountMeshVertex(GetAllMeshFilter(_selectionGameObjects)));
    }
    EditorGUI.indentLevel--;
    EditorGUILayout.EndVertical();
    
    Repaint();
}
[OnInspectorGUI]
private void DrawSelectionVertexCounter2()
{
    // DrawSelectionVertexCounter과 동일
}
[OnInspectorGUI]
private void DrawSelectionVertexCounter3()
{
    // DrawSelectionVertexCounter과 동일
}

 

문제 없이 잘 동작한다!


마무리

필자는 필요한 어트리뷰트만 추가해서 대응했지만 Odin 같은 에셋은 이미 설계부터 제대로 해놨겠지…문득 새삼 Odin이 대단하게 느껴지네

앞으로 또 추가해야 할 일이 생겨도 이제는 이 방식으로 할지 모르겠다구현이 너무 귀찮음


참고 사이트

이번엔 딱히 딱 맞는 참고 사이트는 없었다