Quick Flow
이벤트 channel은 asset으로 하나 만들고, 발신자가 Raise, 수신자가 OnEnable에서 등록하고 OnDisable에서 해제합니다. channel은 scene object reference를 없애 주지만, 호출 순서·listener lifetime·gameplay 상태 보장을 대신하지는 않습니다.
DamageDealer --Raise()--> DamageEvent asset --Action--> UI / audio / quest listeners| 질문 | channel이 맞는 경우 | 직접 호출이 더 나은 경우 |
|---|---|---|
| 누가 반응하는가 | 여러 독립 시스템이 반응 | 단일 필수 collaborator가 있음 |
| scene 교체 | publisher와 listener가 서로 다른 scene 수명 | 같은 prefab 내부의 명확한 dependency |
| 호출 결과 | fire-and-forget notification | return value·실패 처리·순서 보장이 필요 |
| 디버깅 | event 이름·payload·등록자를 추적할 수 있음 | call stack이 바로 보여야 함 |
Channel asset과 API
channel asset에는 event 자체와 payload type만 둡니다. Inspector에서 [CreateAssetMenu]로 asset을 만들고, publisher·listener의 [SerializeField] field에 같은 asset을 연결합니다. channel마다 이름이 겹치지 않게 Player/Damaged, UI/Opened처럼 domain과 과거형/명령형 규칙을 정합니다.
using System;
using UnityEngine;
[CreateAssetMenu(menuName = "Events/Void Event")]
public sealed class VoidEventChannel : ScriptableObject
{
private event Action raised;
public void Subscribe(Action listener) => raised += listener;
public void Unsubscribe(Action listener) => raised -= listener;
public void Raise() => raised?.Invoke();
}field를 public Action으로 열지 않고 Subscribe·Unsubscribe를 통해서만 연결하면, 나중에 null check, duplicate registration log, payload validation을 channel 안에 넣을 수 있습니다. Raise()가 synchronous call이라는 점도 중요합니다. listener 하나가 예외를 던지거나 오래 걸리면 이후 listener와 publisher frame에 영향을 줍니다. critical path에는 명시적 service API나 command queue가 더 적합할 수 있습니다.
구독 lifecycle과 payload
listener는 channel과 같은 asset 수명보다 짧습니다. 그래서 활성화될 때 등록하고 비활성화·파괴될 때 같은 delegate instance를 해제합니다. lambda를 익명으로 새로 만들어 등록·해제하면 서로 다른 delegate라 해제가 되지 않을 수 있으므로 method group 또는 저장한 delegate를 사용합니다.
public sealed class DamageSoundListener : MonoBehaviour
{
[SerializeField] private VoidEventChannel damaged;
private void OnEnable()
{
damaged.Subscribe(PlaySound);
}
private void OnDisable()
{
damaged.Unsubscribe(PlaySound);
}
private void PlaySound()
{
// Audio service 호출
}
}HP, attacker, damage type처럼 data가 필요하면 Action<DamageInfo> channel을 만들고 DamageInfo를 immutable value struct 또는 명확한 data class로 정의합니다. listener가 payload object를 수정해 다른 listener 결과를 바꾸지 않도록 ownership을 정합니다.
Domain Reload를 끈 Editor Play Mode에서는 asset과 static state의 listener 잔류가 test 간 누수로 보일 수 있습니다. session 시작 reset, OnDisable 대칭 해제, test teardown 중 어느 방식으로 cleanup할지 정하고, listener 목록을 무조건 asset에 serialize하지 않습니다.
자주 틀리는 부분
| 증상 | 원인 | 수정 |
|---|---|---|
| scene을 다시 열수록 한 event가 여러 번 재생 | OnEnable 등록만 하고 해제하지 않음 | OnDisable/OnDestroy에서 같은 listener 해제 |
| Unsubscribe가 안 됨 | 익명 lambda를 새로 만들어 서로 다른 delegate 사용 | method group 또는 저장한 delegate 사용 |
| channel이 누가 반응하는지 알 수 없음 | naming·payload·등록 규칙 없음 | domain별 asset, payload 이름, debug trace 정책 추가 |
| publisher가 listener 결과를 필요로 함 | notification을 request/command처럼 사용 | 명시적 service method 또는 result contract 사용 |
| event 순서가 게임 규칙이 됨 | listener 등록 순서에 gameplay를 의존 | 핵심 상태 전이는 한 owner의 명시적 API로 이동 |
참고 링크
3 sources