본문 바로가기

카테고리 없음

디자인 패턴) 이벤트 버스 패턴 Event Bus Pattern _ Unity C#

이벤트 버스

객체가 구독하거나 게시할 수 있는, 특정한 전역 이벤트의 목록을 관리하는 중앙 허브 역할을 합니다.

 

아래의 예시에서는, 경주의 전체 상태 변경을 수신해야 하는 구성 요소에 특정 경주 이벤트를 브로드캐스트 합니다.


이해하기

게시자 객체가 이벤트를 발생하게 하면, 구독자 객체들이 받 수 있는 신호를 보냅니다.

(신호는 작업이 생겼다는 알림 형식)

사용시기

  • 빠른 프로토타이핑 : 새로운 게임 매커니즘이나 기능을 빠르게 프로토타이핑할 때 사용합니다.
  • 프로덕션 코드 : 복잡한 이벤트 타입이나 구조체를 다루지 않아도 되는 경우에 사용합니다.

버스 bus : (컴퓨터용어) 구성 요소 간 연결을 의미합니다.

 

 

Publisher

이벤트를 발행하는 역할을 합니다. 이벤트 버스에서 선언한 특정 종류의 이벤트를 구독자에게 게시할 수 있습니다.

 

Event Bus

이벤트의 중앙 허브 역할을 하며, 이벤트를 받아서 등록된 구독자들에게 전달합니다. 구독자와 게시자 사이의 이벤트 전송을 조정하는 역할을 합니다.

 

Subscriber

이벤트를 수신하고 처리하는 역할을 합니다. 이벤트 버스를 통해, 특정 이벤트의 구독자로 자신을 등록합니다.


 

장점

  • 서로 직접 참조하지 않아도 되므로 구성 요소 간의 결합도를 낮출 수 있습니다.(분리, decoupling) 오브젝트들이 서로 직접 참조하는 대신, 이벤트 버스를 통해 통신할 수 있습니다.
  • 여러 모듈이나 컴포넌트가 이벤트 버스를 통해 쉽게 통신할 수 있습니다.

단점

  • 모든 이벤트가 중앙 버스를 통해 전달되므로, 버스가 병목이 될 수 있습니다.
  • 시스템의 규모가 커지면 이벤트 흐름을 추적하기 어려울 수 있습니다.
  • 전역적으로 구현하면, 디버깅과 유닛 테스트에 어려움이 발생합니다.

전역 범위의 이벤트를 관리할 때는 이벤트 버스 패턴을 사용하고,

오토바이와 도로 벽 간의 충돌과 같은 지역화된 상호작용은 옵저버 패턴을 사용하면 좋습니다.


 

구현하기

public enum RaceEventType
{
    COUNTDOWN,
    START,
    RESTART,
    PAUSE,
    STOP,
    FINISH,
    QUIT
}

public class RaceEventBus : MonoBehaviour
{
    private static readonly IDictionary<RaceEventType, UnityEvent> Events =
        new Dictionary<RaceEventType, UnityEvent>();

    public static void Subscribe(RaceEventType eventType, UnityAction listener)
    {
        UnityEvent thisEvent;

        if(Events.TryGetValue(eventType, out thisEvent))
        {
            thisEvent.AddListener(listener);
        }
        else
        {
            thisEvent = new UnityEvent();
            thisEvent.AddListener(listener);
            Events.Add(eventType, thisEvent);
        }
    }

    public static void Unsubscribe(RaceEventType eventType, UnityAction listener)
    {
        UnityEvent thisEvent;
        if(Events.TryGetValue(eventType, out thisEvent))
        {
            thisEvent.RemoveListener(listener);
        }
    }

    public static void Publish(RaceEventType eventType)
    {
        UnityEvent unityEvent;
        if(Events.TryGetValue(eventType, out unityEvent))
        {
            unityEvent.Invoke();
        }
    }
}

사용해보기

ClientEventBus

public class ClientEventBus : MonoBehaviour
{
    private bool _isButtonEnabled;

    private void Start()
    {
        gameObject.AddComponent<HUDController>();
        gameObject.AddComponent<CountdownTimer>();
        gameObject.AddComponent<BikeController>();

        _isButtonEnabled = true;
    }

    private void OnEnable()
    {
        RaceEventBus.Subscribe(RaceEventType.STOP, Restart);
    }
    private void OnDisable()
    {
        RaceEventBus.Unsubscribe(RaceEventType.STOP, Restart);
    }

    private void Restart()
    {
        _isButtonEnabled = true;
    }

