An Alternative to Events
C# has built-in events and they work fine in Unity projects. End of story, right? Not so fast! Have you ever wondered why the Unity API doesn’t have any C# events in it? Or why Unity made their own UnityEvent
class for UGUI? Maybe there are some valid reasons to avoid C#’s events. Today’s article discusses an alternative with some serious upsides. Read on to learn more!
C# has the event
keyword and it seems to make events trivial. Here’s a little example where the game dispatches an “enemy spawned” event and the sound system listens for it to know when to play a sound:
// Define a class containing all the event arguments class EnemySpawnedEventArgs : EventArgs { public int EnemyId { get; private set; } public Vector3 Location { get; private set; } public EnemySpawnedEventArgs(int enemyId, Vector3 location) { EnemyId = enemyId; Location = location; } } class Game { // Declare an event public event EventHandler<EnemySpawnedEventArgs> OnEnemySpawned; public void SpawnEnemy(int enemyId, Vector3 location) { // The event is is null if there are no listeners if (OnEnemySpawned != null) { // Bundle up the parameters into an object var eventArgs = new EnemySpawnedEventArgs(enemyId, location); // Call all the listeners of the event OnEnemySpawned(eventArgs); } } } class SoundManager { private Game game; public SoundManager(Game game) { this.game = game; // Listen to be notified when an enemy spawns game.OnEnemySpawned += HandleEnemySpawned; } // Called when an enemy spawns private void HandleEnemySpawned(object sender, EnemySpawnedEventArgs eventArgs) { // Use the event arguments Debug.LogFormat("Enemy with ID {0} spawned at {1}", eventArgs.EnemyId, eventArgs.Location); // Stop listening so this function is no longer called when enemies spawn game.OnEnemySpawned -= HandleEnemySpawned; } }
This code is almost purely boilerplate. While the event functionality of starting and stopping listening and dispatching is reusable, we still had to define an EventArgs
, check for null
, listen, and stop listening. All this boilerplate code is the first cost that events impose on us.
Technically we could skip the EventArgs
class and make our event have a type like Action<int, Vector3>
. That too comes at a cost because the code will now be a lot harder to understand. What are these parameters? Since they don’t have names, you’ll have to type a comment, keep it updated, and go read it. When you make your listener functions (e.g. HandleEnemySpawned
) you’ll need to type out all of the arguments. If the number, type, or meaning of the arguments changes, you’ll need to go update all the listener functions. So you have a tradeoff between the usability of EventArgs
and the boilerplate and garbage reduction of Action<int, Vector3>
. A middle-ground is to define your own delegate with named parameters.
Let’s move on from the boilerplate concerns and talk about performance. In this article I showed that normal function calls are about 10x faster than delegates, which are essentially C# events. Then in this article I showed C# events outperforming Unity’s events in both CPU and garbage creation. What I didn’t point out is that creating the delegates to listen to the event with often creates garbage, such as in the case of lambdas or non-static functions. In short, events are 10x slower than normal functions and involve creating garbage to use them.
The next concern with events is with debugging them. Events are basically a list of functions. That list is built at runtime by adding listeners and cleaned up by removing listeners. As such, you can’t look at the code and easily know what functions get called when the event is dispatched because you need to run the code to build the list of functions. That list naturally changes over time, so sometimes functions A and B get called and other times B and C get called. Or maybe no functions get called. To debug, you need to inspect the state of this list in a debugger. The functions in this list probably won’t have useful names since they’re delegate objects, so it’s even harder to tell what’ll get called. It really is a painful debugging experience. This article discusses these issues in more depth and gives some alternatives.
This is also the greatest upside for events. Since you’re dynamically modifying a list of functions that should be called when the event is dispatched, you have a ton of flexibility. There could be no functions to call, one function, or thousands. You can even change the order the listener functions are called in. All of this is at runtime and determined by however complex of code you want to write. C# and Unity events are reusable code that handle all of this listener function list management so you never have to write or maintain any of it.
Those are some of the main “pros” and “cons” of events. Now how about an alternative? In The Problems with Events I showed some simple alternatives for when you just have one listener function. What if you wanted an arbitrary number of listeners? Let’s tackle that problem today.
The main purpose of events is to call zero or more listener functions when we dispatch the event. We don’t want the code that dispatches the event to be tightly coupled to the code that listens for the event, so we need a middle-man. Just like with events, we need the dispatching code to call the middle-man’s function and the middle-man to call the listener functions. We don’t need the middle-man to keep a dynamic list of functions though. That’s just how the C# and Unity event systems happen to work. What if we skipped the list of delegates and just typed out which functions should be called? Let’s try that!
Here’s a version of the above example with the C# event removed and a new EnemySpawnedEvent
added:
// Define a class containing all the event arguments class EnemySpawnedEventArgs : EventArgs { public int EnemyId { get; private set; } public Vector3 Location { get; private set; } public EnemySpawnedEventArgs(int enemyId, Vector3 location) { EnemyId = enemyId; Location = location; } } // Define a class for the event itself class EnemySpawnedEvent { public SoundManager SoundManager; public void Dispatch(object sender, EnemySpawnedEventArgs eventArgs) { if (SoundManager != null) { SoundManager.HandleEnemySpawned(sender, eventArgs); } } } class Game { // Declare an event public EnemySpawnedEvent OnEnemySpawned { get; private set; } public Game() { // Create the event OnEnemySpawned = new EnemySpawnedEvent(); } public void SpawnEnemy(int enemyId, Vector3 location) { // Bundle up the parameters into an object var eventArgs = new EnemySpawnedEventArgs(enemyId, location); // Call all the listeners of the event OnEnemySpawned.Dispatch(this, eventArgs); } } class SoundManager { private Game game; public SoundManager(Game game) { this.game = game; // Listen to be notified when an enemy spawns game.OnEnemySpawned.SoundManager = this; } // Called when an enemy spawns public void HandleEnemySpawned(object sender, EnemySpawnedEventArgs eventArgs) { // Use the event arguments Debug.LogFormat("Enemy with ID {0} spawned at {1}", eventArgs.EnemyId, eventArgs.Location); // Stop listening so this function is no longer called when enemies spawn game.OnEnemySpawned.SoundManager = null; } }
There are only a few differences in this version. First, there’s a new EnemySpawnedEvent
and the Game
has one instead of the C# event. The EnemySpawnedEvent
class has an explicit reference to the SoundManager
rather than the implicit reference that a C# event would have. It allows for the SoundManager
to start and stop listening simply by setting the SoundManager
field to this
or null
. Its Dispatch
function does the null check instead of the Game
. Lastly, HandleEnemySpawned
needs to be public
(or otherwise accessible) so the EnemySpawnedEvent
can call it.
Already the EnemySpawnedEventArgs
is seeming a little silly. We don’t normally package up our function parameters into a class and create garbage to make an instance of it, so why do we do it here? It’s also weird to pass a plain object
“sender” to functions you call. So let’s remove those and see how it looks:
// Define a class for the event itself class EnemySpawnedEvent { public SoundManager SoundManager; public void Dispatch(int enemyId, Vector3 location) { if (SoundManager != null) { SoundManager.HandleEnemySpawned(enemyId, location); } } } class Game { // Declare an event public EnemySpawnedEvent OnEnemySpawned { get; private set; } public Game() { // Create the event OnEnemySpawned = new EnemySpawnedEvent(); } public void SpawnEnemy(int enemyId, Vector3 location) { // Call all the listeners of the event OnEnemySpawned.Dispatch(enemyId, location); } } class SoundManager { private Game game; public SoundManager(Game game) { this.game = game; // Listen to be notified when an enemy spawns game.OnEnemySpawned.SoundManager = this; } // Called when an enemy spawns public void HandleEnemySpawned(int enemyId, Vector3 location) { // Use the event arguments Debug.LogFormat("Enemy with ID {0} spawned at {1}", enemyId, location); // Stop listening so this function is no longer called when enemies spawn game.OnEnemySpawned.SoundManager = null; } }
That was really easy to do. We no longer need to type out the boilerplate EventArgs
or sender
parameters and the code is a lot more natural. Unlike with an Action<int, Vector3>
C# event, the parameters are explicitly named in the Dispatch
function. It’s more like we defined our own delegate
type, except we didn’t have to type out that boilerplate.
So how do we add more listeners? Easy! We just add them to the EnemySpawnedEvent
class. The listeners don’t even need to have the same function signature as each other, which is even more flexible than with normal events. Let’s add a class that tracks game statistics and have it listen to our “enemy spawned” event:
// Define a class for the event itself class EnemySpawnedEvent { public SoundManager SoundManager; public StatsManager StatsManager; public void Dispatch(int enemyId, Vector3 location) { if (SoundManager != null) { SoundManager.HandleEnemySpawned(enemyId, location); } if (StatsManager != null) { StatsManager.HandleEnemySpawned(enemyId); } } } class Game { // Declare an event public EnemySpawnedEvent OnEnemySpawned { get; private set; } public Game() { // Create the event OnEnemySpawned = new EnemySpawnedEvent(); } public void SpawnEnemy(int enemyId, Vector3 location) { // Call all the listeners of the event OnEnemySpawned.Dispatch(enemyId, location); } } class SoundManager { private Game game; public SoundManager(Game game) { this.game = game; // Listen to be notified when an enemy spawns game.OnEnemySpawned.SoundManager = this; } // Called when an enemy spawns public void HandleEnemySpawned(int enemyId, Vector3 location) { // Use the event arguments Debug.LogFormat("Enemy with ID {0} spawned at {1}", enemyId, location); // Stop listening so this function is no longer called when enemies spawn game.OnEnemySpawned.SoundManager = null; } } class StatsManager { private Game game; public StatsManager(Game game) { this.game = game; // Listen to be notified when an enemy spawns game.OnEnemySpawned.StatsManager = this; } // Called when an enemy spawns public void HandleEnemySpawned(int enemyId) { // Use the event arguments Debug.LogFormat("Enemy with ID {0} spawned", enemyId); // Stop listening so this function is no longer called when enemies spawn game.OnEnemySpawned.StatsManager = null; } }
Notice how the critical separation of the dispatcher (Game
) and the listeners (SoundManager
, StatsManager
) is preserved by the middle-man (EnemySpawnedEvent
). We can add and remove listeners without changing either side.
That’s about all there is to this pattern, so let’s look at its “pros” and “cons” compared to C# events. We’ll judge it by the same criteria as before: boilerplate, performance, garbage, debugging, and flexibility.
First up is boilerplate. With C# events you don’t need to type an event class, but you probably do need to type an EventArgs
class. You could skip it, but only with a sizable usability hit. With this alternative system—let’s call them pseudo-events—you need to type the event class but you can skip the EventArgs
class without a usability hit. Both systems have to check for null listeners, it’s just moved to the event class from the dispatch points. I’ll call this category a tie.
Next is performance. Pseudo-events just use normal function calls and are therefore 10x faster. You could opt to use interfaces (e.g. IEnemySpawnedListener
) instead, but you’d take a speed hit to do so. This category is a clear win for pseudo-events.
Now for garbage creation. C# events are a class containing a dynamic list of listener function delegates. You usually create garbage for the delegate, for the list, and for the event itself. With pseudo-events there are no delegates and no list, so that’s zero garbage. You could even make the event class a struct
and have no garbage for that, either. This is another clear win for pseudo-events.
When it comes to debugging, C# events are quite opaque. You’ve got to use a debugger and even then you’ll see a cryptic list of delegates that might not have good names. With pseudo-events you can see named fields referencing your listeners. There is an actual .cs
file with the event source code that you can step into and inspect. Or you can add log statements if you prefer. It’s much more debug-friendly than C# events, so that’s a win for pseudo-events.
Finally there’s flexibility. Both systems allow for the same types of functions as listeners: static, non-static, lambda, or delegate. Both allow for starting and stopping listening at runtime. C# events arguably have two advantages though. First, you can add listeners according to runtime variables. For example, you could make a for
loop to add N
listeners where N
is computed at runtime. Second, you can reorder the list of delegates in C# events based on information you don’t have at compile time. In practice, both of these are pretty unusual. When’s the last time you cared about the order that listeners were called in? Is it even a good idea to write code that relies on that ordering? And when’s the last time you added an arbitrary number of listeners? Usually you just add one, like in the examples. Either of these can be replicated with pseudo-events using a List
or priority values, but you probably won’t need to. On the other hand, C# events force all listener functions to have the same signature. Pseudo-events listeners can have different signatures, as seen in the example. C# events are probably a bit more flexible overall though.
To summarize, here’s a table comparing C# events to pseudo-events:
Category | C# Events | Pseudo-Events |
---|---|---|
Performance | Slow | Fast |
Garbage | Yes | No |
Boilerplate | EventArgs | Event |
Debugging | Hard | Easy |
Flexibility | Great | Good |
What do you think about this alternative to C# and Unity events? Let me know in the comments!
#1 by Valerie on February 21st, 2017 ·
What do I think?
…Love it! Wish I had something more constructive to say, but I’ll have to leave that to other super-smart coders like yourself. I’ll be checking the comments though as I too would love to hear more about these pseudo events. I’m just here to learn and once again you did not disappoint JD.
Thanks!
#2 by jackson on February 21st, 2017 ·
Glad you enjoyed the article. If you give psuedo-events a shot, please leave a comment to let me know how they worked out for you.
#3 by ms on February 22nd, 2017 ·
Hi Jackson,
As always, great article. I learn a lot from each, including this one. My style is heavily influenced by some of the techniques I’ve discovered through many of your past articles. Thank you.
May I play devils advocate? :)
First question/comment: I may be naive but are we not simply leveraging the original intentions of OO message passing? In other words, we’re doing regular ol’ boring method calls so the only relation to ‘events’ is that we’ve labeled and abstracted a class as such, right? Essentially, we’ve bypassed the convenience of builtin constructs for a supposed gain in performance.
Second question based on the data in your article ‘virtual function performance’ — it seems there’s a ~200ms difference between using abstract and concrete classes, so my question is this: is the gain in performance worth the implications of maintenance when the project and system is scaled up?
As stated we commonly only ever have one or two listeners, but my gut (however irrational) would attempt to improve encapsulation and lessen coupling by defining a common interface/base class, thus replacing the hard coded manager references with a List, and also replacing the if statements with a for loop that iterates over the list. Probably overkill.
I imagine this is all context specific and that we must always balance performance against encapsulation/coupling/complexity/readability but for the sake of discussion, I raise these points (however silly they may be).
Thanks again and look forward to future articles.
m
#4 by jackson on February 22nd, 2017 ·
Glad you enjoyed the article. You’re always welcome to play devil’s advocate! :)
Q: Isn’t this just method calls?
A: It’s just method calls, but that doesn’t mean it’s not an event. As the Events Tutorial for C# says, “An event in C# is a way for a class to provide notifications to clients of that class when some interesting thing happens to an object.” That’s true of the pseudo-events in the article. They act as a middle-man that takes one function call and uses it to call many functions.
Q: Are we trading convenience for performance?
A: I judged both systems based on boilerplate, performance, garbage, debugging, and flexibility. Performance and garbage are important, but convenience is taken into account by the boilerplate, debugging, and flexibility categories. On the whole, pseudo-events are at least as convenient as C# events and have much better performance.
Q: Are the benefits of virtual function calls worth the performance hit?
A: That depends on many factors, but not necessarily related to this article. In the pseudo-events example I showed non-virtual function calls, but you could easily make all the functions virtual. If you prefer, feel free to add interfaces or abstract classes. This often isn’t necessary though and I’d encourage you to delay creating those interfaces until you have at least a second class that will implement them.
Q: Should the event class have a
List
of abstract listeners?A: If you do this you’ll basically have a C# event that can only accept objects implementing a particular interface. That will save you from the slowness and garbage creation of delegates and allow you to use a single event class for all your events. It basically requires virtual function calls, mandates a particular event-handling signature, requires garbage for the list, increases complexity, and may lead you to create several versions of it for various numbers of parameters. It’s an interesting middle ground between pseudo-events and C# or Unity events. Let me know how it works out if you give it a shot.
#5 by Valerie on September 18th, 2017 ·
Jackson should I put the Pseudo-event class in a separate file, or in the dispatching class file?
What would you do?
#6 by jackson on September 18th, 2017 ·
I’d put it in its own file, but that’s really mostly a stylistic/aesthetic decision. Both ways will work just as well.
#7 by nightmark on October 20th, 2017 ·
Am I correct in interpreting this as the Mediator pattern instead of the Observer for event handling?
If so I think it would be nice to mention it in the article for people searching for more information on Mediators or for people to look up further information on the subject after reading your article.
#8 by jackson on October 20th, 2017 ·
I can see arguments for calling this either the observer pattern or the mediator pattern. Wikipedia says this about the observer pattern:
One example from the article has
Game
be the subject and it maintains a “list” of observers in itsEnemySpawnedEvent
property. It notifies them of state changes by calling a method ofEnemySpawnedEvent
which in turn calls their their methods.The Wikipedia page for the mediator pattern is less specific, but says this:
In the article, communication between the objects is encapsulated in an
EnemySpawnedEvent
mediator object.Game
no longer communicates directly withSoundManager
, but instead communicates through the mediator.Since the technique shown in this article is a direct replacement for C# events, it seems like you could also describe them as an implementation of either pattern. Just replace
EnemySpawnedEvent
withEventHandler<EnemySpawnedEventArgs>
in the above paragraphs and the descriptions still fit.Perhaps there’s some small semantic difference I’m overlooking and this article actually describes just one pattern or the other. Let me know if you’ve got more precise definitions than the Wikipedia pages. Maybe that’ll clear up the confusion.
#9 by Ross Miller on March 9th, 2018 ·
Hi.
Interesting article.
Why are you worried about garbage so much in a managed language? What kind of garbage? Normally it’s cleaned up by the GC if objects are not longer referenced or specifically marked as invalid. Events are no different.
I would change up this pattern somewhat, and maybe use dependency injection in your Event class so that it can take in any subscribing class.
#10 by jackson on March 9th, 2018 ·
Hi Ross,
Avoiding creating garbage is important because Unity’s garbage collector has a number of issues. When it collects, it collects on the main thread which blocks input handling, rendering, and so forth. It does this collection all at once rather than over generations, which can be quite slow. It also fragments the managed heap, eventually leading to allocation failures that crash the game. So there are tangible wins to reducing the amount of garbage creation in a Unity game and it’s become standard practice for Unity developers to avoid it for the above reasons.
As for using dependency injection, can you elaborate on what that would look like?
#11 by Paiman Roointan on September 1st, 2019 ·
I think I’ve seen an article about Unity changing the GC behavior to avoid doing it all at once, and do it in several frames. Don’t know in which version, but they have done it.
#12 by jackson on September 2nd, 2019 ·
Unity calls that “incremental garbage collection” and as of 2019.2, it’s still “experimental”.
#13 by Adam Hegedus on August 27th, 2018 ·
Good article. For people who are concerned about the boilerplate null checking creates, you can implement the following extension method:
Or if you are on .Net 4.x, you can just simply use null propagation:
#14 by jackson on August 27th, 2018 ·
Good tip. The extension method will introduce an additional function call if the C++ compiler doesn’t inline it and will introduce method initialization overhead from IL2CPP, but the readability improvement may be worth the tradeoff. The .NET 4.x approach has neither drawback and is really the ideal option here.
PS: The comment system stripped your type parameter (<T>) because it looks like HTML, but I put it back in.
#15 by Paiman Roointan on September 1st, 2019 ·
We are using events in our game. Different types of entities have an event subscription code in their parent class, and they subscribe when they are created, and unsubscribe when destroyed.
Couldn’t think of any easy solution to replace them.
We are having performance issues with IL2CPP build for arm64-v8 for Android, while there is no problem in mono builds or even the 32bit IL2CPP build.
#16 by jackson on September 2nd, 2019 ·
There shouldn’t be any sizable performance differences between 32-bit and 64-bit IL2CPP builds, so you might want to file a bug with Unity about that.
#17 by Igor Stojković on December 7th, 2019 ·
There is one downside to this approach. We started using NugetForUnity for sharing code between projects and this pattern would prevent us from splitting code into independent packages. If I wanted Game, SoundManager and StatsManager to be each in its own package I would have to have an EnemySpawnedEvent package as well (and so on for each event) but even that couldn’t work. Game package would have to depend on EnemySpawnedEvent package, but EnemySpawnedEvent would need to depend on all other packages and since every package also accesses EnemySpawnedEvent to set itself that means they need to depend on it as well and we can’t have circular dependencies.
#18 by Thiago on January 25th, 2020 ·
I really like this approach, however is there any way to use lambdas without the performance hit? I mean, part of the appeal of events is to register to them using lambda so I don’t need to create a new class with different logic everytime I want to change something or if I want to reuse the same script for another game.
What about virtual functions? are they worse then using Func or Delegate?
#19 by jackson on January 25th, 2020 ·
The approach described in the article is to avoid delegates. Since lambdas are delegates, they’re not allowed in this approach. It’s definitely a tradeoff.
As for virtual functions, the article mentions this briefly and indirectly by referring to them as interfaces:
In short, they’ll be faster than delegates but slower than non-virtual functions when they can’t be devirtualized by the compiler, which is most of the time.
#20 by Jason Storey on May 23rd, 2021 ·
If you are trying to eek out as much performance as possible, it might be a good idea to create a singleton null object pattern for the Soundmanager and assign it by default.
This way you can completely remove those null checks on the dispatch calls which of course have their own performance implications depending on how the equality operations are implemented
#21 by Daniele Cortesi on October 16th, 2023 ·
I think you’d be hurting performance instead.
For the first point, I’m not sure what the “singleton null object pattern” would look like, but if it involves calling a method of a dummy object it’s slower than null checking.
For the second point, generally a null check will simply be a reference comparison, which is as fast as anything can be.
#22 by Daniele Cortesi on October 16th, 2023 ·
This is interesting but not really an alternative to events, not in a general sense.
That’s because the approach you suggest involves knowing beforehand each and every listener. If you need to add an indeterminate number of listeners at runtime you’d need to add them to a list and you’d need to implement an interface. Or you’d need to keep a separate list for every type.
Am I correct? Do you have a better idea to handle dynamic listeners?
You also introduce a circular dependency between the event class and the listeners, which is generally a code smell. But in this case it’s fine I guess.