// EventBetter // Copyright (c) 2018, Piotr Gwiazdowski using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using UnityEngine; using UnityEngine.LowLevel; using UnityEngine.PlayerLoop; /// /// Intentionally made partial, in case you want to extend it easily. /// public static partial class EventBetter { /// /// Register a message handler. /// /// The will be invoked every time a message of type is raised, /// unless gets destroyed or one of Unlisten/Clear methods is called. /// /// /// /// /// /// After the is invoked - unlisten automatically. /// If is a Behaviour or GameObject, will only invoke /// if is active and enabled. /// Thrown if the internal worker has been disabled somehow. public static void Listen(ListenerType listener, System.Action handler, bool once = false, bool excludeInactive = false, bool persistent = false) where ListenerType : UnityEngine.Object { HandlerFlags flags = HandlerFlags.IsUnityObject; if (once) flags |= HandlerFlags.Once; if (excludeInactive) flags |= HandlerFlags.OnlyIfActiveAndEnabled; if (persistent) flags |= HandlerFlags.Persistent; RegisterInternal(listener, handler, flags); } /// /// Register a message handler. No listener, you unregister by calling Dispose on returned object. /// Handler is not limited in what it is allowed to capture. /// /// /// /// public static IDisposable ListenManual(System.Action handler) { // use the dict as a listener here, it will ensure the handler is going to live forever var actualHandler = RegisterInternal(s_entries, (msg) => handler(msg), HandlerFlags.None); return new ManualHandlerDisposable() { Handler = actualHandler, MessageType = typeof(MessageType) }; } /// /// Invoke all registered handlers for this message type immediately. /// /// /// /// True if there are any handlers for this message type, false otherwise. public static bool Raise(MessageType message) { return RaiseInternal(message); } /// /// Unregisters all handlers for a given listener. /// /// /// /// True if there were any handlers, false otherwise. public static bool Unlisten(UnityEngine.Object listener) { if (listener == null) throw new ArgumentNullException("listener"); return UnregisterInternal(typeof(MessageType), listener, (eventEntry, index, referenceListener) => object.ReferenceEquals(eventEntry.listeners[index], referenceListener)); } /// /// Unregisters all message types for a given listener. /// /// /// True if there were any handlers, false otherwise. public static bool UnlistenAll(UnityEngine.Object listener) { if (listener == null) throw new ArgumentNullException("listener"); bool anyListeners = false; foreach (var entry in s_entriesList) { anyListeners |= UnregisterInternal(entry, listener, (eventEntry, index, referenceListener) => object.ReferenceEquals(eventEntry.listeners[index], referenceListener)); } return anyListeners; } /// /// Unregisters everything. /// public static void Clear(bool clearPersistent = true) { if (clearPersistent) { s_entries.Clear(); s_entriesList.Clear(); } else { foreach (var entry in s_entriesList) { RemoveNonPersistentHandlers(entry); } } } /// /// Removes handlers that will now longer be called because their listeners have been destroyed. Normally /// there's no reason to call that, since EventBetter does it behind the scenes in every LateUpdate. /// public static void RemoveUnusedHandlers() { foreach (var entry in s_entriesList) { RemoveUnusedHandlers(entry); } } #region Coroutine Support /// /// Use this in coroutines. Yield will return when at least one event of type /// has been raised. To get the messages use or /// /// /// /// public static YieldListener ListenWait() where MessageType : class { return new YieldListener(); } public class YieldListener : System.Collections.IEnumerator, IDisposable where MessageType : class { private Delegate handler; public List Messages { get; private set; } public MessageType First { get { if (Messages == null || Messages.Count == 0) return null; return Messages[0]; } } internal YieldListener() { handler = EventBetter.RegisterInternal, MessageType>( this, (msg) => OnMessage(msg), HandlerFlags.DontInvokeIfAddedInAHandler); } public void Dispose() { if (handler != null) { EventBetter.UnlistenHandler(typeof(MessageType), handler); handler = null; } } private void OnMessage(MessageType msg) { if (Messages == null) { Messages = new List(); } Messages.Add(msg); } bool System.Collections.IEnumerator.MoveNext() { if (Messages != null) { Dispose(); return false; } return true; } object System.Collections.IEnumerator.Current { get { return null; } } void System.Collections.IEnumerator.Reset() { } } #endregion #region Async Support public static async Task ListenAsync() { var tcs = new TaskCompletionSource(); var handler = RegisterInternal(s_entries, (msg) => tcs.SetResult(msg), HandlerFlags.DontInvokeIfAddedInAHandler); try { return await tcs.Task; } finally { EventBetter.UnlistenHandler(typeof(MessageType), handler); } } #endregion #region Private private class ManualHandlerDisposable : IDisposable { public Type MessageType { get; set; } public Delegate Handler { get; set; } public void Dispose() { if (Handler == null) return; try { UnlistenHandler(MessageType, Handler); } finally { MessageType = null; Handler = null; } } } [Flags] private enum HandlerFlags { None = 0, OnlyIfActiveAndEnabled = 1 << 0, Once = 1 << 1, DontInvokeIfAddedInAHandler = 1 << 2, IsUnityObject = 1 << 3, Persistent = 1 << 4, } private class EventEntry { public uint invocationCount = 0; public bool needsCleanup = false; public readonly List listeners = new List(); public readonly List handlers = new List(); public readonly List flags = new List(); public int Count { get { return listeners.Count; } } public bool HasFlag(int i, HandlerFlags flag) { return (flags[i] & flag) == flag; } public void SetFlag(int i, HandlerFlags flag, bool value) { if (value) { flags[i] |= flag; } else { flags[i] &= ~flag; } } public void Add(object listener, Delegate handler, HandlerFlags flag) { UnityEngine.Debug.Assert(listeners.Count == handlers.Count); // if not in a handler, don't set this flag as it would ignore first // nested handler if (invocationCount == 0) flag &= ~HandlerFlags.DontInvokeIfAddedInAHandler; listeners.Add(listener); handlers.Add(handler); flags.Add(flag); } public void NullifyAt(int i) { UnityEngine.Debug.Assert(listeners.Count == handlers.Count); listeners[i] = null; handlers[i] = null; flags[i] = HandlerFlags.None; } public void RemoveAt(int i) { UnityEngine.Debug.Assert(listeners.Count == handlers.Count); listeners.RemoveAt(i); handlers.RemoveAt(i); flags.RemoveAt(i); } } /// /// For lookups. /// private static Dictionary s_entries = new Dictionary(); /// /// For faster iteration. /// private static List s_entriesList = new List(); private static bool RaiseInternal(T message) { EventEntry entry; if (!s_entries.TryGetValue(typeof(T), out entry)) return false; bool hadActiveHandlers = false; var invocationCount = ++entry.invocationCount; try { int initialCount = entry.Count; for (int i = 0; i < entry.Count; ++i) { var listener = GetAliveTarget(entry.listeners[i]); bool removeHandler = true; if (listener != null) { if (entry.HasFlag(i, HandlerFlags.OnlyIfActiveAndEnabled)) { var behaviour = listener as UnityEngine.Behaviour; if (!ReferenceEquals(behaviour, null)) { if (!behaviour.isActiveAndEnabled) continue; } var go = listener as GameObject; if (!ReferenceEquals(go, null)) { if (!go.activeInHierarchy) continue; } } if (i >= initialCount) { // this is a new handler; if it has a protection flag, don't call it if (entry.HasFlag(i, HandlerFlags.DontInvokeIfAddedInAHandler)) { entry.SetFlag(i, HandlerFlags.DontInvokeIfAddedInAHandler, false); continue; } } if (!entry.HasFlag(i, HandlerFlags.Once)) { removeHandler = false; } ((Action)entry.handlers[i])(message); hadActiveHandlers = true; } if (removeHandler) { if (invocationCount == 1) { // it's OK to compact now entry.RemoveAt(i); --i; --initialCount; } else { // need to wait entry.needsCleanup = true; entry.NullifyAt(i); } } } } finally { UnityEngine.Debug.Assert(invocationCount == entry.invocationCount); --entry.invocationCount; if (invocationCount == 1 && entry.needsCleanup) { entry.needsCleanup = false; RemoveUnusedHandlers(entry); } } return hadActiveHandlers; } private static Delegate RegisterInternal(ListenerType listener, System.Action handler, HandlerFlags flags) { return RegisterInternal(listener, handler, flags); } private static Delegate RegisterInternal(object listener, Action handler, HandlerFlags flags) { if (listener == null) throw new ArgumentNullException("listener"); if (handler == null) throw new ArgumentNullException("handler"); if ((flags & HandlerFlags.IsUnityObject) == HandlerFlags.IsUnityObject) { Debug.Assert(listener is UnityEngine.Object); } EventEntry entry; if (!s_entries.TryGetValue(typeof(T), out entry)) { entry = new EventEntry(); s_entries.Add(typeof(T), entry); s_entriesList.Add(entry); } entry.Add(listener, handler, flags); return handler; } private static bool UnlistenHandler(Type messageType, Delegate handler) { return EventBetter.UnregisterInternal(messageType, handler, (eventEntry, index, _handler) => eventEntry.handlers[index] == _handler); } private static bool UnregisterInternal(Type messageType, ParamType param, Func predicate) { EventEntry entry; if (!s_entries.TryGetValue(messageType, out entry)) { return false; } return UnregisterInternal(entry, param, predicate); } private static bool UnregisterInternal(EventEntry entry, ParamType param, Func predicate) { bool found = false; for (int i = 0; i < entry.Count; ++i) { if (entry.listeners[i] == null) continue; if (predicate != null && !predicate(entry, i, param)) continue; found = true; if (entry.invocationCount == 0) { // it's ok to compact now entry.RemoveAt(i); --i; } else { // need to wait entry.needsCleanup = true; entry.NullifyAt(i); } } return found; } private static object GetAliveTarget(object target) { if (target == null) return null; var targetAsUnityObject = target as UnityEngine.Object; if (object.ReferenceEquals(targetAsUnityObject, null)) return target; if (targetAsUnityObject) return target; return null; } private static void RemoveUnusedHandlers(EventEntry entry) { for (int i = 0; i < entry.Count; ++i) { var listener = entry.listeners[i]; if (entry.HasFlag(i, HandlerFlags.IsUnityObject)) { if ((UnityEngine.Object)listener != null) continue; } else { if (listener != null) continue; } if (entry.invocationCount == 0) entry.RemoveAt(i--); else entry.NullifyAt(i); } } private static void RemoveNonPersistentHandlers(EventEntry entry) { for (int i = 0; i < entry.Count; ++i) { if (entry.HasFlag(i, HandlerFlags.Persistent)) { continue; } Debug.Assert(entry.invocationCount == 0); entry.RemoveAt(i--); } } #endregion #region Player Loop System Registration struct EventBetterCleanupSystem { } static EventBetter() { #if UNITY_EDITOR UnityEditor.EditorApplication.playModeStateChanged += (change) => { if (change == UnityEditor.PlayModeStateChange.ExitingPlayMode || change == UnityEditor.PlayModeStateChange.ExitingEditMode) { Clear(clearPersistent: false); } }; #endif var rootPlayerLoopSystem = PlayerLoop.GetCurrentPlayerLoop(); var playerLoopSystemTypes = GetPlayerLoopSystemHierarchy(typeof(PreLateUpdate.ScriptRunBehaviourLateUpdate)); RegisterPlayerLoopSystem(ref rootPlayerLoopSystem, playerLoopSystemTypes); PlayerLoop.SetPlayerLoop(rootPlayerLoopSystem); Type[] GetPlayerLoopSystemHierarchy(Type systemType) { List result = new(); for (var t = systemType; t != null; t = t.DeclaringType) { result.Insert(0, t); } return result.ToArray(); } void RegisterPlayerLoopSystem(ref PlayerLoopSystem parentSystem, Span subSystemTypes) { Debug.Assert(subSystemTypes.Length >= 1); ref PlayerLoopSystem[] list = ref parentSystem.subSystemList; if (list != null) { for (int i = 0; i < list.Length; ++i) { if (list[i].type != subSystemTypes[0]) { continue; } if (subSystemTypes.Length == 1) { // insert after var newSystem = new PlayerLoopSystem() { type = typeof(EventBetterCleanupSystem), updateDelegate = EventBetter.RemoveUnusedHandlers }; List tmpList = list.ToList(); tmpList.Insert(i+1, newSystem); list = tmpList.ToArray(); } else { RegisterPlayerLoopSystem(ref list[i], subSystemTypes.Slice(1)); } return; } } throw new InvalidOperationException($"SubSystem {subSystemTypes[0]} is not found in {parentSystem.type}"); } } #endregion }