Unity

Unity Tip : EventBus(이벤트버스)

유니얼 2023. 11. 12. 22:06
728x90

게임 개발에서 이벤트 버스 패턴은 중요한 개념 중 하나입니다. 이 패턴은 게임 오브젝트 간의 통신과 상호작용을 더 간단하게 만들고, 코드의 모듈화 및 유지 보수를 향상할 수 있습니다. Unity에서 이벤트 버스 패턴을 사용하여 게임 개발을 어떻게 더 효율적으로 할 수 있는지 알아보겠습니다.

 

참고 자료 링크 :

https://www.yes24.com/Product/Goods/114854688

 

유니티로 배우는 게임 디자인 패턴 - 예스24

우아하게 게임을 개발하고 싶다면 디자인 패턴을 배우자 이 책은 유니티의 고급 프로그래밍 기술과 디자인 패턴으로 작업을 시작할 준비가 된 모든 게임 개발자를 위해 쓰였다. 소프트웨어 디

www.yes24.com

EventBus

이벤트버스란?

이벤트 버스는 소프트웨어 아키텍처 패턴 중 하나로, 다른 컴포넌트나 모듈 간의 통신을 관리하기 위해 사용됩니다. 이 패턴은 이벤트를 발행(emit)하고, 해당 이벤트를 구독(subscribe)하는 리스너에게 전달함으로써 컴포넌트 간의 결합도를 낮추고, 코드의 모듈화를 촉진합니다.

이벤트 버스 패턴의 장점

  • 느슨한 결합: 컴포넌트 간의 강한 의존성을 피하고 느슨한 결합을 유지할 수 있습니다.
  • 모듈화: 코드를 모듈화 하여 개발 및 유지 보수를 더 쉽게 할 수 있습니다.
  • 확장성: 새로운 이벤트 및 리스너를 추가하기 쉽습니다.

주의사항과 고려 사항

  • 과용하지 않기: 모든 것을 이벤트로 처리하는 것은 오히려 복잡성을 높일 수 있습니다.
  • 성능 고려: 이벤트 처리가 빈번한 경우 성능 문제를 고려해야 합니다.

Unity에서 이벤트 버스 구현하기

Step 1 : 이벤트 클래스 구현하기 

먼저, 게임에서 발생할 수 있는 각각의 이벤트를 나타내는 이벤트 클래스를 정의합니다. 이 클래스는 이벤트의 데이터 및 관련 정보를 포함합니다. 이 예제에서는 GameEventType 열거형과 GameStatesEvent 구조체를 사용한 것처럼 각 이벤트 유형과 관련된 정보를 포함하는 클래스를 정의합니다.

 

// 게임 이벤트 종류를 정의하는 열거형
public enum GameEventType
{
    COUNTDOWN,  // 카운트 다운 이벤트
    START,      // 시작 이벤트
    RESTART,    // 재시작 이벤트
    PAUSE,      // 일시 정지 이벤트
    STOP,       // 중지 이벤트
    FINISH,     // 완료 이벤트
    QUIT,       // 종료 이벤트
}

// 게임 상태 이벤트를 나타내는 구조체
public struct GameStatesEvent
{
    public GameEventType gameEventType;  // 게임 이벤트 타입을 저장하는 변수

    // GameStatesEvent 구조체의 생성자
    public GameStatesEvent(GameEventType gameEventType)
    {
        this.gameEventType = gameEventType;  // 생성자를 통해 게임 이벤트 타입을 초기화합니다.
    }

    // GameStatesEvent 구조체의 인스턴스를 트리거(발생)하는 정적 메서드
    static GameStatesEvent e;

    public static void Trigger(GameEventType gameEventType)
    {
        e.gameEventType = gameEventType;  // gameEventType 변수에 전달된 이벤트 타입을 저장합니다.
        GameEventManager.TriggerEvent<GameStatesEvent>(e);  // GameEventManager 클래스를 사용하여 이벤트를 트리거합니다.
    }
}

 

Step 2 : 이벤트 리스너 인터페이스 구현하기

이제 이벤트를 수신하고 처리할 수 있는 리스너 인터페이스를 정의합니다. 예를 들어, 다음과 같이 EventListener 인터페이스를 만들 수 있습니다.

// 이벤트 리스너 베이스 인터페이스
public interface EventListenerBase { }

