Skip to content

Signals Module

Technical Docs / Signals
TYPE-SAFE DISPOSABLE SUBS AUTO-INIT

Signals
Module

구성 요소 간 결합도를 제거하는 타입 안전 이벤트 버스입니다.

발신자(Publisher)와 수신자(Subscriber)는 서로를 전혀 알지 못합니다. SignalHub가 유일한 중개자이며, 제네릭 타입 또는 정수 ID 두 가지 방식으로 시그널을 발행·구독할 수 있습니다. 구독은 IDisposable을 반환하므로 using으로 생명주기를 자동 관리합니다.

2 Subscribe Modes
FNV-1a String → Int ID Hash
0 Manual Init Required

흐름 구조

Publisher와 Subscriber는 SignalHub를 통해서만 연결됩니다. 양측은 서로의 존재를 알 필요가 없습니다.

Publisher
SignalHub.Publish(signal)
SignalHub
SignalBus (내부)
by Type by ID (int)
Subscriber A
Subscribe<TSignal>(handler)
Subscriber B
Subscribe(signalId, handler)
🔀 Dual Indexing

SignalBus는 리스너를 Typeint SignalId 두 딕셔너리로 동시에 관리합니다. 발행 시 두 인덱스 모두에 dispatch되므로, 타입 구독자와 ID 구독자가 동시에 알림을 받습니다. dispatch 전 리스너 목록을 스냅샷하여 콜백 중 구독 해제가 안전하게 동작합니다.

핵심 클래스

각 클래스의 역할과 계층 구조입니다.

interface ISignal / ISignal<T>

모든 시그널의 기본 계약. SignalId()로 정수 ID를 제공하며, ISignal<T>Payload()Publish()를 추가합니다.

// 페이로드 없는 시그널
public interface ISignal {
int SignalId();
}
// 페이로드 있는 시그널
public interface ISignal<out TPayload> : ISignal {
TPayload Payload();
void Publish();
}
abstract Signal / Signal<T>

구현 기반 클래스. Signal은 페이로드 없는 이벤트, Signal<T>는 값을 전달합니다. 정적 팩토리 메서드 Publish<TSignal>(payload)로 생성·발행을 한 번에 처리합니다.

// 구현 예시
private sealed class DamageSig : Signal<int> {
protected override int Id => 1042;
}
// 발행 (권장)
Signal<int>.Publish<DamageSig>(42);
static SignalHub

전역 정적 퍼사드. 내부적으로 싱글턴 SignalBus를 감쌉니다. 팩토리 기반 발행도 지원합니다 — ISignalFactory로 복잡한 페이로드 변환을 분리할 수 있습니다.

SignalHub.Publish(signal);
SignalHub.Subscribe<DamageSig>(OnDamage);
SignalHub.Subscribe(1042, OnSignal);
// 팩토리 발행
SignalHub.Publish(payload, factory);
static SignalTypeRegistry

Signal ID(int) ↔ Type 매핑 레지스트리. ID만 알고 있을 때 런타임에 시그널을 생성하거나 타입을 조회할 수 있습니다. Flow 노드와 에디터 도구에서 ID 기반 dispatch에 사용됩니다.

SignalTypeRegistry.Register<DamageSig>(1042);
SignalTypeRegistry.TryCreateSignal(1042, out var sig);
SignalTypeRegistry.TryGetSignalType(1042, out var type);
static partial SignalBootstrap

런타임과 에디터 진입 시 자동 초기화됩니다. [RuntimeInitializeOnLoadMethod][InitializeOnLoadMethod]로 수동 호출 없이 등록이 완료됩니다. 여러 ISignalsBootstrapBootstrapKey로 중복 없이 관리합니다.

public sealed class MyBootstrap : SignalsBootstrapBase {
public override string BootstrapKey => nameof(MyBootstrap);
protected override void OnInit() {
Signal.Register<DamageSig>();
}
}
abstract SO SignalFactory<TPayload, TSignal>