    private void OnGUI()
    {
        if (_isButtonEnabled)
        {
            if(GUILayout.Button("Start COUNTDOWN"))
            {
                _isButtonEnabled = false;
                RaceEventBus.Publish(RaceEventType.COUNTDOWN);
            }
        }
    }

}

 

CountdownTimer

public class CountdownTimer : MonoBehaviour
{
    private float _currentTime;
    private float duration = 3.0f;

    private void OnEnable()
    {
        // 오브젝트가 활성화될 때마다, Subscribe()가 호출된다.
        // 즉, 오브젝트가 활성화되었을 때, 구독중이기에 이벤트를 수신한다.
        RaceEventBus.Subscribe(RaceEventType.COUNTDOWN, StartTimer);
    }

    private void OnDisable()
    {
        // 오브젝트가 활성화되면, 구독취소하여, 이벤트를 수신받지 않는다.
        RaceEventBus.Unsubscribe(RaceEventType.COUNTDOWN, StartTimer);
    }

    private void StartTimer()
    {
        StartCoroutine(Countdown()); // 코루틴은 Disable되면, 없어진다.
    }

    IEnumerator Countdown()
    {
        _currentTime = duration;
        while (_currentTime > 0)
        {
            yield return new WaitForSeconds(1f);
            _currentTime--;
        }

        RaceEventBus.Publish(RaceEventType.START);
    }

    private void OnGUI()
    {
        GUI.color = Color.blue;
        GUI.Label(new Rect(125, 0, 100, 20), "COUNTDOWN: " + _currentTime);
    }
}

 

BikeController

public class BikeController : MonoBehaviour
{
    private string _status;

    private void OnEnable()
    {
        RaceEventBus.Subscribe(RaceEventType.START, StartBike);
        RaceEventBus.Subscribe(RaceEventType.STOP, StopBike);
    }

    private void OnDisable()
    {
        RaceEventBus.Unsubscribe(RaceEventType.START, StartBike);
        RaceEventBus.Unsubscribe(RaceEventType.STOP, StopBike);
    }

    private void StartBike()
    {
        _status = "Started";
    }

    private void StopBike()
    {
        _status = "Stopped";
    }

    private void OnGUI()
    {
        GUI.color = Color.green;
        GUI.Label(new Rect(10, 60, 200, 20), "BIKE STATUS: " + _status);
    }
}

 

HUDController

public class HUDController : MonoBehaviour
{
    private bool _isDisplayOn;

    private void OnEnable()
    {
        RaceEventBus.Subscribe(RaceEventType.START, DisplayHUD);
    }

    private void OnDisable()
    {
        RaceEventBus.Unsubscribe(RaceEventType.START, DisplayHUD);
    }

    private void DisplayHUD()
    {
        _isDisplayOn = true;
    }

    private void OnGUI()
    {
        if(_isDisplayOn)
        {
            if(GUILayout.Button("Stop Race"))
            {
                _isDisplayOn = false;
                RaceEventBus.Publish(RaceEventType.STOP);
            }
        }
    }

}

 

핵심 구성 요소가 분리된 채로 이벤트들을 분리하여, 개별 오브젝트의 동작을 특정 순서로 트리거할 수 있습니다. ( 오브젝트끼리 직접 통신하지 않습니다. ) 간단히 구독자나 게시자로 오브젝트를 추가 혹은 제거할 수 있습니다.

 


 

추가내용

이벤트 버스 패턴 vs 옵저버 패턴

 

1. 옵저버 패턴(Observer Pattern)

객체가 다른 객체의 상태 변화를 감지하고 이에 반응하도록 하는 디자인 패턴입니다. 일반적으로 한 객체(주체, Subject)가 상태를 변경하면, 그 상태를 관찰하고 있는 다른 객체들(옵저버, Observer)에게 알림을 보내는 방식으로 동작합니다.

  • Subject: 상태를 보유하고 있고, 상태 변경 시 옵저버들에게 알림을 보냅니다.
  • Observer: 주체의 상태 변화를 관찰하며, 알림을 받았을 때 적절한 행동을 수행합니다.

장점:

  • 객체 간의 결합도를 낮출 수 있습니다.
  • 주체가 다수의 옵저버에게 동시에 알림을 보낼 수 있어 유연한 구조를 제공합니다.

단점:

  • 옵저버가 많아질수록 성능에 부담을 줄 수 있습니다.
  • 순환 참조(Circular Dependency) 문제를 일으킬 수 있습니다.

