Game engine agnostic robust, synchronous pub/sub C# messaging solution, mostly geared towards Unity and XNA/Monogame. See this recorded talk.
- Open Unity Package Manager
- Open the Advanced Package Settings
- Add an entry for a new "Scoped Registry"
- Name:
NPM
- URL:
https://registry.npmjs.org
- Scope(s):
com.wallstop-studios.dxmessaging
andcom.wallstop-studios.unity-helpers
- Name:
- Resolve the latest
DxMessaging
This project has a dependency on my Unity Helpers
project, which contains the System.Runtime.CompilerServices.Unsafe.dll
, which is used for some speed hacks in DxMessaging directly. Unity Helpers bundles a few (small) dependencies, including protobuf. If you don't want these dependencies, or if they conflict in some way, you can either include a copy of the System.Runtime.CompilerServices.Unsafe.dll
yourself without relying on UnityHelpers, or manually download and include the Unity Helpers project and delete anything that conflicts with your project. Or, manually download this project without UnityHelpers. The choice is yours.
DxMessaging is currently a bit slower (2-3x) than Unity's built in messaging solution (when running in Unity). Source.
Message Tech | Operations / Second |
---|---|
Unity | 2,509,901 |
DxMessaging (GameObject) - Normal | 1,031,034 |
DxMessaging (Component) - Normal | 1,066,371 |
DxMessaging (GameObject) - No-Copy | 1,001,639 |
DxMessaging (Component) - No-Copy | 1,080,738 |
DxMessaging (Untargeted) - No-Copy | 1,389,542 |
While not as fast, DxMessaging offers additional functionality as compared to Unity's messaging solution.
Feature | Unity | DxMessaging |
---|---|---|
Require knowledge of receiver's implementation | âś“ | _ |
Send a message to a GameObject | âś“ | âś“ |
Send a message to a Component | _ | âś“ |
Ignore messages dynamically at runtime | _ | âś“ (multiple ways) |
Send messages to all receivers | _ | âś“ |
Listen to messages for another GameObject | _ | âś“ (multiple ways) |
Listen to messages for another Component | _ | âś“ (multiple ways) |
Listen to messages from another GameObject | _ | âś“ (multiple ways) |
Listen to messages from another Component | _ | âś“ (multiple ways) |
Send a message without boxing its parameters | _ | âś“ |
Listen to all messages | _ | âś“ |
View a filter-able history of message registrations | N/A | âś“ |
"LateUpdate" style handlers | _ | âś“ |
There are a few important concepts that DxMessaging provides.
- MessageBus: An implementation of the
IMessageBus
interface, configured to relay messages. There is no limit to these. By default there is only one, the global message bus. - Emitter: Any piece of code, anywhere can emit messages to any MessageBus. By default messages are emitted to the global message bus.
- Receiver: A piece of code that has registered with a message bus to receive messages of certain types from it.
- UntargetedMessage: Messages that are global, and will be sent to every receiver that is registered for this type.
- TargetedMessage: Messages that are intended for a receiver, like a piece of mail or a command.
- BroadcastMessage: Messages that are from an emitter, indicating events that happened to the emitter.
Receivers can subscribe to any number of message types.
- For targeted messages, generally receivers listen to their own object as the target, but the receiver can opt to listen to any target, or even all targets.
- For broadcast messages, receivers can listen to any source, including themselves, or even all sources.
For Unity, we have an easy-to-integrate MessageAwareComponent - simply extend any component you want off of this base class. This will handle message registration lifetimes automatically for you. If you have your own base classes or aren't using Unity, then you'll need to add lifetimes yourself. Please use the MessageAwareComponent as reference.
Please note, if you want to receive messages and inherit off of the MessageAwareComponent
, the component implements several Unity-specific methods to manage lifetimes. Specifically,
protected virtual void Awake()
protected virtual void OnEnable()
protected virtual void OnDisable()
protected virtual void OnDestroy()
protected virtual void OnApplicationQuit()
If you wish to use any of these methods in components that inherit from MessageAwareComponent
, please make sure to have the overrides call the base methods, otherwise messaging may break or not work as expected.
I'm piloting some new tech in version 2 of this library. Currently, if implementing any of the message interfaces, there will be boxing performed on the messages, if they are structs. This is due to the nature of default interface methods, which I was not aware of.
To get around this, I have updated both the Unity Helpers dependent package, as well as this package, to take advantage of Roslyn Source Generators.
Requirements:
- Use one of
DxTargetedMessage, DxBroadcastMessage, DxUntargetedMessage
depending on message requirement. - Ensure that the struct or class is marked as
partial
.
To use:
[DxTargetedMessage]
public partial struct SimpleTargetedMessage // No longer needed : ITargetedMessage<SimpleTargetedMessage>
{
}
[DxBroadcastMessage]
public partial struct SimpleBroadcastMessage // No longer needed : IBroadcastMessage<SimpleBroadcastMessage>
{
}
[DxUntargetedMessage]
public partial struct SimpleUntargetedMessage // No longer needed : IUntargetedMesssage<SimpleUntargetedMessage>
{
}
Or, if you already have messages, you can leave the interface implementations as-is and get a no-boxing implementation with the DxAutoMessageType
attribute. partial
is still required.
To use:
[DxAutoMessageType]
public partial struct SimpleTargetedMessage : ITargetedMessage<SimpleTargetedMessage>
{
}
See the tests directory for examples about how to integrate with the MessageAwareComponent. But, for some starters:
public readonly struct SimpleTargetedMessage : ITargetedMessage<SimpleTargetedMessage>
{
}
public sealed class SimpleMessageAwareComponent : MessageAwareComponent
{
protected override void RegisterMessageHandlers()
{
_ = _messageRegistrationToken.RegisterGameObjectTargeted<SimpleTargetedMessage>(gameObject, HandleSimpleTargetedMessage);
}
-
private void HandleSimpleTargetedMessage(ref SimpleTargetedMessage message)
{
Debug.Log("Received SimpleTargetedMessage.");
}
}
// In some other bit of code
// Select a target
SimpleMessageAwareComponent target = Object.FindObjectOfType<SimpleMessageAwareComponent>();
// Create your message
SimpleTargetedMessage message = new();
// Send it - this will synchrously invoke all relevant handlers and return execution once complete
message.EmitGameObjectTargeted(target.gameObject);
There are three things that need to be done in order to integrate with any of the message types.
The message must be defined. Messages are classes or structs that implement one of the three message interfaces - IUntargetedMessage
, ITargetedMessage
, or IBroadcastMessage
. Structs are generally more efficient, as they don't allocate any memory on the heap. For most of my code that uses this framework, I prefer readonly
structs, as I never have a use case where I want to mutate the message in the typical caller path, as there is no guarantee of message receiver order of execution.
Note: Each of the message types has a more specialized interface that can be inherited from - IUntargetedMessage<T>
, ITargetedMessage<T>
, or IBroadcastMessage<T>
. Implementers of these generic forms will avoid boxing in the messaging system. The core messaging system requires knowledge of the message's type, which will by default call object.GetType()
, which will box structs, generating some garbage. These specialized versions of the message interfaces avoid boxing and allocation, as the messaging system is able to reason about their runtime types more efficiently.
Note: Message definition is totally up to the user of the framework and is one of the primary benefits of the framework. The messages can be arbitrarily complex and have references to any kind of object or property.
public readonly struct SimpleTargetedMessage : ITargetedMessage<SimpleTargetedMessage> {}
public readonly struct SimnpleUntargetedMessage : IUntargetedMessage<SimnpleUntargetedMessage> {}
public readonly struct SimpleBroadcastMessage : ITargetedMessage<SimpleBroadcastMessage> {}
Now that you have a message, you need code that is able to listen to it. The easiest way of doing this is by having a class inherit off of MessageAwareComponent
(Unity). This class will take care of all of the registration lifetime hooks for you automatically.
Note: By default, message listeners are only active if their object / script is active. If you want an implementer to always listen to messages, you can have your implementing class override the MessageRegistrationTiedToEnableStatus
property to return false
. If you do this, the component will still have its listeners actively cleaned up upon script destruction, so there are no leaks or additional worries you have to worry about.
If you cannot inherit from MessageAwareComponent, then you can lift its registration logic and state into your own base/implementing class - there's nothing special about it to the messaging system.
Note: Implementing from MessageAwareComponent
will cause a MessagingComponent
to automatically be added to the GameObject. This component serves as a centralized registration brain and the messaging system expects that there is only one of these per GameObject. There are some "one brain per object" internal invariants that the messaging system core relies upon.
Note: Message listeners can be one of two forms:
void Listener(ref SimpleTargetedMessage message) {}
void Listener(SimpleTargetedMessage) {}
Form one causes a copy to happen. Form two is copy-free. This distinction matters only when your messages are value types (structs). Please note that the behavior of the system is undefined if you update what the message is pointing to while using the ref
concept.
public sealed class MyCoolMessageReceiver : MessageAwareComponent
{
protected override void RegisterMessageHandlers()
{
_ = _messageRegistrationToken.RegisterGameObjectTargeted(gameObject, (ref SimpleTargetedMessage message) => Debug.Log("Received SimpleTargetedMessage!"));
}
}
Now that you have both a message definition and a listener, you can now emit a message! In any piece of code, anywhere, messages can be emitted without any dependency on Unity or MessageAwareComponents.
GameObject target = null; // Thing you want to send your target to, this can *not* be null
SimpleTargetedMessage targetedMessage = new();
targetedMessage.EmitGameObjectTargeted(target);
That's it! Once the call from EmitGameObjectTargeted
completes, all message listeners will have executed.
See Message Emission Functions for more information on the ways that messages can be emitted.
Note: There is no limit to the number of listeners for any given message. Note: Message instances can be cached and re-emitted, if you think this is a good idea for your code. Note: Message registration automatically dedupes listeners - even if your code registers a listener more than once, it will only be called once.
public readonly struct SimpleUntargetedMessage : IUntargetedMessage<SimpleUntargetedMessage>
{
public readonly string debugMessage;
public SimpleUntargetedMessage(string debugMessage)
{
this.debugMessage = debugMessage;
}
}
public sealed class SimpleUntargetedReceiver : MessageAwareComponent
{
protected override void RegisterMessageHandlers()
{
_ = _messageRegistrationToken.RegisterUntargeted((ref SimpleUntargetedMessage message) => Debug.Log($"Received SimpleUntargetedMessage {message.debugMessage}."));
}
}
// In some piece of code
SimpleUntargetedMessage message = new("Hello, world");
message.EmitUntargeted();
UntargetedMessages are a great fit for when you do not care about a sender or a receiver context. That is, the only thing you care about is that the message is sent and, potentially, the contents of the message. UntargetedMessages will be received by all active listeners, they're essentially global messages. What would normally require some global event bus or static event handlers is now completely decomposed and decoupled into the sender and receiver, each without knowledge of the other.
public readonly struct SimpleTargetedMessage : ITargetedMessage<SimpleTargetedMessage>
{
public readonly string debugMessage;
public SimpleTargetedMessage(string debugMessage)
{
this.debugMessage = debugMessage;
}
}
public sealed class SimpleTargetedReceiver : MessageAwareComponent
{
protected override void RegisterMessageHandlers()
{
_ = _messageRegistrationToken.RegisterGameObjectTargeted((ref SimpleTargetedMessage message) => Debug.Log($"Received SimpleTargetedMessage {message.debugMessage}."));
}
}
// In some piece of code
GameObject target = null; // You need a reference to the thing you're targeting, it can't be null
SimpleTargetedMessage message = new($"I'm targeting you, {target.name}");
message.EmitGameObjectTargeted(target);
TargetedMessages are a great fit for when you want to send a command to something. Instead of having to reach into the object's guts and find the event handler to call, the caller can just emit the message at the target object, and the message framework will take care of the handling automatically. This functionality basically replace looking up specific component(s) and calling public methods on them, allowing loose coupling between senders and receivers.
Note: TargetedMessages can be sent to either GameObjects or Components. If sent to a GameObject, all listeners on that object that have registered for GameObjectTargeted
will be invoked. If sent to a Component, only the listeners on that Component will be invoked. Recommendation is to use GameObjectTargeted
unless you absolutely require callers to differentiate between receivers. ComponentTargeted
requires knowledge of what Component to send the message to, requiring a tighter coupling than just knowing about a GameObject.
Note: TargetedMessages can be received as if they were UntargetedMessages. That is, listeners can register without needing a target to bind to at registration time. To do so, register a listener with the signature
void HandleSimpleTargetedMessageWithoutTargeting(ref InstanceId target, ref SimpleTargetedMessage message) {}
This listener will receive all messages of this type along with the target that the message is for. Unity users can get the GameObject or Component the message is from using InstanceId's .Object
property.
public readonly struct SimpleBroadcastMessage : IBroadcastMessage<SimpleBroadcastMessage>
{
public readonly string debugMessage;
public SimpleBroadcastMessage(string debugMessage)
{
this.debugMessage = debugMessage;
}
}
public sealed class SimpleBroadcastReceiver : MessageAwareComponent
{
[SerializeField]
private GameObject _thingToListenTo;
protected override void RegisterMessageHandlers()
{
_ = _messageRegistrationToken.RegisterGameObjectBroadcast(_thingToListenTo, (ref SimpleBroadcastMessage message) => Debug.Log($"Received SimpleBroadcastMessage {message.debugMessage}."));
}
}
// In the code that is the source of the message
SimpleBroadcastReceiver message = new("Something happened to me!");
// gameObject here is the gameObject property of the source
message.GameObjectBroadcast(gameObject);
BroadcastMessages are one of the most commonly used types of messages in the games that I build. Broadcast messages are events that happen to something. The thing that the event happens to broadcasts this message to anyone that is listening. Concepts like "my health changed", "I died", "I started channeling", where stuff is happening to you, the source code. This concept replaces a traditional event handler that has to be manually attached to, allowing for decoupling. The messaging system requires that receivers of BroadcastEvent must have some reference to the source object at registration time.
Note: BroadcastMessages can be sent from either GameObjects or Components. If sent from a GameObject, all listeners that have registered for events from that GameObject via RegisterGameObjectBroadcast
will be invoked. If sent from a Component, only listeners that have explicitly listened to that Component will be invoked. Recommendation is to use GameObjectBroadcast
unless you absolutely require receivers to differentiate between callers. ComponentBroadcast
requires knowledge of the specific Component that is sending the message, requiring tighter coupling than just knowing about a GameObject.
Note: BroadcastMessages can be received as if they were UntargetedMessages. That is, listeners do not need a source to bind to at registration time. To do so, register a listener with the signature
void HandleSimpleBroadcastMessageWithoutSource(ref InstanceId source, ref SimpleBroadcastMessage message) {}
This listener will receive all messages of this type along with the source that the message is from. Unify users can get the GameObject or Component the message is from using InstanceId's .Object
property.
The core functionality of the messaging system is for code to be sending and receiving messages of one of the three supported types. However, the messaging system provides additional functionality beyond this. With DxMessaging, you can...
Sometimes, depending on certain system state, you may want to have all listeners ignore certain types of messages. This is where the concept of Interceptors fits in - Interceptors are message pre-processors that run in a specified order. They have the following forms:
/// <summary>
/// Given an Untargeted message, determines whether or not it should be processed or skipped
/// </summary>
/// <typeparam name="TMessage">Specific type of message.</typeparam>
/// <param name="message">Message to consider.</param>
/// <returns>True if the message should be processed, false if it should be skipped.</returns>
public delegate bool UntargetedInterceptor<TMessage>(ref TMessage message) where TMessage : IUntargetedMessage;
/// <summary>
/// Given an Targeted message and its target, determines whether or not it should be processed or skipped.
/// </summary>
/// <typeparam name="TMessage">Specific type of message.</typeparam>
/// <param name="target">Target of the message.</param>
/// <param name="message">Message to consider.</param>
/// <returns>True if the message should be processed, false if it should be skipped.</returns>
public delegate bool TargetedInterceptor<TMessage>(ref InstanceId target, ref TMessage message) where TMessage : ITargetedMessage;
/// <summary>
/// Given an Broadcast message and its source, determines whether or not it should be processed or skipped.
/// </summary>
/// <typeparam name="TMessage">Specific type of message.</typeparam>
/// <param name="source">Source of the message.</param>
/// <param name="message">Message to consider.</param>
/// <returns>True if the message should be processed, false if it should be skipped.</returns>
public delegate bool BroadcastInterceptor<TMessage>(ref InstanceId source, ref TMessage message) where TMessage : IBroadcastMessage;
The primary use case of Interceptors is to block the actual emission of a message, by returning false
. Unlike the message handlers, where ref
is optional, ref
is the only form of the Interceptor's parameters. This is because, by design, the Interceptors can mutate the message, allowing for very interesting runtime behavior.
Note: ref
mutation isn't required, and will likely lead to confusing scenarios.
Note: Interceptors are ran sequentially. If any return false, the rest in line are not ran, and no message handler is ran.
Note: Interceptors run before messages of that type are handled, by design.
When registering an Interceptor, the system asks for a priority. Interceptors are ran from low -> high priority. Interceptors at the same priority are ran in the order registered.
Similar to a the LateUpdate
concept that many game engines provide, DxMessaging system provides registration for handlers that run after all regular handlers. These are referred to as PostProcessors. This concept is useful if you want to guarantee that some listener runs after another.
Note: PostProcessors will still be ran synchronously before the Emit
call finishes on the message.
DxMessaging provides hooks for listeners to register a GlobalAcceptAll
, where the listener will receive all messages that are sent through the system. This is particularly useful for networked applications where you want to serialize messages across the network, or if you have something like a HUD proxying messages from the player. This is an open-closed approach and allows for loosely coupled systems when the right need arises.
GlobalAcceptAll requires registration of listener functions for all three message types.
Note: GlobalAcceptAll listeners are ran before the normal listener and PostProcessing loop.
You can bind the MessagingDebug.Log
function to a custom logging function (likely Debug.Log) to get any error messages from the messaging system that indicate something has gone wrong.
DxMessaging provides a registration log that is turned off by default. This registration log can be turned on by referencing MessageBus.RegistrationLog
and setting Enabled=true
. You can then programatically dump/filter events.
By default, DxMessaging uses an implicit global message bus. But you can create as many MessageBuses as you like, if you want to segment your game space. Each registration and emission function is overloaded for the users to specify a message bus to send or listen for messages on. If the message bus is null, the global bus is used.
Message emission is relatively simple. Since the point of the framework is to decouple senders and receivers, the APIs are verbose to prevent bugs. Since it's possible to listen to and for messages involving either Components or GameObjects, my philosophy is that I'd rather have longer lines of code that are more descriptive ("I'm listening to this Component for this message) than accidentally have an incorrect coupling ("I sent this message to a Component when I meant to send it to a GameObject").
Each of the message extension functions aims to be as clear in its intent as possible.
This is the only way to emit an UntargetedMessage.
This is the Unity-agnostic way of emitting TargetedMessages. Unity users can use this by providing a GameObject or Component as the target, but it is discouraged.
Send a TargetedMessage to a particular GameObject. Only receivers that are registered via RegisterGameObjectTargeted
will be called. GameObject cannot be null.
Send a TargetedMessage to a particular Component. Only receivers that are registered via RegisterComponentTargeted
will be called. Component cannot be null.
This is the Unity-agnostic way of emitting BroadcastMessages. Unity users can use this by providing a GameObject or Component as the source, but it is discouraged.
Send a BroadcastMessage from a particular GameObject. Only listeners that are registered via RegisterGameObjectBroadcast
will be called. GameObject cannot be null.
Send a BroadcastMessage from a particular Component. Only listeners that are registered via RegisterComponentBroadcast
will be called. Component cannot be null.
The MessageRegistrationToken is used to automatically track the lifecycle of listeners as well as provide integration with the messaging system. This is the class that you will be interacting with the most in user code to register listeners. If you're using Unity, one is provided as a usable member variable named _messageRegistrationToken
in the base class MessageAwareComponent.
Each of the registration functions corresponds to one of the concepts linked above. They are heavily doc-commented and should be mostly self-explanatory.