Event Manager3의 싱글톤 EventBus를 좀 더 안전하게 만들어 보겠습니다.1. EventBus 구현[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]"플레이 시작할 때 static 변수들 깨끗하게 리셋해라”는 뜻입니다. var snapshot = list.ToArray(); Publish 도중에 발생하는 구조 변경으로부터 “이번 이벤트”를 격리하기 위해 snapshot을 쓰는 것 Publish 콜백에서 Publish / Subscribe / Unsubscribe가 일어날 수 있기 때문에 쓰는 것입니다. 그리고 “그 이벤트는 누구에게 전달되는가”를 고정하기 위해서입니다. EventBus.cs 파일 using System;
using System.Collections.Generic; using UnityEngine; public sealed class EventBus : MonoBehaviour { private static EventBus _instance; private static bool _isQuitting; public static EventBus Instance { get { if (_isQuitting) return null; if (_instance == null) { var go = new GameObject(nameof(EventBus)); _instance = go.AddComponent<EventBus>(); DontDestroyOnLoad(go); } return _instance; } } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void ResetStatics() { _instance = null; _isQuitting = false; } private void OnApplicationQuit() { _isQuitting = true; } // ---------------------------------------------------- private sealed class Subscription { public WeakReference TargetRef; public Delegate Callback; } private readonly Dictionary<Type, List<Subscription>> _events = new(); // ---------------------------------------------------- #region Subscribe / Unsubscribe public void Subscribe<T>(object subscriber, Action<T> callback) { if (subscriber == null) throw new ArgumentNullException(nameof(subscriber)); if (callback == null) throw new ArgumentNullException(nameof(callback)); // Lambda / 잘못된 캡처 차단 if (callback.Target != subscriber) { Debug.LogError( $"[EventBus] Lambda or foreign target detected. " + $"Subscriber: {subscriber}, Callback.Target: {callback.Target}"); return; } var type = typeof(T); if (!_events.TryGetValue(type, out var list)) { list = new List<Subscription>(); _events[type] = list; } list.Add(new Subscription { TargetRef = new WeakReference(subscriber), Callback = callback }); } public void Unsubscribe<T>(object subscriber, Action<T> callback) { if (subscriber == null || callback == null) return; var type = typeof(T); if (!_events.TryGetValue(type, out var list)) return; list.RemoveAll(sub => ReferenceEquals(sub.TargetRef.Target, subscriber) && sub.Callback == (Delegate)callback ); if (list.Count == 0) _events.Remove(type); } #endregion // ---------------------------------------------------- #region Publish public void Publish<T>(T evt) { var type = typeof(T); if (!_events.TryGetValue(type, out var list)) return; // Snapshot으로 컬렉션 수정 안전 var snapshot = list.ToArray(); foreach (var sub in snapshot) { var target = sub.TargetRef.Target; // GC된 경우 if (target == null) { list.Remove(sub); continue; } // Unity Object 파괴된 경우 if (target is UnityEngine.Object uObj && uObj == null) { list.Remove(sub); continue; } try { ((Action<T>)sub.Callback)?.Invoke(evt); } catch (Exception e) { Debug.LogException(e); } } if (list.Count == 0) _events.Remove(type); } #endregion } 2. Event 정의PlayerScoreEvent.cs 파일public class PlayerScoredEvent
{ public int Score { get; private set; } public PlayerScoredEvent(int score) { Score = score; } } 3. 이벤트 구독ScoreManager.cs 파일using UnityEngine;
public class ScoreManager : MonoBehaviour { void OnEnable() { EventBus.Instance.Subscribe<PlayerScoredEvent>(this, OnPlayerScored); } void OnDisable() { EventBus.Instance?.Unsubscribe<PlayerScoredEvent>(this, OnPlayerScored); } void OnPlayerScored(PlayerScoredEvent e) { Debug.Log("EventBus: Score: " + e.Score); } } 4. 이벤트 발행EventBus.Instance.Publish(new PlayerScoredEvent(10));
Subscribe / Unsubscribe를 어디서 할것인가? OnEnable / OnDisable → 오브젝트 활성화 상태에 따라 구독을 켰다 껐다 하는 패턴
결론: EventBus 같은 전역 이벤트에서는 OnDestroy에서 구독 해제하는 것이 방탄 패턴이 다. 5. 일반 오브젝트 사용하기MonoBehaviour를 상속하지 않은 일반 오브젝트를 EventBus에서 사용 하는 방법에 대해 알아 보겠습니다.Logger.cs 파일 using UnityEngine;
public class Logger { public Logger() { } private void OnEvent(PlayerScoredEvent evt) { Debug.Log("[Logger] PlayerScoredEvent Event received: " + evt.Score); } public void AddEvent() { EventBus.Instance.Subscribe<PlayerScoredEvent>(this, OnEvent); } public void RemoveEvent() { // Logger를 더 이상 사용하지 않으면 구독 해제 if(EventBus.Instance) EventBus.Instance.Unsubscribe<PlayerScoredEvent>(this, OnEvent); } } Logger를 ScoreManager 필드로 추가합니다. ScoreMnager.cs 파일 using UnityEngine;
public class ScoreManager : MonoBehaviour { private Logger _logger; // 일반 C# 객체 private void Awake() { _logger = new Logger(); } private void Start() { _logger.AddEvent(); } void OnEnable() { EventBus.Instance.Subscribe<PlayerScoredEvent>(this, OnPlayerScored); } void OnDisable() { if(EventBus.Instance) EventBus.Instance.Unsubscribe<PlayerScoredEvent>(this, OnPlayerScored); } void OnPlayerScored(PlayerScoredEvent e) { Debug.Log("EventBus: Score: " + e.Score); } private void OnDestroy() { _logger.RemoveEvent(); _logger = null; } } EventBus.Instance?.Unsubscribe<PlayerScoredEvent>(this, OnPlayerScored); 다음의 워닝이 발생 합니다. Unity objects should not use null propagation. 여러가지 해결 방법이 있지만 여기서는 if 문 null 체크를 사용해서 해결 하겠습니다. if(EventBus.Instance) EventBus.Instance.Unsubscribe<PlayerScoredEvent>(this, OnPlayerScored); |