버튼 어트리뷰트 여행기
자꾸 까먹어서 뒤적뒤적하다가 작성합니다.
목표 : 언제서나 인스펙터에서 테스트용으로 사용할 버튼을 쉽고 간단하게 한줄로 만들고 싶다.
머리
테스트. 중요합니다. 런타임이든 에디터상이든 원하는 기능을 즉각적으로 바로 눌러 테스트할 수 있다는건 아주 좋습니다. (반대로 이게 안되는 스탠드얼론 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");
}
내부에 클래스를 둔 것이 아닌 상속의 베이스 클래스이기 때문에 버튼을 누르는 상황에서의 접근에 제약이 생깁니다. 하지만 뎁스 한 번쯤이야.. 귀찮음에 비하면… 대신 장점을 조금 빼고 파일이 추가될 때 마다 필요한 노가다를 확 줄입니다.
파일을 분리하고, 어디 짱박을 수 있으니 코드가 좀 더 길어져도 괜찮습니다.
따라서 매 렌더마다 모든 함수를 검사하는것은 비효율적이기 때문에 적당히 값이 바뀔때나 처음에 구워두도록 합시다.
솔직히 이정도까지만 가면 큰 문제 없습니다.
추가적으로 결국 해당스크립트에 대한 버튼이기 때문에 상속한다고 부모클래스의 버튼 어트리뷰트가 동작하진 않습니다.
꼬리
사실 odin 인스펙터사면 모든게 행복해집니다.
하지만 버튼 기능만 가져와서 쓰기엔 이것저것 너무 많아서
짧게 버튼만 구현해볼 수 있도록 한 번 지식을 뒤져봤습니다.
그러고보니 생각보다 프로그래밍 게시글은 하나도 안올렸네요. 짬짬히 (까먹는 것들을) 써야겠습니다.
https://assetstore.unity.com/packages/tools/utilities/odin-inspector-and-serializer-89041