지연 이벤트 큐

이벤트를 지금 큐에 넣고, 나중에 여러분이 고른 플러시 지점에서 순서대로 발행합니다.

위험한 지점에서는 이벤트를 큐에 넣고, 여러분이 고른 안전한 지점에서 배치 전체를 순서대로 발행하세요.

인터페이스IDeferredBus
끄기 스위치NullDeferredBus
어셈블리CommonGameSystem.Core
시작부팅 시 자동 등록 (23개 서비스 중 하나) — 별도 설정 불필요

하는 일

지연 이벤트 큐는 이벤트 버스(IEventBus) 위의 얇은 계층입니다. 이벤트를 즉시 발행하는 대신 Enqueue합니다. 큐는 Flush()를 호출할 때까지 모든 것을 붙잡아 두었다가, 배치 전체를 이벤트 버스를 통해 발행합니다 — 모든 이벤트 타입에 걸쳐, 큐에 넣은 정확히 그 순서대로.

이벤트 발행을 위험한 지점 — 물리 콜백, 변경 중인 컬렉션을 도는 루프, 입력 처리 도중 — 에서 여러분이 고른 안전한 지점으로 옮기는 데 쓰세요. 큐가 소유하는 것은 보관과 순서뿐입니다. 라우팅과 구독자 처리는 이벤트 버스의 몫으로 남습니다. 자동 플러시는 없습니다: 배치가 언제 발화할지는 여러분이 결정합니다. 메인 스레드 전용입니다.

빠른 시작

이벤트를 평범한 클래스로 정의하세요:

public sealed class EnemyDiedEvent
{
    public int EnemyId { get; }
    public EnemyDiedEvent(int enemyId) { EnemyId = enemyId; }
}

그리고 지금 큐에 넣고, 나중에 발행하세요:

using CommonGameSystem.Core;
using UnityEngine;

public class MyDeferredPublisher : MonoBehaviour
{
    private IDeferredBus _bus;

    private void Awake() => _bus = ServiceLocator.Resolve<IDeferredBus>();

    public void OnEnemyDied(int enemyId)
    {
        _bus.Enqueue(new EnemyDiedEvent(enemyId));   // queued — NOT published yet
    }

    private void LateUpdate()
    {
        _bus.Flush();   // publishes the batch through the Event Bus, in queue order
    }
}

Bootstrap이 시작 시 이 서비스를 자동으로 등록합니다. 보여 준 대로 Awake()에서 한 번 resolve해 캐시하세요.

API 레퍼런스

IDeferredBus

멤버설명
void Enqueue<TEvent>(TEvent evt) where TEvent : classevt를 지금 큐에 넣습니다. Flush() 전에는 발행되지 않습니다. null 이벤트는 즉시 ArgumentNullException을 throw하므로 스택 트레이스가 호출자를 가리킵니다. class 제약은 이벤트 버스와 정확히 일치합니다 — 구조체 이벤트는 지원되지 않습니다. IL2CPP/AOT 빌드에서 안전하며, 아무것도 박싱되지 않습니다.
void Flush()큐에 쌓인 배치를 큐 순서대로, 모든 이벤트 타입에 걸쳐, 이벤트 하나당 이벤트 버스 Publish 한 번으로 발행합니다. 플러시가 실행되는 동안 큐에 들어온 이벤트는 다음 배치로 갑니다. 실행 중인 플러시 안에서 Flush를 호출하면 아무 일도 일어나지 않습니다.
int PendingCount { get; }큐에 있지만 아직 플러시되지 않은 이벤트 수. 진단용.

DeferredBusOptions — 생성 시점의 readonly struct

멤버설명
int MaxDrainPerFlushFlush당 발행되는 이벤트 상한. 0(기본)은 무제한. 0이 아닌 값(1–1,000,000으로 클램프)은 그만큼만 발행하고 나머지를 다음 배치의 맨 앞으로 넘기며, 스로틀된 경고를 한 번 로그합니다.
int InitialQueueCapacity내부 버퍼의 시작 크기. 기본 16, 0–4096으로 클램프.
bool LogLifecycletrue이면 에디터 빌드에서 큐/플러시 추적을 로그합니다. 이 추적은 릴리즈 빌드에서는 제거됩니다. 기본 false.
DeferredBusOptions(int maxDrainPerFlush, int initialQueueCapacity, bool logLifecycle = false)명시적 생성자. 숫자 범위는 생성 시 클램프됩니다.
static DeferredBusOptions Default0 / 16 / false. default(DeferredBusOptions)(0 / 0 / false)와는 다르다는 점에 유의하세요.
DeferredBusOptions With(int? maxDrainPerFlush = null, int? initialQueueCapacity = null, bool? logLifecycle = null)이름 있는 오버라이드를 적용한 불변 복사본.