// 이벤트 리스너 인터페이스
public interface EventListener<T> : EventListenerBase
{
    void OnEvent(T eventType);
}

Step 3 : 이벤트 버스 관리 클래스(GameEventManger) 구현하기 

이벤트를 관리하고 통신을 중계할 이벤트 버스 관리 클래스를 만듭니다. 이 클래스는 이벤트를 발행하고 구독하는 메서드를 포함합니다. 또한, 이벤트를 수신한 리스너에게 전달하는 로직을 구현합니다.

 

// 게임 이벤트 관리자 클래스
public class GameEventManager
{
    // 이벤트 타입별로 구독자(리스너) 목록을 관리하는 사전
    private static readonly Dictionary<Type, List<EventListenerBase>> _subscribersList
        = new Dictionary<Type, List<EventListenerBase>>();

    // 이벤트를 구독하는 메서드
    public static void Subscribe<GameEvents>(EventListener<GameEvents> listener) where GameEvents : struct
    {
        Type eventType = typeof(GameEvents);

        if (!_subscribersList.ContainsKey(eventType))
        {
            _subscribersList[eventType] = new List<EventListenerBase>();
        }

        if (!SubscriptionExists(eventType, listener))
        {
            _subscribersList[eventType].Add(listener);
        }
    }

    // 이벤트 구독 취소 메서드
    public static void Unsubscribe<GameEvents>(EventListener<GameEvents> listener) where GameEvents : struct
    {
        Type eventType = typeof(GameEvents);

        if (!_subscribersList.ContainsKey(eventType))
            return;

        List<EventListenerBase> subscriberList = _subscribersList[eventType];

        for (int i = subscriberList.Count - 1; i >= 0; i--)
        {
            if (subscriberList[i] == listener)
            {
                subscriberList.Remove(subscriberList[i]);

                if (subscriberList.Count == 0)
                {
                    _subscribersList.Remove(eventType);
                }
                return;
            }
        }
    }

    // 이미 구독 중인지 확인하는 메서드
    private static bool SubscriptionExists(Type type, EventListenerBase receiver)
    {
        List<EventListenerBase> receivers;
        if (!_subscribersList.TryGetValue(type, out receivers)) return false;

        bool exists = false;
        for (int i = receivers.Count - 1; i >= 0; i--)
        {
            if (receivers[i] == receiver)
            {
                exists = true;
                break;
            }
        }
        return exists;
    }

    // 이벤트를 발생시키는 메서드
    public static void TriggerEvent<GameEvents>(GameEvents events) where GameEvents : struct
    {
        Type eventType = typeof(GameEvents);
        List<EventListenerBase> list;
        if (!_subscribersList.TryGetValue(eventType, out list))
        {
            return;
        }

        for (int i = list.Count - 1; i >= 0; i--)
        {
            (list[i] as EventListener<GameEvents>).OnEvent(events);
        }
    }
}

// 이벤트 리스너를 등록 및 해제하는 확장 메서드
public static class GameEventsRegister
{
    public delegate void Delegate<T>(T eventType);

    // 이벤트 리스너를 등록하는 확장 메서드
    public static void EventStartingListening<EventType>(this EventListener<EventType> caller) where EventType : struct
    {
        GameEventManager.Subscribe<EventType>(caller);
    }

    // 이벤트 리스너를 해제하는 확장 메서드
    public static void EventStopListening<EventType>(this EventListener<EventType> caller) where EventType : struct
    {
        GameEventManager.Unsubscribe<EventType>(caller);
    }
}

 

Step 4 : 이벤트 발행 및 구독

이제 게임 오브젝트에서 필요한 이벤트를 구독하고, 이벤트를 발생시킬 때 해당 이벤트를 트리거합니다.

PlayerController.cs

이 코드는 Unity 게임에서 상태 변화를 처리하기 위한 스크립트입니다. 아래는 코드의 주요 기능과 설명입니다:

using UnityEngine;

public class PlayerController : MonoBehaviour, EventListener<GameStatesEvent>
{
    private string _status;

    private void OnEnable()
    {
        // GameStatesEvent 이벤트를 구독합니다.
        this.EventStartingListening<GameStatesEvent>();
    }

    private void OnDisable()
    {
        // 스크립트 비활성화 시 이벤트 구독을 해제합니다.
        this.EventStopListening<GameStatesEvent>();
    }

