Home 버튼 어트리뷰트 여행기
Post
Cancel

버튼 어트리뷰트 여행기

버튼 어트리뷰트 여행기

자꾸 까먹어서 뒤적뒤적하다가 작성합니다.

목표 : 언제서나 인스펙터에서 테스트용으로 사용할 버튼을 쉽고 간단하게 한줄로 만들고 싶다.

머리

테스트. 중요합니다. 런타임이든 에디터상이든 원하는 기능을 즉각적으로 바로 눌러 테스트할 수 있다는건 아주 좋습니다. (반대로 이게 안되는 스탠드얼론 VR / 빌드 필수 콘솔은 이제 화가 … ) 그걸 위해 스크립트에 함수를 호출하는 버튼을 넣습니다.

유니티는 버튼을 누르면 함수를 실행하게 할 수 있습니다

1
2
3
4
5
6
7
if (GUILayout.Button("ButtonName"))
{
		onclick?.Invoke();
		(target as Something)?.Foo();
		SingletonClass.Instance?.Fooooo();
		Nuclear.Launch();
}

이런 방식입니다.

이걸위해 에디터파일을 만들고 클래스이름 지어주고 커스텀에디터 이름이어주고 oninspectorGUI에서 버튼을 만들고 타겟 캐스팅하고…

벌써 복붙할게 네개입니다. 너무귀찮습니다.

따라서 좀 더 편하게 시작할 수 있도록 줄입니다.

본문

그러면 기억을 되새기며, 하나씩 구현을 해보고 불편한걸 고쳐보겠습니다.

1. 내부에 클래스파기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using System;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

public class ButtonSeries : MonoBehaviour
{
    private void FetchTask() { }
    
    private void Foo() { }

#if UNITY_EDITOR
    [CustomEditor(typeof(ButtonSeries))]
    public class ButtonSeriesEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            //Add Functions At Here
            Button((target as ButtonSeries).FetchTask);
            Button((target as ButtonSeries).Foo);

            //Local Func
            void Button(Action onclick)
            {
                if (GUILayout.Button("Run " + onclick.Method.Name))
                {
                    onclick?.Invoke();
                }
            }
        }
    }
#endif
}

클래스마다 내부에 박아버리는 방식입니다.

클래스 내부에 존재하기 때문에 멤버접근도 가능하기 때문에 테스트코드를 작성할땐 좋지만,

파일마다, 함수가 추가될 때 마다 노가다를 해야하며, 결과적으로 코드파일 자체가 늘어나는 형태입니다.

2. 리플렉션사용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using System;
using UnityEngine;
using System.Diagnostics;
using System.Reflection;

#if UNITY_EDITOR
using UnityEditor;

#endif

[AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = false)]
[Conditional("UNITY_EDITOR")]
public class ButtonAttribute : Attribute { }

public class ButtonSeries : MonoBehaviour
{
    [Button]
    private void FetchTask() { }

#if UNITY_EDITOR
    [CustomEditor(typeof(ButtonSeries))]
    public class ButtonSeriesEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            BuildButtons();
        }

        void BuildButtons()
        {
            var methods = target.GetType().GetMethods(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
            foreach (var method in methods)
            {
                var attributes = method.GetCustomAttributes(typeof(ButtonAttribute), false);
                if (attributes.Length > 0)
                {
                    if (GUILayout.Button("Run " + method.Name))
                    {
                        method.Invoke(target, null);
                    }
                }
            }
        }
    }
#endif
}

버튼 생성을 어트리뷰트와 리플렉션을 통해 자동으로 해줍니다.

기존과 동일한 장점, 추가로 함수가 추가될 때 마다 필요한 노가다 감소.

따라서 partial로 분리하든 파일분리하든 안보이게 숨길 순 있으며 에디터코드에서 특수화를 하기엔 좋지만… 파일마다는 여전히 노가다. 굉장히 귀찮습니다.

3. 상속

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//File ButtonAttribute.cs
using System;
using System.Reflection;
using System.Diagnostics; //Conditional
using System.Linq;
using System.Collections.Generic;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

[AttributeUsage(AttributeTargets.All, AllowMultiple = false, Inherited = false)]
[Conditional("UNITY_EDITOR")]
public class ButtonAttribute : Attribute { }

public class DevMono : MonoBehaviour
{
#if UNITY_EDITOR
    [CustomEditor(typeof(DevMono), true)]
    public class DevEditor : Editor
    {
        private List<MethodInfo> ButtonAttributeInfos = new List<MethodInfo>();

        private void OnEnable() => Initialize();

        private void OnValidate() => Initialize();

        private void Initialize()
        {
            var flag = BindingFlags.Static | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;

            ButtonAttributeInfos = target.GetType().GetMethods(flag)
                .Where(method => method.GetCustomAttributes(typeof(ButtonAttribute), false).Length > 0).ToList();
        }

        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            BuildButtons();
        }

        void BuildButtons()
        {
            foreach (var method in ButtonAttributeInfos)
            {
                if (GUILayout.Button("Run " + method.Name))
                {
                    method.Invoke(target, null);
                }
            }
        }
    }
#endif
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//File ButtonSeries.cs
using System;
using UnityEngine;
using System.Collections.Generic;

public class ButtonSeries : DevMono //상속만 바꾸면됨! 양산가능!
{
    [Button] //으아 편하다!
    private static void StaticFunction()
        => Debug.Log("Call StaticFunction");

    [Button]
    private void FetchTask()
        => Debug.Log("Call FetchTask");

    [Button]
    private void FooFooo()
        => Debug.Log("Call FooFooo");

}

내부에 클래스를 둔 것이 아닌 상속의 베이스 클래스이기 때문에 버튼을 누르는 상황에서의 접근에 제약이 생깁니다. 하지만 뎁스 한 번쯤이야.. 귀찮음에 비하면… 대신 장점을 조금 빼고 파일이 추가될 때 마다 필요한 노가다를 확 줄입니다.

파일을 분리하고, 어디 짱박을 수 있으니 코드가 좀 더 길어져도 괜찮습니다.

따라서 매 렌더마다 모든 함수를 검사하는것은 비효율적이기 때문에 적당히 값이 바뀔때나 처음에 구워두도록 합시다.

솔직히 이정도까지만 가면 큰 문제 없습니다.

Untitled

Untitled

추가적으로 결국 해당스크립트에 대한 버튼이기 때문에 상속한다고 부모클래스의 버튼 어트리뷰트가 동작하진 않습니다.

꼬리

사실 odin 인스펙터사면 모든게 행복해집니다.

하지만 버튼 기능만 가져와서 쓰기엔 이것저것 너무 많아서

짧게 버튼만 구현해볼 수 있도록 한 번 지식을 뒤져봤습니다.

그러고보니 생각보다 프로그래밍 게시글은 하나도 안올렸네요. 짬짬히 (까먹는 것들을) 써야겠습니다.

https://assetstore.unity.com/packages/tools/utilities/odin-inspector-and-serializer-89041

This post is licensed under CC BY 4.0 by the author.
Contents