예제

using CommonGameSystem.Core;
using UnityEngine;

public sealed class DamageDealtEvent
{
    public int EnemyId { get; }
    public int Amount { get; }
    public DamageDealtEvent(int enemyId, int amount) { EnemyId = enemyId; Amount = amount; }
}

public class DamageResolver : MonoBehaviour
{
    private IDeferredBus _deferred;

    private void Awake() => _deferred = ServiceLocator.Resolve<IDeferredBus>();

    // Called from inside a physics callback — publishing immediately here could
    // re-enter the collection we are iterating. Queue it instead.
    public void OnHit(int enemyId, int amount)
    {
        _deferred.Enqueue(new DamageDealtEvent(enemyId, amount));
    }

    // Publish at a known-safe point: the end of the fixed-update step.
    private void FixedUpdate()
    {
        if (_deferred.PendingCount > 0)
            _deferred.Flush();   // every queued event publishes here, in queue order
    }
}

끄는 방법

ServiceLocator.Replace<IDeferredBus>(new NullDeferredBus());

이렇게 하면 지연이 통째로 무음 처리됩니다. Enqueue는 이벤트를 조용히 버리고(실제 버스로 재라우팅되는 일은 없습니다), Flush는 아무것도 하지 않으며, PendingCount0에 머뭅니다. 실제 지연 큐에 연결된 게임은 지연 기능이 꺼진 채 계속 돌아갑니다. 이벤트 버스의 조용한 NullEventBus와 달리, 이 교체 구현은 생성 시 경고를 하나 로그합니다. 조용히 버려지는 큐("이벤트가 큐에는 들어가는 것 같은데 도착하지 않는다")는 진단이 어렵기에, 실수로 이뤄진 교체를 눈에 띄게 만든 것입니다. null 이벤트는 실제 서비스와 똑같이 여전히 throw합니다.

흔한 함정

  • Flush하기 전에는 아무것도 발화하지 않습니다. 자동 플러시는 없습니다. 큐에 넣고 플러시하지 않으면 이벤트는 영영 발행되지 않고 PendingCount는 무한히 자랍니다. 예측 가능한 드레인 지점 하나 — FixedUpdate의 끝, 프레임의 끝 — 를 정해 항상 거기서 플러시하세요.
  • 플러시 중에 큐에 들어온 이벤트는 다음 플러시를 기다립니다. 배치는 Flush가 시작되는 순간 고정됩니다. 배치가 발행되는 동안 구독자가 큐에 넣은 것은 다음 Flush까지 보관됩니다. 이것은 의도된 동작입니다: 모든 플러시 하나하나를 유한하게 유지합니다.
  • MaxDrainPerFlush는 배치를 나눌 뿐 — 이벤트를 버리지 않습니다. 상한에 도달하면 남은 이벤트는 다음 배치의 맨 앞으로 이동하므로, 나중에 큐에 들어온 것보다 여전히 먼저 발행됩니다. 순서는 보존되고, 스로틀된 경고가 한 번 로그됩니다. 압력 밸브이지, 폐기가 아닙니다.
  • 플러시는 절대 throw하지 않고, 나쁜 이벤트 하나가 나머지를 막을 수 없습니다. Publish 호출이 throw하면 큐가 잡아서 로그하고 남은 이벤트로 계속합니다. 프레임워크 자체의 이벤트 버스는 결함 있는 구독자를 스스로 이미 격리합니다. 이 가드는 그렇게 하지 않는 교체 버스를 대비한 것입니다.

관련 페이지

  • 이벤트 버스 — 이 큐가 발행을 통과시키는 라우팅 계층
  • 스케줄러 — 플러시 지점 대신, 선택한 시각에 콜백 실행
  • 로거 — 플러시 결함과 라이프사이클 추적이 가는 곳