    private void GameStart()
    {
        // 게임 시작 시 상태를 업데이트합니다.
        _status = "게임 시작됨";
    }

    private void GameStopped()
    {
        // 게임 정지 시 상태를 업데이트합니다.
        _status = "게임 정지됨";
    }

    private void OnGUI()
    {
        // GUI에 게임 상태를 표시합니다.
        GUI.color = Color.green;
        GUI.Label(new Rect(10, 60, 200, 20), "게임 상태: " + _status);
    }

    public void OnEvent(GameStatesEvent eventType)
    {
        // GameStatesEvent 이벤트가 발생하면 처리하는 메서드입니다.
        switch (eventType.gameEventType)
        {
            case GameEventType.START:
                GameStart();
                break;
            case GameEventType.STOP:
                GameStopped();
                break;
        }
    }
}
  • OnEnable 메서드: 스크립트가 활성화될 때 호출되며, GameStatesEvent 이벤트를 구독합니다. 이렇게 하면 이벤트가 발생할 때 해당 이벤트 핸들러가 호출됩니다.
  • OnDisable 메서드: 스크립트가 비활성화될 때 호출되며, 이벤트 구독을 해제합니다. 스크립트가 비활성화되면 더 이상 이벤트를 수신하지 않습니다.
  • GameStart 메서드: 게임 시작 상태를 처리하는 메서드로, 상태를 업데이트하여 _status 변수에 "게임 시작됨"을 할당합니다.
  • GameStopped 메서드: 게임 정지 상태를 처리하는 메서드로, 상태를 업데이트하여 _status 변수에 "게임 정지됨"을 할당합니다.
  • OnGUI 메서드: GUI에 게임 상태를 표시하는 메서드로, _status 변수에 저장된 상태를 녹색으로 표시합니다.
  • OnEvent 메서드: GameStatesEvent 이벤트를 처리하는 메서드입니다. 이벤트 타입에 따라 GameStart 또는 GameStopped 메서드를 호출하여 게임 상태를 업데이트합니다.

HUDController.cs

이 코드는 Unity 게임에서 HUD (Head-Up Display)를 관리하는 스크립트를 나타냅니다. 코드의 주요 기능과 설명은 다음과 같습니다:

using UnityEngine;

public class HUDController : MonoBehaviour, EventListener<GameStatesEvent>
{
    private bool _isDisplayOn; // HUD 표시 여부를 나타내는 변수

    private void OnEnable()
    {
        // 이 스크립트가 활성화될 때 GameStatesEvent 이벤트를 구독합니다.
        this.EventStartingListening<GameStatesEvent>();
    }

    private void OnDisable()
    {
        // 이 스크립트가 비활성화될 때 이벤트 구독을 해제합니다.
        this.EventStopListening<GameStatesEvent>();
    }

    private void DisplayHUD()
    {
        // HUD를 표시하는 메서드로, _isDisplayOn을 true로 설정합니다.
        _isDisplayOn = true;
    }

    private void OnGUI()
    {
        if (_isDisplayOn)
        {
            // HUD가 표시 중일 때
            if (GUILayout.Button("게임 정지"))
            {
                // "게임 정지" 버튼을 누르면 HUD를 숨기고 게임 정지 이벤트를 트리거합니다.
                _isDisplayOn = false;
                GameEventManager.TriggerEvent(new GameStatesEvent
                {
                    gameEventType = GameEventType.STOP
                });
            }
        }
    }

    public void OnEvent(GameStatesEvent eventType)
    {
        // GameStatesEvent 이벤트를 처리하는 메서드입니다.
        switch (eventType.gameEventType)
        {
            case GameEventType.START:
                // 게임 시작 이벤트를 받으면 HUD를 표시합니다.
                DisplayHUD();
                break;
        }
    }
}

 

  • OnEnable 메서드: 스크립트가 활성화될 때 호출되며, GameStatesEvent 이벤트를 구독합니다. 이렇게 하면 이벤트가 발생할 때 해당 이벤트 핸들러가 호출됩니다.
  • OnDisable 메서드: 스크립트가 비활성화될 때 호출되며, 이벤트 구독을 해제합니다. 스크립트가 비활성화되면 더 이상 이벤트를 수신하지 않습니다.
  • DisplayHUD 메서드: HUD를 표시하는 메서드로, _isDisplayOn 변수를 true로 설정하여 HUD가 표시됨을 나타냅니다.
  • OnGUI 메서드: GUI를 그리는 메서드로, HUD가 표시 중일 때 "게임 정지" 버튼을 표시하고 클릭하면 HUD를 숨기고 게임 정지 이벤트를 트리거합니다.
  • OnEvent 메서드: GameStatesEvent 이벤트를 처리하는 메서드입니다. 이벤트 타입에 따라 게임이 시작되면 HUD를 표시합니다.

