Communication is key. In all aspects of life, problems can often be solved quicker through slick communication between people than raw individual brainpower alone. When making games, youâll encounter problems which are best solved by maintaining strong separation between the components involved and providing a robust way to send data between them. In this article, weâll look at messaging and event systems in Unity, discuss why theyâre important, and develop smarter ways to send data between objects.
The Basics
My favourite programming book, hands-down, is Game Programming Patterns by Bob Nystrom, and itâs available for free online. When I started to program games in Unity, thereâs a certain amount of organisational discipline enforced by Unityâs component structure, but I still ended up with monolith classes which invariably contained the word Manager
or Controller
somewhere in the name. This book details many strategies for splitting up classes and decoupling parts of the code from each other - one of those is called the Observer design pattern.
This pattern exists to replace direct function calls with a system where observers (a class which wishes to receive messages) subscribe to a subject (the class sending the messages). In this pattern, a subject doesnât know any details about who is subscribing to it - it just maintains a list of some Observer
type or similar. This is good because the subject can send the same message to any number of observers, and those observers can implement custom event-processing behaviour on their end. Without this pattern, the event-processing logic occurs on the subjectâs end, which strongly couples together the subject and all its observers. Weâll go into more detail about this pattern later.
Unity SendMessage
Unity has a built-in method which iterates through all components on a GameObject
and invokes all instances of a particular method. The SendMessage method takes the name of the function to invoke as a string
parameter, alongside an optional parameter of type object
to be used for sending arbitrary data with the message, and a third parameter to control how the messaging system behaves. Hereâs a simple example.
public GameObject attackTarget;
private void Attack()
{
attackTarget.SendMessage("TakeDamage", 10.0f);
}
Whenever the Attack
method is called, Unity will look at the components attached to the attackTarget
GameObject
, attempt to find a method called TakeDamage
accepting a single float
as a parameter on each object, and if one exists, invoke (i.e. call) it. The third parameter, which isnât included here, is an enum
called SendMessageOptions
with two possible values - RequireReceiver
and DontRequireReceiver
. When RequireReceiver
is used - the default behaviour - the receiving GameObject
must have a component attached with a matching method, else an error is logged to the console.
There are two similar variations with the same parameters as SendMessage
. Where SendMessage
only invokes the method on the singular GameObject
it is called on, SendMessageUpwards
also send the message to its parent, and its parentâs parent, and so on; BroadcastMessage
sends the message to the GameObject
plus all its children recursively.
Why is SendMessage
useful? Well, itâs easy to understand. You need a message sent somewhere? Just send it! From a âfire-and-forgetâ perspective, thereâs little effort required on the part of the programmer because thereâs no need to iterate through the list of components, or through parents and children, to send a whole heap of messages - just make sure the method names and parameters match and youâre good. If one extra data variable seems restrictive, then donât fret - since it extends object
you can send anything, including custom struct
data wrappers.
private void Attack()
{
var thing = new Thing();
attackTarget.SendMessage("TakeDamage", thing);
}
struct Thing
{
public float stuff;
public Quaternion rotation;
}
However, it isnât without drawbacks. SendMessage
and its variants all need to search through a potentially huge list of components to find those that contain a matching method, introducing significant amounts of overhead. Worse still, because they use strings for method names, they use reflection to identify matching methods. In this context, reflection is the ability to interact with and modify the type system at runtime - but invoking methods through reflection is slower than calling a method in the normal way. Thatâs fine if youâre using reflection once or twice, but if youâre frequently using SendMessage
then those small performance hits add up. Not only that, but because all of this is happening at runtime, thereâs no compile-time error checking at all. That makes it easier for small mistakes like typos in the method name take ages to debug.
Another huge drawback comes courtesy of another feature of reflection: you can call private methods on other objects. In the following scenario, where one object contains this code:
public GameObject attackTarget;
private void Start()
{
attackTarget.SendMessage("SuperPrivateMethod");
}
and a second object contains this code:
private void SuperPrivateMethod()
{
Debug.Log("My bank details are: [redacted]");
}
then the private method on the second object will be called, and privacy is a thing of the past.
A key potential issue with the architecture that weâve set up is that weâve coupled together the subject (the GameObject
sending the message) with the object itâs sending a message to. In some scenarios, this is fine. Picture two objects, where one has a trigger collider and contains a script intended to send a âhitâ message to the other. They might get away with the following methods:
// Sender
private void OnTriggerEnter(Collider other)
{
other.SendMessage("GetHit", SendMessageOptions.DontRequireReceiver);
}
// Receiver
public void GetHit()
{
Debug.Log("I'm hit!");
}
In this example, an observer pattern doesnât make much sense - that would mean all objects capable of being hit need to subscribe to all objects that could hit them. The sender would still need to know which object it hit anyway, so that all other subscribers can ignore the message since theyâre not being hit. However, an alternative scenario sees an object updating some UI element when it is hit, such as a health bar. Here, the health bar is just watching the health value of the first object and will spring into action when it changes, but the object shouldnât need to know that. The object ought to broadcast some message to the wider world mentioning its health has changed without worrying who is listening. Thatâs where events come in.
C# Events
In Game Programming Patterns, the Observer chapter opens by mentioning that C# bakes the pattern right into the language through the event
keyword. If you needed any indication that Observer is an important design pattern, thatâs it. Due to some heavy terminology, C# events can be a bit tricky to get used to if youâve never used them before, so weâll start with basic examples and work our way up.
A subject - or publisher as the C# documentation calls it - declares an event. You declare them kind of like a variable - it looks something like this:
public event EventHandler HealthChangedEvent;
Letâs take this apart word by word. The public
keyword is self-explanatory - you can also use other access modifiers like private
or protected
, which work as youâd expect here. If itâs private
, for example, then other objects wonât be able to subscribe to this event. The event
keyword comes next and, as expected, tells C# that this is an event which can be invoked. The next thing, which in this case is EventHandler
, is a delegate
method - itâs basically a stand-in that says âany subscribers must subscribe using a method with the same parameter list and return type as meâ. Itâs difficult to appreciate whatâs happening here without seeing examples, so rest assured Iâll go into more detail later. Just remember that in the most basic case, youâll probably write EventHandler
here. The final word is the eventâs name, which in our case is HealthChangedEvent
. The EventHandler
type is contained in the System
namespace, so make sure youâre using
it.
A publisher is responsible for invoking the event whenever it wants. In our scenario, we want to raise an event whenever a health variable changes. We do this using the Invoke
method. For the example, pretend the publisher has some other method which calls OnGetHit
at the appropriate time.
private int health = 100;
public event EventHandler HealthChangedEvent;
private void OnGetHit(int damage)
{
health -= damage;
HealthChangedEvent?.Invoke(this, EventArgs.Empty);
}
See the ?.
operator? Thatâs the null-conditional member access operator, which is an unnecessarily long name for âif not nullâ; if nobody has subscribed to the event, this null
check will fail and the event will not fire. The Invoke
method takes the same parameters as the event delegate - in our case, this is EventHandler
, which has an (object sender, EventArgs e)
signature. Usually, the sender
will be the publisher, so we can just use this
, and EventArgs
is a base type for additional data. For now, weâre including no extra data so we can use EventArgs.Empty
.
Thatâs the publisher sorted out. What about the subscribers? Well, to start off, the subscribers need access to the publisher, at least temporarily. Usually, a publisher-subscriber relationship is one-to-many, so the publisher should be easy to access - weâll pretend the subscribers already have access in our magical code black box, but in practice you could read up on the Service Locator design pattern (Game Programming Patterns comes to my rescue yet again). This is the inverse of the situation we had when using SendMessage
, where the message-sender needed a reference to the message-receiver. To receive events, a subscribing object needs a method with the same signature (except the method name) as the delegate.
private float healthBarLevel = 100;
private void UpdateHealthBar(object sender, EventArgs e)
{
// Update health bar here!
}
The subscriber needs to subscribe to the publisherâs event. This is handled using the +=
operator, where the thing being âaddedâ is the name of the method we just wrote. You can add several subscribers in this manner - those subscribers could be different instances of the same type as this one, or a mix of other types which have a method which matches the delegate, or you can even define a method on the publisher and subscribe it to its own event. Note that weâre still using a private
method, but since this class is responsible for subscribing its own methods, itâs a far cry from the SendMessage
approach which could raid another class through reflection. Assume we got the publisher
reference by magic elsewhere:
private void Start()
{
publisher.HealthChangedEvent += UpdateHealthBar;
}
I love the semantics of using this operator to mean âadd this method to the event subscriber listâ. Similar semantics are used when unsubscribing from an event - use the -=
operator.
private void OnDestroy()
{
publisher.HealthChangedEvent -= UpdateHealthBar;
}
Basic events are as easy as that! However, you probably noticed we didnât send any useful data to the subscribers. Letâs look at two ways to send extra data - the âcustom delegateâ way and the âcustom EventArgs
â way.
Custom delegates
Typically, this isnât the way youâll pass custom data through events (or at least, itâs not the way recommended by the C# docs), but I think itâs worth discussing how delegates work in a bit more detail. The problem we faced was that a basic EventHandler
and basic EventArgs
were unable to contain custom data, and one solution is to use a custom delegate to replace EventHandler
. Letâs discard EventArgs
completely.
public delegate void HealthChangedEventHandler(int health);
public event HealthChangedEventHandler HealthChangedEvent;
private void OnGetHit(int damage)
{
health -= damage;
HealthChangedEvent?.Invoke(health);
}
We define new delegates with the delegate
keyword. They are defined similarly to an abstract
method; all you need to supply is the method signature - its name, return types and parameter list. In our case, I called it HealthChangedEventHandler
so itâs clear what this is for, kept the void
return type, and changed the EventArgs
in the parameter list to an int
called health
. I also removed the sender
variable because we werenât using it. Then, we tweak the event
definition to use the new HealthChangedEventHandler
instead of EventHandler
. Finally, we change the invocation from using EventArgs.Empty
to using the health
value directly.
On the receiving end, we must change the subscribing methodâs parameter list to match the new delegate. Now that we have access to the health value, we can also do something with it in the method body.
private void UpdateHealthBar(int health)
{
healthBarLevel = health;
Debug.Log(health);
}
The C# documentation states that scenarios requiring a custom delegate are rare, but I hope this might clarify what a delegate is, and thereby solidify your understanding of what EventArgs
is doing in the original example. Itâs just a stand-in for a concrete method elsewhere in code. Now letâs look the other approach.
Custom EventArgs
To send custom data, weâll need to create a new class which derives from EventArgs
- you can put its definition in a separate file or below an existing class. This class holds the event data in the form of member variables; weâre not required to put anything else in here, but weâll add a constructor.
public class HealthEventArgs : EventArgs
{
public int health;
public HealthEventArgs(int health)
{
this.health = health;
}
}
To use HealthEventArgs
, we need to modify the event definition. So far, weâve used the regular version of EventHandler
, but in order to swap out EventArgs
can use the generic version instead. I wonât go into too many details about what generics are, but if youâve ever used a List<Something>
then thatâs generics in action - we use a type parameter to specify what the List
will hold. In our case, we can use EventHandler<HealthEventArgs>
and C# will know that the second parameter when we invoke the event needs to be of type HealthEventArgs
instead of the regular EventArgs
. Weâll also tweak the line where we fire the event - weâll construct a new HealthEventArgs
and use that instead of EventArgs.Empty
.
public event EventHandler<HealthEventArgs> HealthChangedEvent;
private void OnGetHit(int damage)
{
health -= damage;
HealthChangedEvent?.Invoke(this, new HealthEventArgs(health));
}
We also need to modify the subscriberâs method again to use HealthEventArgs
.
private void UpdateHealthBar(object sender, HealthEventArgs e)
{
healthBarLevel = e.health;
Debug.Log(e.health);
}
And thatâs how to use custom EventArgs
. We established in the SendMessage
section that there are problems that donât really fit the Observer pattern, but if youâve found a problem where you can reasonably choose between normal method calls, SendMessage
and C# events, then there are advantages of using C# events. Compared to SendMessage
, there is no reflection so itâs much faster, and we can take advantage of compile-time type checking to avoid common errors like misspelling a method name. And compared to a traditional method call, weâve removed the burden on the publisher to keep a list of all the objects it needs to send messages to; it makes a lot more sense for the subscribers to locate the publisher and register themselves as a listener. With events, a wide range of different classes can become subscribers, as long as they contain a matching method - with direct method calls, all âsubscribersâ would have to be the same type, or otherwise inherit a common interface
. In my opinion, events deal with this more elegantly. The drawback is that they take a bit longer to get used to, but hopefully this tutorial goes some way to alleviating that issue.
The other major drawback of C# events is that they exist solely within code, so they are not quite as friendly for non-programmers. Luckily, C# events are not the only mechanism for event invocation in Unity.
UnityEvents
One of the most powerful features of Unity is the ability to assign and modify things in the Inspector. When you create public
variables on a script, by default theyâll appear in the Inspector so that theyâre easy to change per instance. However, as we saw with C# events, there are some things that donât translate from code to the Editor very well. Thatâs where UnityEvent
s come in. A UnityEvent
is a simplified event callback mechanism which still allows you to subscribe and unsubscribe at runtime through code, but also can be exposed to the Inspector so that designers and other non-programmers can assign subscribers in-Editor.
The base UnityEvent
takes no parameters, like our first C# event example. The architecture is the same too. The publisher declares a UnityEvent
and is responsible for invoking it where appropriate. The class is contained in the UnityEngine.Events
namespace, so make sure youâre using
it.
public UnityEvent healthChangedEvent;
private void OnGetHit(int damage)
{
health -= damage;
healthChangedEvent.Invoke();
}
On the subscriberâs end, itâs not too different either. If we want to subscribe via code, weâll change the lines where we subscribe and unsubscribe. Instead of using +=
and -=
operators, weâll use AddListener
and RemoveListener
methods respectively.
private void Awake()
{
publisher.healthChangedEvent.AddListener(UpdateHealthBar);
}
private void UpdateHealthBar()
{
// Update health bar here!
}
private void OnDestroy()
{
publisher.healthChangedEvent.RemoveListener(UpdateHealthBar);
}
So far, this is possibly a bit easier to understand than C# events because there are fewer cumbersome keywords to keep track of, but we havenât gained much else apart from that - until we check the Inspector. Weâre met with a window which lets us press the plus arrow to add a new object from the Hierarchy or the Project View, then add a public
method - weâll need to make UpdateHealthBar
public
, then we can add it to the list. Even if we remove the AddListener
and RemoveListener
calls above, itâs very easy to add this method as a listener.
You can add as many objects and methods as youâd like to this list, and theyâll all get called when the event fires. Now a programmer can set up an event framework like this, and if an extra event needs to be added or an existing one removed from the list at a later date, a designer or artist working away from the codebase can make the change themselves without needing to call upon a programmer to dive behind the scenes.
Custom event data
Of course, weâve lost the ability to pass custom data through the event. To add this back, we can add up to four type parameters to UnityEvent
to specify what kind of data we want to send.
public UnityEvent<int> healthChangedEvent;
private void OnGetHit(int damage)
{
health -= damage;
healthChangedEvent?.Invoke(health);
}
Then on the subscriber, weâll add back the extra parameter.
private void UpdateHealthBar(int health)
{
healthBarLevel = health;
Debug.Log(health);
}
We can subscribe via code like before, but this isnât quite what we want yet because the event has now disappeared from the Inspector. One of the slightly more annoying parts of this approach is that UnityEvent<int>
, or indeed any UnityEvent<T>
, is no longer serializable, so weâll need one more step to make it show up in the Inspector again. Luckily, all we need to do is create a tiny class that derives the generic UnityEvent<int>
and label it with [System.Serializable]
.
// Outside the publishing class definition.
[System.Serializable]
public class HealthChangedEvent : UnityEvent<int>
{
}
// Inside the publishing class.
public HealthChangedEvent healthChangedEvent;
The HealthChangedEvent
type is essentially just a UnityEvent<int>
, and by labelling it with the System.Serializable
attribute, it should now appear in the Inspector again. This time, when selecting a method from the drop-down, there will be a section at the top with the heading âDynamic intâ, which will list the methods which match the parameter list (in our case, a single int
). When one of those is selected, the method will receive the value passed into the Invoke
method.
The other useful feature of UnityEvent
is that we can also add methods which donât match the parameter list - they will just fire normally without receiving the data passed into Invoke
. You can add up to four parameters by using UnityEvent<int, string>
, or UnityEvent<float, int, bool>
and so on - just use the âextension classâ trick above to make it pop up in the Inspector.
Conclusion
Weâve seen a range of messaging and event systems which can be used in Unity and scenarios where each one might be useful, as well as use cases where they fall down. The backbone of a smoothly running game - and, crucially, an easily maintainable one - is robust communication between the different components, and hopefully with this article youâll be able to level up your gameâs messaging architecture.
Acknowledgements
Supporters
Support me on Patreon or buy me a coffee on Ko-fi for PDF versions of each article and to access certain articles early! Some tiers also get early access to my YouTube videos or even copies of my asset packs!
Special thanks to my Patreon backers!
Gemma Louise Ilett
Jesper Kuutti
BVR Jack Dixon John Selig Pablo Ruiz
Chris Sims FonzoUA Jason Swearingen Moishi Rand Shaun Wall Udons
Anna Voronova Gabriella Pimenta James Poole Christopher Pereira Zachary Alstadt
And a shout-out to my top Ko-fi supporters!
Hung Hoang Takuya Mysterious Anonymous Person