MUdons 00 - ActiveToggle, ActiveList 추상클래스
https://github.com/Mascari4615/MUdons/commit/2763f45666b8930c75c905e4abb8944f262680fe
📀 머리글
원래 VRChat UdonSharp에서는 추상 클래스/메서드를 지원하지 않았는데, 혹시 업데이트가 되었나 싶어 시도해보니 컴파일도 잘 되고, 코드 그대로 빌드 테스트를 진행해도 별다른 문제도 보이지 않았다. 언제부터인가 지원되고 있었나보다.
상속은 기존에도 가능하긴 했지만, 추상 클래스를 사용할 수 있게 된 김에, 그동안 고쳐야겠다 생각하고 있던 ObjectActive
, ObjectActiveList
, CanvasGroupActive
, CanvasGroupActiveList
그리고 CameraActive
우동의 코드/구조 중복를 개선하고자 했다.
📀 문제
먼저 ~Active
우동들의 목적을 정리해본다.
~Active
우동들은 특정 상황에 특정 요소를 활성화/비활성화시키는 간단한 목적을 가지고 있다.
특정 요소의 타입은 GameObject
가 될 수도 있고, CanvasGroup
이 될 수도 있고, Camera
가 될 수도 있고, 내가 만든 MPickup
이 될 수도 있다.
어쨌거나 활성화/비활성화의 개념을 가질 수 있는 것이면 된다.
( 내가 만든 MPickup
은 여러 픽업 관련 기능과 함께 Enable
개념을 가지고 있다, Enable
의 값에 따라 해당 MPickup
의 콜라이더나 메쉬렌더러, 자식 오브젝트들이 활성화/비활성화되는 식이다. ) ( .. 이거 완전 ~Active
기능이잖아? 그렇다, MPickup
은 ~Acitve
기능을 가지고 있었다. 추후 MPickup
의 이런 부분들은 ~Active
우동으로 대체해도 좋을 것 같다. )
~Active
우동의 종류는 크게 두 가지로 나뉜다.
토글과 리스트.
토글 기능은 말그대로 요소들을 토글, 활성화/비활성화하는 기능이고,
리스트 기능은 여러 가지 요소들 중에서, 특정 상황에 맞게 특정 요소만을 활성화시키고 나머지는 비활성화시키는, 조금은 기능이 추가된 우동이다.
이런 토글, 리스트 기능들은, 똑같은 코드에 타입만 다른 채 ObjectActive
, CanvasGroupActive
, CameraActive
같이 여러 우동으로 존재하고 있다.
코드가 많이 중복되고 있었고, 이는 아름답지 않다 !
📀 추상화
똑같은 코드에 타입만 다르면.. 이거 일반화
잖아?
하지만 엄밀히 따져보면 똑같은 코드는 아니었다.
대상으로 하는 타입들은 개념적으로는
모두 활성화/비활성화 상태를 가지고 있지만, 서로 다른 모양으로 구현되어있기 때문에 ( GameObject.active
와 Camera.enabled
는 다르다 ! ) 이를 일반화하기엔 무리가 있다.
또 애초에 Unity 인스펙터에서 요소들의 참조를 직접 등록하는 것이 전제라, 하나의 클래스/메서드에 여러 타입을 위한 일반화 메서드를 만들 수는 없다.
하나의 클래스에 GameObject
, CanvasGroup
, Camera
타입을 Object
로 받아내거나 전부 하나하나 변수로 만들 수는 없으니까..
컴포넌트들이라도 Behaviour.enable
를 써서 일반화해보려고 했는데, UdonSharp에서는 Behaviour.enabled
의 set
을 지원하지 않았다.
그래서 ~Active
우동들의 공통된 개념과 기능은 미리 구현되어 있지만, 활성화/비활성화 처리는 추상화 메서드로 남겨둔 추상화 클래스를 만들고,
이를 각 타입마다 상속받아 활성화/비활성화 상태에 따라 각 타입 별 처리를 구현하도록 했다.
📀 코드 - Core (ActiveToggle
, ActiveList
)
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
57
58
59
60
61
62
63
64
65
public abstract class ActiveToggle : MBase
{
[Header("_" + nameof(ActiveToggle))]
[SerializeField] private bool defaultActive;
[Header("_" + nameof(ActiveToggle) + " - Options")]
[SerializeField] private MBool mBool;
private bool _active;
public bool Active
{
get => _active;
private set
{
_active = value;
UpdateActive();
}
}
private void Start()
{
Init();
}
private void Init()
{
SetActive(defaultActive);
if (mBool != null)
{
mBool.RegisterListener(this, nameof(UpdateValueByMBool));
UpdateValueByMBool();
}
UpdateActive();
}
protected abstract void UpdateActive();
public void SetActive(bool newActive)
{
MDebugLog($"{nameof(SetActive)}({newActive})");
if (mBool != null)
{
mBool.SetValue(newActive);
}
else
{
Active = newActive;
}
}
public void UpdateValueByMBool()
{
if (mBool)
Active = mBool.Value;
}
public void SetMBool(MBool mBool)
{
this.mBool = mBool;
UpdateValueByMBool();
}
}
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public abstract class ActiveList : MBase
{
[Header("_" + nameof(ActiveList))]
[SerializeField] private int defaultValue;
[Header("_" + nameof(ActiveList) + " - Options")]
[SerializeField] protected MValue mValue;
[SerializeField] protected ActiveListOption option;
[SerializeField] protected int targetIndex = NONE_INT;
private int _value;
public int Value
{
get => _value;
private set
{
_value = value;
UpdateActive();
}
}
private void Start()
{
Init();
}
private void Init()
{
SetValue(defaultValue);
if (mValue != null)
{
InitMValueMinMax();
mValue.RegisterListener(this, nameof(UpdateActiveByMValue));
}
UpdateActive();
}
/// <summary>
/// 대상이 되는 요소들의 수를 바탕으로 mValue.SetMinMaxValue
/// </summary>
protected abstract void InitMValueMinMax();
/// <summary>
/// ActiveListOption에 대하여 각 케이스 별로 구현
/// </summary>
/// <param name="value"></param>
protected abstract void UpdateActive();
public void SetValue(int newValue)
{
MDebugLog($"{nameof(SetValue)}({newValue})");
if (mValue != null)
{
mValue.SetValue(newValue);
}
else
{
Value = newValue;
}
}
public void UpdateActiveByMValue()
{
if (mValue)
Value = mValue.Value;
}
public void SetMValue(MValue mValue)
{
this.mValue = mValue;
InitMValueMinMax();
UpdateActiveByMValue();
}
}
📀 코드 - Impl (ObjectActive
, ObjectActiveList
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ObjectActive : ActiveToggle
{
[Header("_" + nameof(ObjectActive))]
[SerializeField] private GameObject[] activeObjects;
[SerializeField] private GameObject[] disableObjects;
protected override void UpdateActive()
{
MDebugLog($"{nameof(UpdateActive)}");
foreach (GameObject o in activeObjects)
o.SetActive(Active);
foreach (GameObject o in disableObjects)
o.SetActive(!Active);
}
}
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
public class ObjectActiveList : ActiveList
{
[Header("_" + nameof(ObjectActiveList))]
[SerializeField] private GameObject[] objectList;
// [SerializeField] private Behaviour[] behaviours; // 240819 : U#에서 .enabled set을 지원하지 않음
protected override void InitMValueMinMax()
{
int maxLen = Mathf.Max(objectList.Length);
mValue.SetMinMaxValue(0, maxLen);
}
protected override void UpdateActive()
{
MDebugLog($"{nameof(UpdateActive)}({Value})");
switch (option)
{
// MValue를 리스트의 인덱스로 사용할 때
// 예를 들어, MValue가 0일 때 리스트의 0번째 오브젝트만 활성화, 나머지는 비활성화
case ActiveListOption.UseMValueAsListIndex:
for (int i = 0; i < objectList.Length; i++)
{
if (objectList[i])
objectList[i].SetActive(i == Value);
}
break;
// MValue를 타겟 인덱스로 사용할 때
// 예를 들어, MValue가 0일 때 타겟 인덱스가 0이면 리스트의 모든 오브젝트 활성화, 아니면 비활성화
case ActiveListOption.UseMValueAsTargetIndex:
bool isTargetIndex = Value == targetIndex;
foreach (GameObject obj in objectList)
{
if (obj)
obj.SetActive(isTargetIndex);
}
break;
default:
MDebugLog($"{nameof(UpdateActive)}({Value}) - {option}, Invalid Option");
break;
}
}
}
📀 말꼬리
ObjectActiveList
의 경우, 프로젝트를 진행함에 따라 GameObject
뿐만 아니라 CanvasGroup
과 MPickup
도 함께 다루게 되었는데, 이번에 구조를 바꾸면서 GameObject
만 다루도록 수정했다.
CanvasGroup
은 별도의 CanvasGroupList
가 있으니 이것으로 대체하면 되고, MPickup
은 딱 한 프로젝트에서만 사용했던 터라, 다시 필요하게 된다면 CanvasGroupList
처럼 별도의 우동을 새로 만들어야 할 것 같다.
러나저러나 간단한 목적을 가진 우동들이라, 프로젝트마다 그때그때 새로 만들어도 되긴 하지만..
자주 쓰이는 기능은 계속 다시 만드는 것 보다는 한 번 잘 만들어서 재사용하는게 작업 효율이 좋다고 생각해서 이렇게 작은 모듈들을 하나하나 만들기 시작했다.
그런데 그렇게 만든 기능들을 다시 디벨롭하는 과정에서, 이전 프로젝트들의 우동 샘플들도 함께 유지보수하게 되는데, 이러나 그때그때 똑같은 기능을 새로 만드는 것 보다 더 많은 시간이 쓰게 되는 것 같기도 하다.
보통의 라이브러리들처럼 구조가 안정화되어, 다른 사람들도 구조변경 걱정없이 편히 쓸 수 있고, 나는 새 기능 개발에 더욱 신경쓸 수 있으면 좋겠다.
v1.0.0
을 향해.