이 코드를 사용하면 게임 시작 시 HUD를 표시하고 "게임 정지" 버튼을 클릭하여 HUD를 숨길 수 있으며, 게임 정지 이벤트를 트리거하여 게임 상태를 제어할 수 있습니다.

CountDownTimer.cs

이 코드는 Unity 게임에서 카운트다운을 관리하는 스크립트를 나타냅니다. 코드의 주요 기능과 설명은 다음과 같습니다:

using UnityEngine;

public class CountDownTimer : MonoBehaviour, EventListener<GameStatesEvent>
{
    private float _currentTime; // 현재 카운트다운 시간을 나타내는 변수
    private float _duration = 3.0f; // 카운트다운 기간을 설정하는 변수

    private void OnEnable()
    {
        // 이 스크립트가 활성화될 때 GameStatesEvent 이벤트를 구독합니다.
        this.EventStartingListening<GameStatesEvent>();
    }

    private void OnDisable()
    {
        // 이 스크립트가 비활성화될 때 이벤트 구독을 해제합니다.
        this.EventStopListening<GameStatesEvent>();
    }

    private void StartTimer()
    {
        // 카운트다운을 시작하는 메서드입니다.
        StartCoroutine(CountDown());
    }

    private IEnumerator CountDown()
    {
        _currentTime = _duration;

        while (_currentTime > 0)
        {
            yield return new WaitForSeconds(1.0f);
            _currentTime--;
        }

        // 카운트다운이 끝나면 게임 시작 이벤트를 트리거합니다.
        GameEventManager.TriggerEvent(new GameStatesEvent
        {
            gameEventType = GameEventType.START
        });
    }

    public void OnEvent(GameStatesEvent eventType)
    {
        // GameStatesEvent 이벤트를 처리하는 메서드입니다.
        switch (eventType.gameEventType)
        {
            case GameEventType.COUNTDOWN:
                // COUNTDOWN 이벤트를 받으면 카운트다운을 시작합니다.
                StartTimer();
                break;
        }
    }

    private void OnGUI()
    {
        // GUI에 현재 카운트다운 시간을 표시합니다.
        GUI.color = Color.blue;
        GUI.Label(new Rect(125, 0, 100, 20), "남은 시간: " + _currentTime);
    }
}
  • OnEnable 메서드: 스크립트가 활성화될 때 호출되며, GameStatesEvent 이벤트를 구독합니다. 이렇게 하면 이벤트가 발생할 때 해당 이벤트 핸들러가 호출됩니다.
  • OnDisable 메서드: 스크립트가 비활성화될 때 호출되며, 이벤트 구독을 해제합니다. 스크립트가 비활성화되면 더 이상 이벤트를 수신하지 않습니다.
  • StartTimer 메서드: 카운트다운을 시작하는 메서드로, CountDown 코루틴을 시작합니다.
  • CountDown 코루틴: 지정된 기간 동안 카운트다운을 진행하고, 시간이 끝나면 게임 시작 이벤트를 트리거합니다.
  • OnEvent 메서드: GameStatesEvent 이벤트를 처리하는 메서드입니다. 이벤트 타입에 따라 COUNTDOWN 이벤트를 받으면 카운트다운을 시작합니다.
  • OnGUI 메서드: GUI를 그리는 메서드로, 현재 카운트다운 시간을 표시합니다.

이 코드를 사용하면 COUNTDOWN 이벤트를 트리거하여 카운트다운을 시작하고, 화면에 남은 시간을 표시할 수 있습니다. 카운트다운이 끝나면 게임 시작 이벤트를 트리거하여 게임을 시작할 수 있습니다.

ClientEventBus.cs

