본문 바로가기

[unity] 모듈 제작 : 나만의 이벤트 매니저 만들기

앤디가이 2022. 6. 24.

Unity에서 인터페이스를 활용한 이벤트 매니저 모듈을 만드는 방법에 대해 알아보자.

 

1. 유니티에서 이벤트 시스템

유니티에서는 기본적으로 다양한 이벤트 시스템을 지원하며, 우리는 유니티에서 제공해주는 다양한 이벤트 시스템을 이미 활용하고 있다. 

만약 특정 오브젝트를 터치 시작 -> 드래그 -> 터치 종료 같은 기능을 사용해서 이동하고 싶을 때, 어떻게 구현을 했을까?

UnityEngine.EventSystems; 를 우선 선언해주고

IBeginDragHandler, IDragHandler, IEndDragHandler 인터페이스를 상속받아 기능을 구현해줬을 것이다.

코드로 보면 다음과 같다.

namespace Modules.EventSystem
{
	public class EventManager : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
	{
		public void OnBeginDrag(PointerEventData eventData)
		{
			//Drag 시작 시 처리.
		}
		...
	}
}

위 예제처럼 이벤트를 받고싶은 인터페이스를 상속받으면, 우리는 쉽게 터치 이벤트를 받아 기능 구현을 할 수 있다.

 

위에서 제작된 사례처럼 우리가 커스텀한 이벤트 시스템과 이벤트 받는 방법에 대해 알아보자.

 

2. EventManager 클래스 제작

나만의 이벤트를 관리하고 이벤트 등록을 요청한 오브젝트에게 이벤트를 알려주는 매니저 클래스를 먼저 제작해보자.

EventManager 클래스는 간단하게 다음과 같은 기능을 정의하면 좋을 것 같다.

1. 이벤트리스너(이벤트를 받을 오브젝트)의 추가 함수

2. 이벤트리스너의 삭제 함수

3. 이벤트 브로드케스트(전달) 함수

4. 싱글톤으로 구현

 

싱글톤 클래스에 대한 설명은 아래 글을 참고하면 좋다.

2022.06.23 - [분류 전체보기] - [Unity] 디자인 패턴 : 싱글톤 패턴(Singleton Pattern)

 

[Unity] 디자인 패턴 : 싱글톤 패턴(Singleton Pattern)

Unity와 C#을 통한 싱글톤 패턴(Singleton Pattern)의 정의와 사용 방법에 대해 알아보자. 1. 싱글톤 패턴(Singleton Pattern)이란? 싱글톤 패턴(Singleton Pattern)은 클래스 인스턴스를 하나만 만들고, 그 인스..

wonjuri.tistory.com

 

프로젝트 내 Modules/Scripts 폴더를 만든 후 EventManager.cs 클래스를 제작해보자.

EventManager 코드로 구현해 보면 다음과 같다.

using UnityEngine;
using System.Collections.Generic;
using UnityEngine.SceneManagement;

namespace Modules.EventSystem
{
	public class EventManager : MonoBehaviour
	{
		//싱글톤으로 구현
		public static EventManager Instance { get; private set; }

		//이벤트 리스너 리스트 관리
		private Dictionary<EVENT_TYPE, List<IEventListener>> Listeners = new Dictionary<EVENT_TYPE, List<IEventListener>>();

		void Awake()
		{
			Instance = this;
		}
		private void OnEnable()
		{
			SceneManager.sceneLoaded += SceneManager_sceneLoaded;
		}
        private void OnDisable()
        {
			SceneManager.sceneLoaded -= SceneManager_sceneLoaded;
		}
        private void OnDestroy()
        {
			Instance = null;
		}

		/// <summary>
        /// 씬이 로드될 때 호출되는 이벤트 함수.
        /// </summary>
        /// <param name="arg0"></param>
        /// <param name="arg1"></param>
        private void SceneManager_sceneLoaded(Scene arg0, LoadSceneMode arg1)
        {
			//씬이 바뀜에 따라 이벤트 의존성을 제거해준다.
			RefreshListeners();
		}

