// Copyright (c) Pixel Crushers. All rights reserved. using UnityEngine; using System.Collections.Generic; using System; namespace PixelCrushers { /// /// General purpose message system. /// public static class MessageSystem { public class ListenerInfo { public IMessageHandler listener; public string message; public string parameter; public int frameAdded; public bool removed; public ListenerInfo() { } public ListenerInfo(IMessageHandler listener, string message, string parameter) { this.listener = listener; this.message = message; this.parameter = parameter; this.frameAdded = Time.frameCount; this.removed = false; } public void Assign(IMessageHandler listener, string message, string parameter) { this.listener = listener; this.message = message; this.parameter = parameter; this.frameAdded = Time.frameCount; this.removed = false; } public void Clear() { this.listener = null; this.message = null; this.parameter = null; this.removed = false; } } private static List s_listenerInfo = new List(); private static Pool s_listenerInfoPool = new Pool(); private static HashSet s_sendersToLog = new HashSet(); private static HashSet s_listenersToLog = new HashSet(); private static bool s_sendInEditMode = false; private static bool s_allowReceiveSameFrameAdded = true; private static bool s_debug = false; private static bool s_allowExceptions = false; private static int s_sendMessageDepth = 0; #if UNITY_2019_3_OR_NEWER && UNITY_EDITOR [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] static void InitStaticVariables() { s_listenerInfo = new List(); s_listenerInfoPool = new Pool(); s_sendersToLog = new HashSet(); s_listenersToLog = new HashSet(); s_sendInEditMode = false; s_allowReceiveSameFrameAdded = true; s_debug = false; s_sendMessageDepth = 0; } #endif /// /// Send messages even when not playing. /// public static bool sendInEditMode { get { return s_sendInEditMode; } set { s_sendInEditMode = value; } } /// /// Allow listeners to receive messages on the same frame they registered with the MessageSystem. /// public static bool allowReceiveSameFrameAdded { get { return s_allowReceiveSameFrameAdded; } set { s_allowReceiveSameFrameAdded = value; } } /// /// Log message system activity. /// public static bool debug { get { return s_debug && Debug.isDebugBuild; } set { s_debug = value; } } /// /// Don't catch exceptions thrown by message recipients. /// public static bool allowExceptions { get { return s_allowExceptions && Debug.isDebugBuild; } set { s_allowExceptions = value; } } private static List listenerInfo { get { return s_listenerInfo; } } private static Pool listenerInfoPool { get { return s_listenerInfoPool; } } /// /// When we're in SendMessage(), don't remove items from listenerInfo because SendMessage() is /// currently looping through listenerInfo. Instead, mark them for removal afterward. /// private static int sendMessageDepth { get { return s_sendMessageDepth; } set { s_sendMessageDepth = value; } } /// /// Checks if the specified listener, message, and parameter is registered with the message system. /// /// Listener to check. /// Message to check. /// Parameter to check, or blank for any parameter. /// public static bool IsListenerRegistered(IMessageHandler listener, string message, string parameter) { for (int i = 0; i < listenerInfo.Count; i++) { var x = listenerInfo[i]; if (!x.removed && x.listener == listener && string.Equals(x.message, message) && (string.Equals(x.parameter, parameter) || string.IsNullOrEmpty(x.parameter))) { return true; } } return false; } /// /// Adds a listener. /// /// Listener. /// Message to listen for. /// Message parameter to listen for, or blank for any parameter with the message. public static void AddListener(IMessageHandler listener, string message, string parameter) { if (debug) Debug.Log("MessageSystem.AddListener(listener=" + listener + ": " + message + "," + parameter + ")"); // Check if listener is already registered: for (int i = 0; i < listenerInfo.Count; i++) { var x = listenerInfo[i]; if (x.listener == listener && string.Equals(x.message, message) && (string.Equals(x.parameter, parameter) || string.IsNullOrEmpty(x.parameter))) { x.removed = false; return; } } // Otherwise add: var info = listenerInfoPool.Get(); info.Assign(listener, message, parameter); listenerInfo.Add(info); } /// /// Adds a listener. /// /// Listener. /// Message to listen for. /// Message parameter to listen for, or blank for any parameter with the message. public static void AddListener(IMessageHandler listener, StringField message, StringField parameter) { AddListener(listener, StringField.GetStringValue(message), StringField.GetStringValue(parameter)); } /// /// Adds a listener. /// /// Listener. /// Message to listen for. /// Message parameter to listen for, or blank for any parameter with the message. public static void AddListener(IMessageHandler listener, StringField message, string parameter) { AddListener(listener, StringField.GetStringValue(message), parameter); } /// /// Adds a listener. /// /// Listener. /// Message to listen for. /// Message parameter to listen for, or blank for any parameter with the message. public static void AddListener(IMessageHandler listener, string message, StringField parameter) { AddListener(listener, message, StringField.GetStringValue(parameter)); } /// /// Removes a listener from listening to a specific message and parameter. /// /// Listener. /// Message to no longer listen for, or blank for all messages. /// Message parameter, or blank for all parameters. public static void RemoveListener(IMessageHandler listener, string message, string parameter) { if (debug) Debug.Log("MessageSystem.RemoveListener(listener=" + listener + ": " + message + "," + parameter + ")"); if (listenerInfo.Count <= 0) return; for (int i = listenerInfo.Count - 1; i >= 0; i--) { var x = listenerInfo[i]; if (x.listener == listener && (string.Equals(x.message, message) || string.IsNullOrEmpty(message)) && (string.Equals(x.parameter, parameter) || string.IsNullOrEmpty(parameter))) { x.removed = true; if (sendMessageDepth == 0) { listenerInfo.RemoveAt(i); x.Clear(); listenerInfoPool.Release(x); } } } } private static void RemoveMarkedListenerInfo() { var listenersToRemove = listenerInfo.FindAll(x => x.removed); listenerInfo.RemoveAll(x => x.removed); for (int i = 0; i < listenersToRemove.Count; i++) { var listenerToRemove = listenersToRemove[i]; listenerToRemove.Clear(); listenerInfoPool.Release(listenerToRemove); } } /// /// Removes a listener from listening to a specific message and parameter. /// /// Listener. /// Message to no longer listen for. /// Messaeg parameter, or blank for all parameters. public static void RemoveListener(IMessageHandler listener, StringField message, StringField parameter) { RemoveListener(listener, StringField.GetStringValue(message), StringField.GetStringValue(parameter)); } /// /// Removes a listener from listening to a specific message and parameter. /// /// Listener. /// Message to no longer listen for. /// Messaeg parameter, or blank for all parameters. public static void RemoveListener(IMessageHandler listener, StringField message, string parameter) { RemoveListener(listener, StringField.GetStringValue(message), parameter); } /// /// Removes a listener from listening to a specific message and parameter. /// /// Listener. /// Message to no longer listen for. /// Messaeg parameter, or blank for all parameters. public static void RemoveListener(IMessageHandler listener, string message, StringField parameter) { RemoveListener(listener, message, StringField.GetStringValue(parameter)); } /// /// Removes a listener from listening to all messages. /// public static void RemoveListener(IMessageHandler listener) { RemoveListener(listener, string.Empty, string.Empty); } /// /// Log a debug message when this object sends a message. /// public static void LogWhenSendingMessages(GameObject sender) { if (sender == null) return; s_sendersToLog.Add(sender); } /// /// Stop logging debug messages when this object sends a message. /// public static void StopLoggingWhenSendingMessages(GameObject sender) { if (sender == null) return; s_sendersToLog.Remove(sender); } /// /// Log a debug message when this listener receives a message. /// public static void LogWhenReceivingMessages(GameObject listener) { if (listener == null) return; s_listenersToLog.Add(listener); } /// /// Stop logging debug messages when this listener receives a message. /// public static void StopLoggingWhenReceivingMessages(GameObject listener) { if (listener == null) return; s_listenersToLog.Add(listener); } private static bool ShouldLogSender(object sender) { if (sender is UnityEngine.Object && (sender as UnityEngine.Object) == null) return false; return (sender is GameObject && s_sendersToLog.Contains(sender as GameObject)) || (sender is Component && s_sendersToLog.Contains((sender as Component).gameObject)); } private static bool ShouldLogReceiver(IMessageHandler receiver) { return (receiver is Component && (receiver as Component) != null && s_listenersToLog.Contains((receiver as Component).gameObject)); } /// /// Sends a message to listeners. /// /// Object/info about object that's sending the message. /// Intended recipient, or null for any. /// Message. /// Message parameter. /// Any number of additional values to send with message. public static void SendMessageWithTarget(object sender, object target, string message, string parameter, params object[] values) { if (!(Application.isPlaying || sendInEditMode)) return; if (debug || ShouldLogSender(sender)) Debug.Log("MessageSystem.SendMessage(sender=" + sender + ((target == null) ? string.Empty : (" target=" + target)) + ": " + message + "," + parameter + ")"); var messageArgs = new MessageArgs(sender, target, message, parameter, values); // struct passed on stack; no heap allocated. try { sendMessageDepth++; for (int i = 0; i < listenerInfo.Count; i++) { var x = listenerInfo[i]; if (x == null || x.removed) continue; if (x.listener == null) { x.removed = true; continue; } if (!allowReceiveSameFrameAdded && x.frameAdded == Time.frameCount) continue; if (string.Equals(x.message, message) && (string.Equals(x.parameter, parameter) || string.IsNullOrEmpty(x.parameter))) { if (allowExceptions) { SendMessageToListener(x, sender, target, message, parameter, messageArgs); } else { try { SendMessageToListener(x, sender, target, message, parameter, messageArgs); } catch (System.Exception e) { Debug.LogError("Message System exception sending '" + message + "'/'" + parameter + "' to " + x.listener + ": " + e.Message); } } } } } finally { sendMessageDepth--; if (sendMessageDepth == 0) RemoveMarkedListenerInfo(); } } private static void SendMessageToListener(ListenerInfo x, object sender, object target, string message, string parameter, MessageArgs messageArgs) { if (ShouldLogReceiver(x.listener)) { Debug.Log("MessageSystem.SendMessage(sender=" + sender + ((target == null) ? string.Empty : (" target=" + target)) + ": " + message + "," + parameter + ")"); } x.listener.OnMessage(messageArgs); } /// /// Sends a message to listeners. /// /// Object/info about object that's sending the message. /// Intended recipient, or null for any. /// Message. /// Message parameter. /// Any number of additional values to send with message. public static void SendMessageWithTarget(object sender, object target, StringField message, string parameter, params object[] values) { SendMessageWithTarget(sender, target, StringField.GetStringValue(message), parameter, values); } /// /// Sends a message to listeners. /// /// Object/info about object that's sending the message. /// Intended recipient, or null for any. /// Message. /// Message parameter. /// Any number of additional values to send with message. public static void SendMessageWithTarget(object sender, object target, StringField message, StringField parameter, params object[] values) { SendMessageWithTarget(sender, target, StringField.GetStringValue(message), StringField.GetStringValue(parameter), values); } /// /// Sends a message to listeners. /// /// Object/info about object that's sending the message. /// Intended recipient, or null for any. /// Message. /// Message parameter. /// Any number of additional values to send with message. public static void SendMessageWithTarget(object sender, object target, string message, StringField parameter, params object[] values) { SendMessageWithTarget(sender, target, message, StringField.GetStringValue(parameter), values); } /// /// Sends a message to listeners. /// /// Object/info about object that's sending the message. /// Message. /// Message parameter. /// Any number of additional values to send with message. public static void SendMessage(object sender, string message, string parameter, params object[] values) { SendMessageWithTarget(sender, null, message, parameter, values); } /// /// Sends a message to listeners. /// /// Object/info about object that's sending the message. /// Message. /// Message parameter. /// Any number of additional values to send with message. public static void SendMessage(object sender, StringField message, StringField parameter, params object[] values) { SendMessageWithTarget(sender, null, StringField.GetStringValue(message), StringField.GetStringValue(parameter), values); } /// /// Sends a message to listeners. /// /// Object/info about object that's sending the message. /// Message. /// Message parameter. /// Any number of additional values to send with message. public static void SendMessage(object sender, StringField message, string parameter, params object[] values) { SendMessageWithTarget(sender, null, StringField.GetStringValue(message), parameter, values); } /// /// Sends a message to listeners. /// /// Object/info about object that's sending the message. /// Message. /// Message parameter. /// Any number of additional values to send with message. public static void SendMessage(object sender, string message, StringField parameter, params object[] values) { SendMessageWithTarget(sender, null, message, StringField.GetStringValue(parameter), values); } /// /// Sends a message. If the message contains a colon (:), the part after the /// colon is sent as the parameter. If it contains a second colon, the part /// after the second colon is sent as a value. /// public static void SendCompositeMessage(object sender, string message) { if (string.IsNullOrEmpty(message)) return; var parameter = string.Empty; object value = null; if (message.Contains(":")) // Parameter? { var colonPos = message.IndexOf(':'); parameter = message.Substring(colonPos + 1); message = message.Substring(0, colonPos); if (parameter.Contains(":")) // Value? { colonPos = parameter.IndexOf(':'); var valueString = parameter.Substring(colonPos + 1); parameter = parameter.Substring(0, colonPos); int valueInt; bool isNumeric = int.TryParse(valueString, out valueInt); if (isNumeric) value = valueInt; else value = valueString; } } if (value == null) { SendMessage(sender, message, parameter); } else { SendMessage(sender, message, parameter, value); } } } }