이 코드는 Unity 게임에서 클라이언트 이벤트 버스를 관리하는 스크립트를 나타냅니다. 코드의 주요 기능과 설명은 다음과 같습니다:

using UnityEngine;

public class ClientEventBus : MonoBehaviour, EventListener<GameStatesEvent>
{
    private bool _isButtonEnabled; // 버튼 활성화 여부를 나타내는 변수

    private void Start()
    {
        // HUDController, CountDownTimer, PlayerController 스크립트를 게임 오브젝트에 추가합니다.
        gameObject.AddComponent<HUDController>();
        gameObject.AddComponent<CountDownTimer>();
        gameObject.AddComponent<PlayerController>();

        _isButtonEnabled = true; // 버튼을 활성화 상태로 초기화합니다.
    }

    private void OnEnable()
    {
        // 이 스크립트가 활성화될 때 GameStatesEvent 이벤트를 구독합니다.
        this.EventStartingListening<GameStatesEvent>();
    }

    private void OnDisable()
    {
        // 이 스크립트가 비활성화될 때 이벤트 구독을 해제합니다.
        this.EventStopListening<GameStatesEvent>();
    }

    private void ReStart()
    {
        // 재시작 메서드로, 버튼을 다시 활성화 상태로 설정합니다.
        _isButtonEnabled = true;
    }

    private void OnGUI()
    {
        if (_isButtonEnabled)
        {
            // 버튼이 활성화된 상태일 때
            if (GUILayout.Button("카운트다운 시작"))
            {
                _isButtonEnabled = false; // 버튼을 비활성화 상태로 변경합니다.
                GameEventManager.TriggerEvent(new GameStatesEvent
                {
                    gameEventType = GameEventType.COUNTDOWN
                }); // COUNTDOWN 이벤트를 트리거합니다.
            }
        }
    }

    public void OnEvent(GameStatesEvent eventType)
    {
        // GameStatesEvent 이벤트를 처리하는 메서드입니다.
        switch (eventType.gameEventType)
        {
            case GameEventType.STOP:
                // STOP 이벤트를 받으면 ReStart 메서드를 호출하여 버튼을 다시 활성화 상태로 변경합니다.
                ReStart();
                break;
        }
    }
}
  • Start 메서드: 게임 시작 시 실행되며, HUDController, CountDownTimer 및 PlayerController 스크립트를 게임 오브젝트에 추가합니다. 또한 _isButtonEnabled 변수를 true로 초기화하여 버튼을 활성화 상태로 설정합니다.
  • OnEnable 메서드: 스크립트가 활성화될 때 호출되며, GameStatesEvent 이벤트를 구독합니다.
  • OnDisable 메서드: 스크립트가 비활성화될 때 호출되며, 이벤트 구독을 해제합니다.
  • ReStart 메서드: 게임을 재시작하는 메서드로, 버튼을 다시 활성화 상태로 설정합니다.
  • OnGUI 메서드: GUI를 그리는 메서드로, _isButtonEnabled 변수가 true인 경우 "카운트다운 시작" 버튼을 표시하고 클릭하면 COUNTDOWN 이벤트를 트리거하여 게임 카운트다운을 시작합니다.
  • OnEvent 메서드: GameStatesEvent 이벤트를 처리하는 메서드입니다. STOP 이벤트를 받으면 ReStart 메서드를 호출하여 버튼을 다시 활성화 상태로 변경합니다.

이 코드를 사용하면 게임 시작 시 버튼을 클릭하여 게임 카운트다운을 시작하고, 게임이 정지되면 버튼을 다시 활성화할 수 있습니다. STOP 이벤트를 통해 버튼 상태를 관리하여 게임을 제어할 수 있습니다.

Step 5 : 씬 구성 및 결과 확인하기

이제 빈 씬을 만들고 새로운 GameObject를 생성하고 ClientEventBus 컴포넌트를 추가합니다.

EventBus_Result

실행결과 : 

마무리

이벤트 버스 패턴은 Unity 게임 개발에서 강력한 도구 중 하나로, 게임의 모듈화와 유지 보수성을 향상할 수 있습니다. 적절하게 사용하면 코드의 가독성을 향상하고 다양한 컴포넌트 간의 효율적인 통신을 도울 수 있습니다. 이 패턴을 적용하여 더욱 효율적인 Unity 게임을 개발할 수 있습니다.

반응형