이번에 제작한 EventDispatcher 는 싱글톤 클래스로 전역에 걸쳐 이벤트를 등록,제거, 전송하는 역할을 하는 클래스이다.
굉장히 기초적으로 제작하였는데 이는 info 매니저 혹은 data 매니저 등 다른 싱글톤 클래스들에 비해 사용법이 익숙하지가 않아 명시적으로 오버로딩 하여 구현한 메서드가 많았다.
기본적인 사용방법은 이렇다.
- 메서드를 다른 오브젝트 또는 스크립트에 전달해야 하는경우 Init()또는 Awake() 메서드 사용시 EventDispatcher 의 인스턴스를 불러와 메서드를 등록한다.
- 메서드 등록된 메서드는 EventDispatcher 의 딕셔너리에 Enum 상수 타입을 키로 저장되며 이는 메서드를 등록하고 자하는 사람이 명시적으로 상수 등록후 사용하기 위함이다
* 사실 동일키에 다른 메서드가 들어와도 Deletgate.Combine 메서드를 사용하여 멀티캐스트를 사용할수 있지만 현재 우리 프로젝트의 구조에서는 그럴일이 딱히 없기에 구현만 해두었다 ( 즉 필요 없다면 막아놓아도 무관하다) - 등록된 메서드를 Dispatch 할땐 EventDispatcher 의 Dispatch () 메서드를 사용하여 특정 이벤트 발생시 이벤트를 실행시킨다.
우선 코드는 아래와 같다.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
public class EventDispatcher
{
/// <summary>
/// 메서드 사용키 (EventDispatcher enum 상수 등록후 사용 * 중복 메서드 등록 방지))
/// </summary>
public enum EventName
{
Test,
Test2,
Test3,
}
private static EventDispatcher instance;
private Dictionary<EventName, Delegate> listeners = new Dictionary<EventName, Delegate>();
public static EventDispatcher Instance
{
get
{
if (instance == null)
{
instance = new EventDispatcher();
}
return instance;
}
}
/// <summary>
/// 반환값이 없는 메서드 혹은 인스턴스 메서드 등록시
/// </summary>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void AddListener(EventName eventName, Action listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Combine(listeners[eventName], listener);
}
else
{
listeners.Add(eventName, listener);
}
}
/// <summary>
/// 인자 1개를 받으며 반환값이 없는 메서드 혹은 인스턴스 메서드 등록시
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void AddListener<T>(EventName eventName, Action<T> listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Combine(listeners[eventName], listener);
}
else
{
listeners.Add(eventName, listener);
}
}
/// <summary>
/// 인자 2개를 받으며 반환값이 없는 메서드 혹은 인스턴스 메서드 등록시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void AddListener<T1, T2>(EventName eventName, Action<T1, T2> listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Combine(listeners[eventName], listener);
}
else
{
listeners.Add(eventName, listener);
}
}
/// <summary>
/// 반환 값이 있는 메서드 등록시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void AddListener<TResult>(EventName eventName, Func<TResult> listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Combine(listeners[eventName], listener);
}
else
{
listeners.Add(eventName, listener);
}
}
/// <summary>
/// 인자 1개를 받으며 반환 값이 있는 메서드 등록시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void AddListener<T1, TResult>(EventName eventName, Func<T1,TResult> listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Combine(listeners[eventName], listener);
}
else
{
listeners.Add(eventName, listener);
}
}
/// <summary>
/// 인자 2개를 받으며 반환 값이 있는 메서드 등록시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void AddListener<T1,T2,TResult>(EventName eventName, Func<T1,T2, TResult> listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Combine(listeners[eventName], listener);
}
else
{
listeners.Add(eventName, listener);
}
}
/// <summary>
/// 반환값이 없는 메서드 삭제시
/// </summary>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void RemoveListener(EventName eventName, Action listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Remove(listeners[eventName], listener);
if (listeners[eventName] == null)
{
listeners.Remove(eventName);
}
}
}
/// <summary>
/// 인자 1개를 받으며 반환값이 없는 메서드 삭제시
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void RemoveListener<T>(EventName eventName, Action<T> listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Remove(listeners[eventName], listener);
if (listeners[eventName] == null)
{
listeners.Remove(eventName);
}
}
}
/// <summary>
/// 인자 2개를 받으며 반환값이 없는 메서드 삭제시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void RemoveListener<T1, T2>(EventName eventName, Action<T1, T2> listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Remove(listeners[eventName], listener);
if (listeners[eventName] == null)
{
listeners.Remove(eventName);
}
}
}
/// <summary>
/// 반환 값이 있는 메서드 제거시
/// </summary>
/// <typeparam name="TResult"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void RemoveListener<TResult>(EventName eventName, Func<TResult> listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Remove(listeners[eventName], listener);
if (listeners[eventName] == null)
{
listeners.Remove(eventName);
}
}
}
/// <summary>
/// 인자 1개를 받으며 반환 값이 있는 메서드 제거시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void RemoveListener<T1,TResult>(EventName eventName, Func<T1,TResult> listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Remove(listeners[eventName], listener);
if (listeners[eventName] == null)
{
listeners.Remove(eventName);
}
}
}
/// <summary>
/// 인자 2개를 받으며 반환 값이 있는 메서드 제거시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="eventName"></param>
/// <param name="listener"></param>
public void RemoveListener<T1, T2,TResult>(EventName eventName, Func<T1, T2,TResult> listener)
{
if (listeners.ContainsKey(eventName))
{
listeners[eventName] = Delegate.Remove(listeners[eventName], listener);
if (listeners[eventName] == null)
{
listeners.Remove(eventName);
}
}
}
/// <summary>
/// 반환값이 없는 메서드 혹은 인스턴스 메서드 사용시
/// </summary>
/// <param name="eventName"></param>
public void Dispatch(EventName eventName)
{
if (listeners.ContainsKey(eventName))
{
((Action)listeners[eventName])();
}
}
/// <summary>
/// 반환값이 없는 메서드 혹은 인스턴스 메서드 사용시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <param name="eventName"></param>
/// <param name="eventParams"></param>
/// <param name="eventParams2"></param>
public void Dispatch<T>(EventName eventName, T eventParams = default)
{
if (listeners.ContainsKey(eventName))
{
((Action<T>)listeners[eventName])(eventParams);
}
}
/// <summary>
/// 반환값이 없는 메서드 혹은 인스턴스 메서드 사용시
/// </summary>
/// <typeparam name="T1"></typeparam>
/// <typeparam name="T2"></typeparam>
/// <param name="eventName"></param>
/// <param name="eventParams"></param>
/// <param name="eventParams2"></param>
public void Dispatch<T1,T2>(EventName eventName, T1 eventParams = default,T2 eventParams2 = default)
{
if (listeners.ContainsKey(eventName))
{
((Action<T1,T2>)listeners[eventName])(eventParams, eventParams2);
}
}
/// <summary>
/// 반환 값이 있는 메서드 혹은 인스턴스 메서드 사용시
/// </summary>
/// <typeparam name="TResult"> 반환 타입</typeparam>
/// <param name="eventName">이벤트 등록 키</param>
/// <param name="result">반환값</param>
/// <exception cref="InvalidCastException"></exception>
public void Dispatch<TResult>(EventName eventName, out TResult result)
{
if (listeners.ContainsKey(eventName))
{
var listener = listeners[eventName];
if (listener is Func<TResult> func)
{
result = func();
}
else
{
throw new InvalidCastException($"Listener for event '{eventName}' is not a Func with the correct signature.");
}
}
else
{
result = default;
}
}
/// <summary>
/// 인자를 1개 받고반환 값이 있는 메서드 혹은 인스턴스 메서드 사용시
/// </summary>
/// <typeparam name="TResult"> 반환 타입</typeparam>
/// <param name="eventName">이벤트 등록 키</param>
/// <param name="eventParams">매개변수</param>
/// <param name="result">반환값</param>
/// <exception cref="InvalidCastException"></exception>
public void Dispatch<T, TResult>(EventName eventName, T eventParams, out TResult result)
{
if (listeners.ContainsKey(eventName))
{
var listener = listeners[eventName];
if (listener is Func<T, TResult> func)
{
result = func(eventParams);
}
else
{
throw new InvalidCastException($"Listener for event '{eventName}' is not a Func with the correct signature.");
}
}
else
{
result = default;
}
}
/// <summary>
/// 인자를 2개 받고반환 값이 있는 메서드 혹은 인스턴스 메서드 사용시
/// </summary>
/// <typeparam name="TResult"> 반환 타입</typeparam>
/// <param name="eventName">이벤트 등록 키</param>
/// <param name="eventParams">첫번째 인자</param>
/// <param name="eventParams2">두번째 인자</param>
/// <param name="result">반환값</param>
/// <exception cref="InvalidCastException"></exception>
public void Dispatch<T1,T2 ,TResult>(EventName eventName, T1 eventParams, T2 eventParams2, out TResult result)
{
if (listeners.ContainsKey(eventName))
{
var listener = listeners[eventName];
if (listener is Func<T1,T2, TResult> func)
{
result = func(eventParams, eventParams2);
}
else
{
throw new InvalidCastException($"Listener for event '{eventName}' is not a Func with the correct signature.");
}
}
else
{
result = default;
}
}
}
좀 길어보이는데 결국 기능은 단순하다 (제너릭 타입을 오버로딩 하였기 때문에 길어보이는것) 결론 적인 기능은 등록, 제거, 사용(발생) 이다.
예를 들어보자 아래와 같이 SayHello 라는 메서드가 있다고 가정하자
private string SayHello(string name)
{
if (this.count == 1)
{
Debug.LogFormat("{0} 야 이미 안녕이라 했어",name);
Debug.LogFormat("{0}의 활성화 상태 {1}", this.name, this.gameObject.activeSelf);
EventDispatcher.Instance.RemoveListener<string,string>(EventName.Test3, this.SayHello);
}
else
{
Debug.LogFormat("{0}야 안녕",name);
this.count++;
Debug.LogFormat("{0}의 활성화 상태 {1}",this.name,this.gameObject.activeSelf );
}
return name;
}이경우 매개변수로 문자열 타입을 받으며 받은 매개변수를 그대로 Return 하는 역할을 하는 메서드 이다 (Debug.Log 는 확인용)
이후 3번 이상 호출시 이벤트를 제거한다.
이경우 아래와 같이 등록이 가능하다.
private void Awake()
{
this.test = () =>
{
this.SayHello("TEST");
};
EventDispatcher.Instance.AddListener(EventName.Test, this.test);
EventDispatcher.Instance.AddListener<string>(EventName.Test2, (Test2) =>
{
Debug.LogFormat("Test2 : {0}",Test2);
});
EventDispatcher.Instance.AddListener<string, string>(EventName.Test3, this.SayHello);
}SayHello 메서드를 를 가진 main 오브젝트의 스크립트가 위와 같이 Awake 시에 세가지 enum 타입 상수를 키로 등록했다고 하자
이 경우 AddListener 는 메서드 오버로딩 상태이고 Action 또는 Func의 경우 인자가 있다면 제너릭 타입을 사용하기에 명시적으로 사용하고자 하는 메서드의 인자타입 (혹은 Func라면 TResutl 반환타입)을 메서드 뒤에 명시해 주어야 한다.
이후 3가지 방법으로 등록된 메서드 들은 아래와 같이 사용 가능하다.
private void Update()
{
if (Input.GetKeyDown(KeyCode.Space)) { EventDispatcher.Instance.Dispatch(EventName.Test); }
if (Input.GetKeyDown(KeyCode.K)) { EventDispatcher.Instance.Dispatch<string>(EventName.Test2,"인자에서 정의하여 사용"); }
if (Input.GetKeyDown(KeyCode.S)) {
var result = default(string);
EventDispatcher.Instance.Dispatch<string, string>(EventName.Test3,this.gameObject.name,out result);
Debug.LogFormat("아웃 매개 변수로 받음 : {0}",result);
}
}위는 UIDirector 모노 오브젝트의 스크립트에서 Disptach를 할 경우의 예시인데
EnumName.Test의 경우 main 스크립트가 필드로 선언하고 Awake 시 정의한 액션을 사용 하는 방법이다 (인자 없음 반환값 없음)
그냥 특정 이벤트 발생시 조건으로 걸어두면 된다.
EnumName.Tes2 의 경우직접 AddListener 의 인자로 등록해주었으며 (인자 1개 , 반환값 없음) 사용 방법은 Disptach매서드 뒤에 제너릭 타입을 명시해주고 두번째 인자로 SayHello 의 인자를 전달해 주면 된다.
EnumName.Test3 의 경우 반환값이 있는 메서드를 등록할시의 예제 이며 이또한 이전과 마찬가지로 제너릭 타입을 명시해주고(TResult 포함) 반환 타입을 Out 매개변수로 받기 위해 미리 변수를 선언해주고 해당 변수에 담아준다.
정리해보면 main(수신자) SayHello(리스너) UIDirector (발신자) 형태가 된다.
버블링을 효과적으로 막을수 있고 등록만 되어있다면 수신자의 게임 오브젝트가 비활성화 상태여도 메서드를 발동 시킬수 있다는 점이 굉장히 매력적이다.
결과창은 아래 확인

'프로젝트 > 건즈앤 레이첼스' 카테고리의 다른 글
| [유니티 프로젝트] 카메라 hit shaking 이펙트 & 스크린 flash 이펙트 코루틴 사용 구현 (0) | 2023.04.19 |
|---|---|
| [유니티 프로젝트] 로그라이크 허브 마을 (기억의 성소) 제작 .3 구조잡기 - 완료 (0) | 2023.04.18 |
| [유니티 프로젝트] 로그라이크 허브 마을 (기억의 성소) 제작 .2 (레이어 정리 및 월드 팝업 UI) (0) | 2023.04.16 |
| [유니티 프로젝트] 로그라이크 허브 마을 (기억의 성소) 제작 .1 (1) | 2023.04.15 |
| [유니티 프로젝트] 인벤토리 스탯창 구현 5 - 완료 (1) | 2023.04.13 |