ScriptableObject 기반 팩토리. 데이터 모델 → 시그널 변환 로직을 에셋으로 분리합니다. SignalHub.Publish(payload, factory)와 함께 사용됩니다.

public class DamageFactory
: SignalFactory<HitData, DamageSig> {
public override DamageSig Create(HitData data) {
return new DamageSig(data.amount);
}
}

사용 패턴

01
시그널 정의

페이로드가 없으면 Signal, 값을 전달하면 Signal<T>를 상속합니다. Id는 고유한 정수값으로 지정합니다. [SignalId] 어트리뷰트를 인스펙터 필드에 붙이면 에디터에서 문자열 → FNV-1a 해시 ID로 자동 동기화됩니다.

// 페이로드 없음
public sealed class GameStartedSig : Signal {
protected override int Id => 1001;
}
// 정수 페이로드
public sealed class DamageSig : Signal<int> {
protected override int Id => 1042;
}
02
Bootstrap에 등록

런타임 초기화 시 Signal.Register<T>()로 시그널을 SignalTypeRegistry에 등록합니다. SignalBootstrap이 자동 호출하므로 수동 초기화 코드가 필요하지 않습니다.

protected override void OnInit() {
Signal.Register<GameStartedSig>();
Signal.Register<DamageSig>();
}
03
구독 (Subscribe)

반환값 IDisposable을 보관하고 있다가 Dispose()로 해제합니다. using var 문법으로 자동 해제도 가능합니다. 두 가지 방식 모두 동일한 버스를 공유합니다.

// 방식 1: 제네릭 타입 (권장)
_sub = SignalHub.Subscribe<DamageSig>(sig => {
int dmg = sig.Payload();
});
// 방식 2: 정수 ID
_sub = SignalHub.Subscribe(1042, sig => { ... });
// 해제
_sub.Dispose();
04
발행 (Publish)

페이로드가 있는 경우 정적 팩토리 메서드를 권장합니다. 내부에서 인스턴스를 생성하고 즉시 발행합니다.

// 페이로드 없음
SignalHub.Publish(new GameStartedSig());
// 페이로드 있음 (권장)
Signal<int>.Publish<DamageSig>(42);
// 팩토리 기반
SignalHub.Publish(hitData, _damageFactory);

Flow 노드 연동

Signals 모듈은 Flow 시스템의 SendSignalNodeWaitForSignalNode에 통합되어 있습니다. 노드 인스펙터에서 [SignalId] 필드로 시그널을 선택합니다.

📤
SendSignalNode
Fire-and-forget

SignalTypeRegistry.TryCreateSignal(signalId)로 시그널 인스턴스를 생성한 뒤 즉시 SignalHub.Publish()합니다. Flow 진입 즉시 실행되며 다음 노드로 즉각 이동합니다.

public override UniTask EnterAsync(CancellationToken ct) {
if (!SignalTypeRegistry.TryCreateSignal(signalId, out var sig))
return UniTask.CompletedTask;
SignalHub.Publish(sig);
return UniTask.CompletedTask;
}
WaitForSignalNode
Async gate

지정한 시그널이 도착할 때까지 Flow 실행을 일시 중단합니다. TaskCompletionSource로 대기하다가 시그널 수신 시 완료 처리됩니다. 구독은 IDisposable로 자동 정리됩니다.

public override async UniTask EnterAsync(CancellationToken ct) {
_subscription = SignalHub.Subscribe(nextSignalId, OnSignal);
await _waitSource.Task;
}
private void OnSignal(ISignal _) => _waitSource?.TrySetResult();

에디터 지원

🏷
[SignalId] Attribute

int 필드에 붙이면 인스펙터에서 문자열로 입력 가능합니다. SignalIdUtil.HashId() (FNV-1a)로 정수 ID를 자동 계산하여 동기화합니다. 휴먼 리더블한 이름으로 시그널을 관리할 수 있습니다.

🔗
SignalBridge (ScriptableObject)

커스텀 시그널 등록 로직을 에셋으로 분리합니다. Register()를 오버라이드하여 조건부 등록이나 런타임 주입 패턴에 활용합니다.