        /// <summary>
        /// 이벤트 기다림 등록
        /// </summary>
        /// <param name="Event_Type"></param>
        /// <param name="Listener"></param>
        public void AddListener(EVENT_TYPE Event_Type, IEventListener Listener)
		{
			List<IEventListener> ListenList = null;

			if (Listeners.TryGetValue(Event_Type, out ListenList))
			{
				//해당 이벤트 키값이 존재한다면, 이벤트를 추가해준다.
				ListenList.Add(Listener);
				return;
			}

			ListenList = new List<IEventListener>();
			ListenList.Add(Listener);
			Listeners.Add(Event_Type, ListenList);
		}

		/// <summary>
        /// 이벤트 알림 브로드케스트
        /// </summary>
        /// <param name="Event_Type">전달할 이벤트 타입</param>
        /// <param name="Sender">전달자</param>
        /// <param name="Param">추가 파라메터</param>
		public void PostNotification(EVENT_TYPE Event_Type, Component Sender, object Param = null)
		{

			List<IEventListener> ListenList = null;

			//이벤트 리스너(대기자)가 없으면 그냥 리턴.
			if (!Listeners.TryGetValue(Event_Type, out ListenList))
				return;

			//모든 이벤트 리스너(대기자)에게 이벤트 전송.
			for (int i = 0; i < ListenList.Count; i++)
			{
				if (!ListenList[i].Equals(null)) //If object is not null, then send message via interfaces
					ListenList[i].OnEvent(Event_Type, Sender, Param);
			}
		}

		/// <summary>
        /// 이벤트 제거
        /// </summary>
        /// <param name="Event_Type">이벤트 타입</param>
		public void RemoveEvent(EVENT_TYPE Event_Type)
		{
			Listeners.Remove(Event_Type);
		}

		/// <summary>
        /// 이벤트 리스너를 Refresh 해준다.
        /// </summary>
		private void RefreshListeners()
		{
			//임시 Dictionary 생성
			Dictionary<EVENT_TYPE, List<IEventListener>> TmpListeners = new Dictionary<EVENT_TYPE, List<IEventListener>>();

			//씬이 바뀜에 따라 리스너가 Null이 된 부분을 삭제해준다. 
			foreach (KeyValuePair<EVENT_TYPE, List<IEventListener>> Item in Listeners)
			{
				for (int i = Item.Value.Count - 1; i >= 0; i--)
				{
					if (Item.Value[i].Equals(null))
						Item.Value.RemoveAt(i);
				}

				if (Item.Value.Count > 0)
					TmpListeners.Add(Item.Key, Item.Value);
			}
			//살아있는 리스너는 다시 넣어준다.
			Listeners = TmpListeners;
		}
    }
}

 

3. IEventListener 인터페이스 구현

이벤트를 받기를 원하는 객체에 필요한 IEventListener 인터페이스를 구현해 보자.

해당 기능은 디자인패턴에서 배웠던 옵저버 패턴에 대한 글을 참고하고 보면 이해하기 좀 더 쉽다.

2022.06.17 - [unity3d/DesignPattern] - [Unity] 디자인 패턴 : 옵저버 패턴(Observer Pattern)

 

[Unity] 디자인 패턴 : 옵저버 패턴(Observer Pattern)

Unity와 C#을 통한 옵저버 패턴(Observer Pattern)의 정의와 사용 방법에 대해 알아보자. 1. 옵저버 패턴(Observer Pattern)이란? 옵저버 패턴(Observer Pattern)은 한 객체(주제)의 상태가 바뀌면 그 객체에 의존..

wonjuri.tistory.com

 

프로젝트 내 Modules/Scripts 폴더에 IEventListener.cs 클래스를 제작해보자.

IEventListener 코드로 구현해 보면 다음과 같다.

using UnityEngine;

namespace Modules.EventSystem
{
	public interface IEventListener
	{
		//이벤트 발생시 전송되는 이벤트 정보.
		void OnEvent(EVENT_TYPE Event_Type, Component Sender, object Param = null);
	}
}

 

4. EventType Enum 추가

사용자가 원하는 이벤트 종류를 Enum으로 만들어서 관리하면 좋다.

나는 테스트를 위해 Room안에 들어갔을 때 발생하는 이벤트와 Room을 나갔을때 발생하는 이벤트를 선언하였다.(아래 코드 참고)

