UnityEvent, UnityAction and Delegate
유니티에서 버튼에 CallBack을 등록시키기 위해서 에디터에서 클래스와 메쏘드를 지정하는 방법이 있고, 스크립트에서 직접 등록하는 방법이 있다.
이 때 등장하는 것이 UnityEvent이다. UnityEvent는 MonoBehavior를 상속받는 모든 클래스에 사용가능하다. 일단 선언이 되면 Editor에서 CallBack 함수를 등록하는 GUI가 추가되는 것을 볼 수 있다.
간단하기야 당연히 이렇게 Editor에서 Class의 Method를 지정해 주는 것이 간단하지만, 실제로는 에셋과 스크립트 간의 버젼 충돌을 피하고 원활한 업데이트를 위해서 스크립트로 이벤트를 집어 넣어주는 경우가 많다. 그 때 쓰이는 함수가 AddListener이다.
1 2 3 |
public void AddListener(Events.UnityAction call); |
인자로 들어가는 것이 Events.UnityAction인데 이 UnityAction은 유니티에서 엔진에서 사용하려고 미리 만들어 놓은 C# Delegate이다. 정의는 아래와 같이 void부터 4개의 인자를 가지는 Delegate까지 커버한다.
1 2 3 4 5 6 7 8 9 10 |
namespace UnityEngine.Events { public delegate void UnityAction(); public delegate void UnityAction<T0>(T0 arg0); public delegate void UnityAction<T0, T1>(T0 arg0, T1 arg1); public delegate void UnityAction<T0, T1, T2>(T0 arg0, T1 arg1, T2 arg2); public delegate void UnityAction<T0, T1, T2, T3>(T0 arg0, T1 arg1, T2 arg2, T3 arg3); } |
그렇다면 해당 Delegate를 등록받는 onClick 이라는 녀석은 무엇일까?
바로 이 onClick이 UnityEventBase를 상속받는 UnityEvent 클래스의 Instance이다. 즉, 이 Class의 Instance에 UnityAction을 Register하는 것이다. 그런 다음 해당 클래스의 Invoke 메소드가 호출되면서 모든 등록된 Delegate들이 실행되게 된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class UnityEvent : UnityEventBase { [Serializable] public class UnityEvent : UnityEventBase { ... } [Serializable] public abstract class UnityEvent<T0> : UnityEventBase { ... } [Serializable] public abstract class UnityEvent<T0, T1> : UnityEventBase { ... } [Serializable] public abstract class UnityEvent<T0, T1, T2> : UnityEventBase { ... } [Serializable] public abstract class UnityEvent<T0, T1, T2, T3> : UnityEventBase { ... } } |
UnityEvent가 UnityAction을 취하는 방법을 살펴 보면,
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 |
using UnityEngine; using System.Collections; using UnityEngine.Events; public class UnityEventTest : MonoBehaviour { int state = 0; void Update() { if (Input.GetKeyDown (KeyCode.A)) { switch(state) { case 0: FuncA(); break; case 1: FuncB(); break; case 2: FuncC(); break; case 3: FuncD(); break; case 4: FuncE(); break; case 5: FuncF(); break; case 6: FuncG(); break; case 7: FuncA();FuncB();FuncC();FuncD();FuncE();FuncF();FuncG(); break; } state = (++state>7) ? 0 : state; } } public void FuncA() { Debug.Log("Air"); } public void FuncB() { Debug.Log("Baby"); } public void FuncC() { Debug.Log("Cat"); } public void FuncD() { Debug.Log("Do"); } public void FuncE() { Debug.Log("Ear"); } public void FuncF() { Debug.Log("Fly"); } public void FuncG() { Debug.Log("Good"); } } |
위 코드에서 A키를 8회 눌렀을 때 출력은 아래와 같다. 순서대로 찍히고 마지막에 다 찍힌다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
1> Air 2> Baby 3> Cat 4> Do 5> Ear 6> Fly 7> Good 8> Air Baby Cat Do Ear Fly Good |
동일한 기능을 하는 코드를 UnityEvent와 UnityAction을 이용하면 아래와 같이 구현할 수 있다.
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 |
using UnityEngine; using System.Collections; using UnityEngine.Events; using UnityEngine.UI; public class UnityEventTest : MonoBehaviour { public UnityAction _unityAction; public UnityEvent[] _event = new UnityEvent[8]; int state = 0; void Start () { //create unityaction delegate UnityAction []action = new UnityAction[8]; action[0] = new UnityAction(FuncA); action[1] = new UnityAction(FuncB); action[2] = new UnityAction(FuncC); action[3] = new UnityAction(FuncD); action[4] = new UnityAction(FuncE); action[5] = new UnityAction(FuncF); action[6] = new UnityAction(FuncG); action[7] = new UnityAction(FuncA); action[7] += new UnityAction(FuncB); action[7] += new UnityAction(FuncC); action[7] += new UnityAction(FuncD); action[7] += new UnityAction(FuncE); action[7] += new UnityAction(FuncF); action[7] += new UnityAction(FuncG); //register for(int i=0; i<8; i++) { _event[i] = new UnityEvent(); _event[i].AddListener (action[i]); } } void Update () { if (Input.GetKeyDown (KeyCode.A)) { _event[state].Invoke (); state = (++state>7) ? 0 : state; } } public void FuncA() { Debug.Log("Air"); } public void FuncB() { Debug.Log("Baby"); } public void FuncC() { Debug.Log("Cat"); } public void FuncD() { Debug.Log("Do"); } public void FuncE() { Debug.Log("Ear"); } public void FuncF() { Debug.Log("Fly"); } public void FuncG() { Debug.Log("Good"); } } |
이렇게 하면 코드량은 조금 더 길어 보일 지 모르나, 각 이벤트에 등록된 UnityAction의 추가(AddListener) 및 삭제(RemoveListener)가 동적으로 자유로우므로, 마치 Observer Pattern을 적용한 느낌도 나면서 훨씬 더 유연한 코드 작성이 가능하다. (사실 UnityEvent는 미리 구현하여 내장된 Observer Pattern으로 보는 것이 맞을 것이다.)
아래와 같이 추상 클래스인 UnityEvent<T0>를 상속하여 선언함으로써 인자를 넘겨 주는 타입의 UnityAction을 호출할 수도 있다. 인자를 4개 받는 것까지 확장 가능하다.
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 |
public class MyEvent: UnityEvent <int> {} public class UnityEventTest : MonoBehaviour { public UnityAction<int> _unityAction; public MyEvent[] _event = new MyEvent[8]; int state = 0; void Start () { //create unityaction delegate UnityAction<int> []action = new UnityAction<int>[8]; action[0] = new UnityAction<int>(FuncA); action[1] = new UnityAction<int>(FuncB); action[2] = new UnityAction<int>(FuncC); action[3] = new UnityAction<int>(FuncD); action[4] = new UnityAction<int>(FuncE); action[5] = new UnityAction<int>(FuncF); action[6] = new UnityAction<int>(FuncG); action[7] = new UnityAction<int>(FuncA); action[7] += new UnityAction<int>(FuncB); action[7] +=new UnityAction<int>( FuncC); action[7] += new UnityAction<int>(FuncD); action[7] += new UnityAction<int>(FuncE); action[7] += new UnityAction<int>(FuncF); action[7] += new UnityAction<int>(FuncG); //register for(int i=0; i<8; i++) { _event[i] = new MyEvent(); _event[i].AddListener (action[i]); } } void Update () { if (Input.GetKeyDown (KeyCode.A)) { _event[state].Invoke (100); state = (++state>7) ? 0 : state; } } public void FuncA(int cnt) { Debug.Log("Air:" + cnt); } public void FuncB(int cnt) { Debug.Log("Baby:" + cnt); } public void FuncC(int cnt) { Debug.Log("Cat:" + cnt); } public void FuncD(int cnt) { Debug.Log("Do:" + cnt); } public void FuncE(int cnt) { Debug.Log("Ear:" + cnt); } public void FuncF(int cnt) { Debug.Log("Fly:" + cnt); } public void FuncG(int cnt) { Debug.Log("Good:" + cnt); } } |
Unity에서 사용하는 Mono 2.x는 C# 3.0까지는 안전하게 지원하므로, C# 3.0 버젼의 Lambda를 이용할 수 있다. Lambda expression은 anonymous function의 일종으로 이를 이용하면 간단히 delegate를 생성 가능하다. 따라서, 아래와 같이 동일 기능을 더 간단히 구현할 수 있다. Lambda 정의 Ref
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 |
using UnityEngine; using System.Collections; using UnityEngine.Events; using UnityEngine.UI; public class MyEvent: UnityEvent <int> {} public class UnityEventTest : MonoBehaviour { public MyEvent[] _event = new MyEvent[8]; int state = 0; void Start () { //create unityaction delegate UnityAction<int> []action = new UnityAction<int>[8]; action[0] = (cnt) => { Debug.Log ("Air:"+cnt); }; action[1] = (cnt) => { Debug.Log ("Baby:"+cnt); }; action[2] = (cnt) => { Debug.Log ("Cat:"+cnt); }; action[3] = (cnt) => { Debug.Log ("Do:"+cnt); }; action[4] = (cnt) => { Debug.Log ("Ear:"+cnt); }; action[5] = (cnt) => { Debug.Log ("Fly:"+cnt); }; action[6] = (cnt) => { Debug.Log ("Good:"+cnt); }; action[7] = (cnt) => { Debug.Log ("Air:"+cnt); Debug.Log ("Baby:"+cnt); Debug.Log ("Cat:"+cnt); Debug.Log ("Do:"+cnt); Debug.Log ("Ear:"+cnt); Debug.Log ("Fly:"+cnt); Debug.Log ("Good:"+cnt); }; //register for(int i=0; i<8; i++) { _event[i] = new MyEvent(); _event[i].AddListener (action[i]); } } void Update () { if (Input.GetKeyDown (KeyCode.A)) { _event[state].Invoke (100); state = (++state>7) ? 0 : state; } } } |
또한, C# 2.0에서부터 도입된, delegate-method-group-conversion에 의해서 바로 Method name을 Delegate에 Assign할 수도 있다.
이러한 모든 방법들을 다 Event를 거는데 활용해보면, 결과적으로 다음 코드와 같이 다양한 형태를 받는 AddListener가 나올 수 있게 된다.
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 |
using UnityEngine; using System.Collections; using UnityEngine.Events; using UnityEngine.UI; public class UnityEventTest : MonoBehaviour { int _index = 0; Button _btn; void Start() { GameObject go = new GameObject("createdGO"); go.AddComponent<Button>(); _btn = go.GetComponent<Button>(); //#1 lambda expression _btn.onClick.AddListener(() => Method1(_index)); //#2 assign direct method (delegate-method-group-conversion) _btn.onClick.AddListener(Method2); //#3 unityaction with lambda expression UnityEngine.Events.UnityAction action = ()=>{ Method3(_index);}; _btn.onClick.AddListener(action); //#4 delegate _btn.onClick.AddListener(delegate{ Method4(_index); }); } void Update () { if (Input.GetKeyDown (KeyCode.A)) { _btn.onClick.Invoke(); } } void Method1(int index) { Debug.Log("Method1:" + index); } void Method2() { Debug.Log("Method2");} void Method3(int index) { Debug.Log("Method3:" + index); } void Method4(int index) { Debug.Log("Method4:" + index); } } |
출력은 아래와 같고 버튼 클릭 시에 모두 잘 동작한다.
1 2 3 4 5 6 |
Method1:0 Method2 Method3:0 Method4:0 |
UnityEvent와 UnityAction에 대한 이러한 이해를 기반으로 Unity Official Tutorial 에 나와있는 EventManager Tutorial를 살펴보면 이해가 훨씬 쉽다.
P.S. Delegate를 다룰 때 많이 보게되는 event keyword는 위에서 언급한 UnityEvent와는 다른 개념이다. 이는 Private Field를 생성해주는 키워드이다. 이와 관련해서는 StackOverflow에서 Jon Skeet이 잘 설명해주고 있다. Why do we need the “event” keyword while defining events?
An event is fundamentally like a property – it’s a pair of add/remove methods (instead of the get/set of a property). When you declare a field-like event (i.e. one where you don’t specify the add/remove bits yourself) a public event is created, and a private backing field. This lets you raise the event privately, but allow public subscription. With a public delegate field, anyone can remove other people’s event handlers, raise the event themselves, etc – it’s an encapsulation disaster.
“이벤트는 근본적으로 Property와 비슷하다. Property가 get/set 을 가지는 반면 add/remove method를 가지고 있다. Field-like event를 선언하게 되면(명시적인 add/remove 선언을 하지 않은), public event가 만들어지고, back단에 private field가 만들어진다. 이로써 public하게 등록 가능한, private event가 만들어지는 것이다. 단순한 Public delegate field로써는 다른 누구든지 해당 delegate 이벤트를 제거하거나 호출할 수 있고 이는 encapsulation 재앙을 가져온다.”
따라서 기능적인 관점에서 delegate 앞에 event keyword를 붙이나 안붙이나 비슷하지만, Encapsulation의 관점에서 본다면 event를 붙이게 되면 Access Contorl을 가능하게 할 수 있다. (선언된 Class 외부에서 Invoke, Clear, Assign(+=, -=제외)을 할 수 없다.)