이벤트 버스
게임 시스템 간 타입 안전한 동기 발행/구독.
게임 시스템 간 타입 안전 발행/구독 · 동기, 메인 스레드 전용 ·
Subscribe는IDisposable토큰을 반환 · no-op 대체물:NullEventBus
무엇을 하는가
이벤트 버스는 시스템들이 서로를 모른 채 대화하게 해 줍니다. 발행자가 bus.Publish(new PlayerDamagedEvent { ... })를 호출하면, bus.Subscribe<PlayerDamagedEvent>(handler)로 등록된 모든 핸들러가 구독한 순서대로 동기적으로 이를 받습니다. 이벤트 타입은 컴파일 타임에 매칭됩니다 — 리플렉션도, 동적 조회도 없습니다 — 그래서 IL2CPP(AOT 컴파일) 빌드에서도 버스가 안전하게 동작합니다. 입력 서비스, 씬 전환, UI 패널 스택, 그리고 여러분의 게임 코드를 잇는 배관입니다.
빠른 예제
이벤트 클래스를 정의하고 OnEnable에서 구독, OnDisable에서 구독 해제:
using System;
using CommonGameSystem.Core;
using UnityEngine;
// Any plain class works as an event.
public class PlayerDamagedEvent
{
public int Amount;
}
[DefaultExecutionOrder(100)]
public class DamageUI : MonoBehaviour
{
private IEventBus _bus;
private IDisposable _subscription;
private void Awake()
{
_bus = ServiceLocator.Resolve<IEventBus>(); // Cache it once
}
private void OnEnable()
{
_subscription = _bus.Subscribe<PlayerDamagedEvent>(OnPlayerDamaged);
}
private void OnDisable()
{
_subscription?.Dispose();
}
private void OnPlayerDamaged(PlayerDamagedEvent ev)
{
Debug.Log($"Damage: {ev.Amount}");
// Update UI, play a sound, etc.
}
}
발행은 메인 스레드 어디서든 한 줄입니다:
_bus.Publish(new PlayerDamagedEvent { Amount = 12 });
하나의 스코프 안에서만 살아야 하는 핸들러라면 using이 토큰을 자동으로 dispose합니다:
private void RunScopedListener()
{
using var token = _bus.Subscribe<PlayerDamagedEvent>(ev => Debug.Log(ev.Amount));
// The handler is active here...
} // ...and unsubscribed automatically when the scope ends.
전체 API
IEventBus 인터페이스
-
IDisposable Subscribe<TEvent>(Action<TEvent> handler) where TEvent : classTEvent타입 이벤트의 핸들러를 등록합니다.Dispose()가 구독을 해제하는 토큰을 반환합니다 — 두 번 dispose해도 안전하며 두 번째는 아무 일도 하지 않습니다. 이벤트 타입은 참조 타입(class또는record)이어야 합니다. 핸들러가 null이면ArgumentNullException을 던집니다. -
void Publish<TEvent>(TEvent ev) where TEvent : class정확히TEvent타입으로 등록된 모든 핸들러를 구독 순서대로(먼저 구독한 쪽이 먼저 호출) 동기적으로 호출합니다. 핸들러가 없으면 조용히 반환합니다. 이벤트가 null이면ArgumentNullException을 던집니다. 예외를 던진 핸들러는 잡혀서 로그에 남고, 나머지 핸들러는 계속 실행됩니다.
EventBusOptions 튜닝 (선택)
기본 버스는 부트스트랩이 기본 옵션으로 등록합니다. 직접 버스를 생성한다면(테스트용, 또는 격리된 서브 버스) 다음을 조정할 수 있습니다:
var bus = new DefaultEventBus(
EventBusOptions.Default.With(
initialListCapacity: 8,
warnSubscribersPerType: 100));
WarnSubscribersPerType(기본 50): 에디터 전용 임계값; 어떤 이벤트 타입의 구독자 수가 이를 초과하면 타입당 한 번 경고를 기록합니다. 대개 구독 누수의 신호입니다.WarnPublishDepth(기본 8): 에디터 전용 임계값; 발행 호출이 이보다 깊게 중첩되면(핸들러가 또 다른 이벤트를 발행하는 식) 한 번 경고합니다. 발행 호출 스택이 완전히 풀리면 경고가 다시 활성화됩니다.InitialListCapacity(기본 4): 메모리 튜닝 — 타입별 구독자 리스트의 시작 크기.
EventBusOptions는 불변 struct입니다: EventBusOptions.Default에서 시작해 .With(...)로 개별 값을 재정의하십시오.
EventBusDiagnostics (에디터 전용)
버스 상태 점검을 위해 에디터 빌드에서만 제공됩니다:
#if UNITY_EDITOR
int count = EventBusDiagnostics.SubscribeCount<MyEvent>(_bus);
int depth = EventBusDiagnostics.PeekPublishDepth(_bus);
long warnings = EventBusDiagnostics.WarningsEmitted;
#endif
끄는 방법
ServiceLocator.Replace<IEventBus>(new NullEventBus());
NullEventBus는 모든 발행을 조용히 버리고 Subscribe에서 무해한 no-op 토큰을 반환합니다. 호출자 코드를 하나도 바꾸지 않고 테스트나 특수 빌드에서 이벤트 라우팅을 끌 때 사용하십시오. 다른 서비스(입력, UI, 씬 플로우)는 계속 동작합니다 — 단지 이벤트를 받지 않게 될 뿐입니다. 로깅도, 부작용도 없습니다.
동작 및 엣지 케이스
-
메인 스레드 전용. 모든
Subscribe와Publish호출은 Unity 메인 스레드에서 이루어져야 합니다. 디버그 빌드는 이를 assert합니다. Release 빌드는 속도를 위해 검사를 생략하므로, 거기서의 백그라운드 스레드 호출은 상태를 오염시킬 수 있습니다 — 절대 하지 마십시오. 워커 스레드에서 발행하려면 먼저 작업을 메인 스레드로 되돌려 큐에 넣으십시오(스케줄러 서비스에 정확히 이 용도의 메인 스레드 실행 디스패치가 있습니다). -
디스패치는 스냅샷 위에서 돕니다. 각
Publish는 발행 시작 시점에 찍은 구독자 리스트의 스냅샷을 순회합니다. 그래서 디스패치 도중 핸들러가 — 자기 자신마저도 — 구독하거나 구독 해제해도 안전합니다: 변경은 진행 중인 발행이 아니라 다음 발행부터 적용됩니다. 스냅샷은 발행마다의 작은 할당이며, 구독자가 많은 상태에서 발행 빈도가 극단적으로 높으면 가비지 컬렉터 압력으로 나타나므로, 아주 수다스러운 업데이트는 가능한 한 묶어서 보내십시오. -
버스를 캐시하고, 매번 Resolve하지 마십시오.
Resolve<IEventBus>()는 딕셔너리 조회입니다.Update에서 호출하면 프레임마다 그 비용을 냅니다.Awake나Start에서 한 번 얻어 참조를 유지하십시오. -
이벤트 타입은
class또는record여야 하며,struct는 안 됩니다. 디스패치를 단순하게 유지하고 박싱 할당을 피하기 위함입니다. -
상속 기반 디스패치는 없습니다. 기반 이벤트 타입을 구독해도 파생 이벤트 타입은 받지 못합니다.
Publish<DerivedEvent>를 호출할 때Subscribe<BaseEvent>핸들러는 호출되지 않습니다. 발행하는 바로 그 타입을 구독하십시오. -
구독 토큰을 dispose하십시오.
Dispose()를 잊으면 핸들러가 영원히 등록된 채로 남습니다 — 구독한 오브젝트까지 살려 두는 메모리 누수입니다.OnDisable/OnDestroy에서 구독을 해제하거나, 스코프 한정 핸들러에는using을 사용하십시오. -
핸들러 예외는 디스패치를 깨뜨리지 않습니다. 한 핸들러가 예외를 던지면 예외는 로그에 남고 다음 핸들러는 계속 실행됩니다. 여러분의 발행 호출은 항상 정상적으로 완료됩니다.
-
커스텀 이벤트 클래스는 IL2CPP에서
link.xml이 필요합니다. IL2CPP로 빌드한다면, 빌드의 코드 스트리핑이 이벤트 클래스를 제거하지 않도록 프로젝트의link.xml에 추가하십시오:<assembly fullname="YourNamespace" preserve="all"> <type fullname="YourNamespace.PlayerDamagedEvent" preserve="all"/> </assembly>