사용 예시:

  • 객체 간의 명시적 관계가 필요한 경우: 주체(Subject)와 옵저버(Observer) 간의 관계가 명확하고, 주체의 상태 변화가 특정 옵저버들에게만 영향을 주는 경우에 적합합니다.
  • 상태 변경을 즉시 반영해야 하는 경우: 주체의 상태 변화가 옵저버들에게 즉각적으로 전달되어야 하는 상황에서 유리합니다.
  • 단일 애플리케이션 내의 간단한 통신: 작은 규모의 애플리케이션에서 주체와 옵저버 간의 관계가 명확하고, 복잡한 이벤트 흐름이 필요하지 않은 경우 적합합니다.
  • 이벤트 흐름이 간단할 때: 단순한 이벤트 체계로 주체와 옵저버 간의 상호작용을 제어할 때 유리합니다.
  • GUI 툴킷에서 이벤트 시스템 (버튼 클릭 시 여러 컴포넌트에게 알림을 보냄)
  • 모델-뷰-컨트롤러(MVC) 아키텍처에서 모델과 뷰 간의 상태 동기화

2. 이벤트 버스 패턴(Event Bus Pattern)

시스템의 여러 구성 요소가 중앙 이벤트 버스를 통해 서로 통신하는 방식입니다. 이벤트 버스는 중앙에서 모든 이벤트를 관리하며, 이벤트 발생 시 관련된 모든 구독자(Subscriber)에게 이를 전달합니다.

  • Event Bus: 이벤트의 중앙 허브 역할을 하며, 이벤트를 받아서 등록된 구독자들에게 전달합니다.
  • Publisher: 이벤트를 발행하는 역할을 합니다.
  • Subscriber: 이벤트를 수신하고 처리하는 역할을 합니다.

장점:

  • 서로 직접 참조하지 않아도 되므로 구성 요소 간의 결합도를 낮출 수 있습니다.
  • 여러 모듈이나 컴포넌트가 이벤트 버스를 통해 쉽게 통신할 수 있습니다.

단점:

  • 모든 이벤트가 중앙 버스를 통해 전달되므로, 버스가 병목이 될 수 있습니다.
  • 시스템의 규모가 커지면 이벤트 흐름을 추적하기 어려울 수 있습니다.

사용 예시:

  • 대규모 애플리케이션: 시스템의 여러 모듈이나 컴포넌트가 서로 독립적으로 동작하면서 중앙 이벤트 버스를 통해 통신해야 하는 경우 적합합니다.
  • 느슨한 결합이 필요한 경우: 이벤트 발생자(Publisher)와 수신자(Subscriber)가 서로를 직접 참조하지 않고, 독립적으로 동작해야 할 때 유리합니다.
  • 이벤트의 복잡한 흐름 관리: 다양한 이벤트가 발생하고, 여러 컴포넌트가 그 이벤트를 처리해야 하는 복잡한 상황에서 유용합니다.
  • 확장성: 시스템이 확장되어 새로운 모듈이 추가되더라도, 기존 모듈에 큰 영향을 주지 않고 쉽게 통합할 수 있는 구조가 필요할 때 적합합니다.
  • 대규모 애플리케이션에서 모듈 간의 통신
  • 마이크로서비스 아키텍처에서 서비스 간의 이벤트 기반 통신

비교

  • 결합도:
    • 옵저버 패턴은 주체와 옵저버 간에 어느 정도의 결합이 존재합니다(옵저버가 주체를 알고 있어야 함).
    • 이벤트 버스 패턴은 구성 요소들이 이벤트 버스를 통해 간접적으로 연결되므로 결합도가 더 낮습니다.
  • 확장성:
    • 옵저버 패턴은 주체가 변경될 때 옵저버의 동작도 함께 고려해야 할 수 있습니다.
    • 이벤트 버스는 새로운 이벤트나 구독자를 추가해도 기존 구성 요소에 미치는 영향이 적습니다.
  • 성능:
    • 옵저버 패턴은 특정 주체에 대한 옵저버들이 직접 연결되어 있어, 이벤트가 즉각적으로 전달됩니다.
    • 이벤트 버스 패턴은 중앙 버스를 경유하므로, 대규모 시스템에서는 성능 저하가 발생할 수 있습니다.

요약

옵저버 패턴은 특정 객체와 그 객체를 관찰하는 여러 객체 간의 관계를 명시적으로 정의하는 패턴이고, 이벤트 버스 패턴은 더 느슨한 결합을 허용하며 시스템의 다양한 구성 요소가 중앙 이벤트 버스를 통해 통신하는 방식입니다. 각각의 패턴은 사용 목적과 상황에 따라 적절히 선택하여 사용해야 합니다.


소스코드 링크 : https://github.com/PacktPublishing/Game-Development-Patterns-with-Unity-2021-Second-Edition/tree/main