namespace Modules.EventSystem
{
    //사용자 정의 이벤트 추가.
    public enum EVENT_TYPE
    {
        EVENT_ROOM_IN,
        EVENT_ROOM_OUT
    };
}

 

5. 테스트 코드 구축

코드 구축은 되었으니, 테스트 환경을 만들어 보자.

구축한 코드 3개는 다음과 같다.

Unity Event Moules 스크립트 구성
Unity Event Moules 스크립트 구성

Scene에 GameObject를 만든 후 이름을 EventManager로 변경 후 EventManager 스크립트를 붙여준다.

Unity EventManager
Unity EventManager 컴포넌트

 

이제 테스트 이벤트를 보내는 오브젝트와 이벤트를 받는 오브젝트를 추가해 보자.

이벤트를 보내는 오브젝트는 EventSender라고 GameObject를 만든 후 아래와 같이 스크립트를 만들어 Attach 해보자.

EventSender는 숫자키 1,2번을 눌렀을 때 이벤트를 발생시키는 기능만을 하는 테스트 코드이다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Modules.EventSystem
{
    public class EventSender : MonoBehaviour
    {
        void Update()
        {
            if(Input.GetKeyUp(KeyCode.Alpha1))
            {
                EventManager.Instance.PostNotification(EVENT_TYPE.EVENT_ROOM_IN, this, null);
            }
            if (Input.GetKeyUp(KeyCode.Alpha2))
            {
                EventManager.Instance.PostNotification(EVENT_TYPE.EVENT_ROOM_OUT, this, 10);
            }
        }
    }
}

이제 이벤트를 받아야 되는 테스트 오브젝트인 EventReceiver도 만들어 보자. 

Scene에 EventReceiver 로 GameObject를 생성 후 아래와 같이 스크립트를 만들어 Attach 해보자.

Start()에서 받고 싶은 이벤트를 등록한다.

OnEvent()는 이벤트를 받았을 경우 호출되는 함수이다. 받은 이벤트에 따라서 처리를 해주면 된다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Modules.EventSystem
{
    public class EventReceiver : MonoBehaviour, IEventListener
    {
        private void Start()
        {
            //받고 싶은 이벤트 등록
            EventManager.Instance.AddListener(EVENT_TYPE.EVENT_ROOM_IN, this);
            EventManager.Instance.AddListener(EVENT_TYPE.EVENT_ROOM_OUT, this);
        }

        //이벤트 받았을 때 처리 함수
        public void OnEvent(EVENT_TYPE event_Type, Component sender, object param = null)
        {
            string result = string.Format("받은 이벤트 종류 :  {0}, 이벤트 전달한 오브젝트 : {1}", event_Type, sender.gameObject.name.ToString());
            Debug.Log(result);

            switch(event_Type)
            {
                case EVENT_TYPE.EVENT_ROOM_IN:
                    Debug.Log("룸 안으로 들어왔습니다.");
                    break;
                case EVENT_TYPE.EVENT_ROOM_OUT:
                    Debug.Log("룸을 나갔습니다.");
                    Debug.Log(param.ToString());
                    break;
            }
        }

    }
}

 

테스트 Scene 구성은 다음과 같다.

EventSystem 테스트 Scene 구성
EventSystem 테스트 Scene 구성

플레이 후 숫자키 1,2번을 눌러서 이벤트가 전달되는 과정을 확인해 보자.

 

숫자키 1번 눌렀을 경우 결과 메세지

숫자키 1번 테스트 결과
숫자키 1번 메세지 테스트 결과

숫자키 2번 눌렀을 경우 결과 메시지

숫자키 2번 눌렀을 경우 결과 메세지
숫자키 2번 눌렀을 경우 결과 메세지

 

6. 정리

나만의 이벤트 시스템을 만드는 방법에 대해서 알아보았다.

위 예제의 숫자키 2번 눌렀을 경우 받는 object pram 형태로 다양한 데이터 수치 값도 받을 수 있는 걸 확인할 수 있다.

위와 같은 이벤트 시스템을 사용할 경우, 오브젝트 간 결합도가 낮아지기 때문에 유연한 코드 구성을 할 수 있다. 물론 이벤트를 너무 많이 사용한다면 코드 디버깅에 있어서 어려움이 있을 수 있다는 점은 참고하면 좋을 것 같다.

댓글