Events are a key part of general C# development. In Unity, they can sometimes be overlooked and less optimal options are used. If you’ve never used events, you’ll be happy to know that they’re easy to get started with and add a lot of value to your project architecture.
Before I cover the Event system in detail, I’d like to go over some of the common alternatives you’ll see in Unity projects.
BroadcastMessage
The BroadcastMessage method is part of the MonoBehaviour class. It allows you to send a loosely coupled message to all active gameobjects.
BroadcastMessage is simple to use and accomplishes the task of sending a message from one gameObject to another.
The biggest issue with BroadcastMessage is how it refers to the method that will be called. Because it takes a string as the first parameter, there is no compile time verification that the method actually exists.
It can be prone to typos, and it introduces danger when refactoring / renaming your methods. If the method name in the string doesn’t match the method name in your classes, it will no-longer work, and there’s no obvious indication of this beyond your game failing to work properly.
The other parameters are also not tightly coupled, so there’s no parameter verification. If your method required a string and an int, but you call BroadcastMessage with two strings, your call will fail, and again there’s no compile time indication of this issue.
Another big drawback to BroadcastMessage is the fact that it only broadcasts to children. For the example given, the UI Text would only receive the message if it’s a child of the player.
Update Polling
Another common technique I see in Unity projects is polling properties in the Update() method.
Polling in an Update() method is fine for many things, but generally not the cleanest way to deal with cross gameobject communication.
using UnityEngine; using UnityEngine.UI; namespace UpdatePolling { public class PlayerHPBar : MonoBehaviour { private Text _text; private Player _player; private void Awake() { _text = GetComponent<Text>(); _player = FindObjectOfType<Player>(); } private void Update() { _text.text = _player.HP.ToString(); } } }
In this example, we update the text of our UI every frame to match the HP value of the player. While this works, it’s not very extensible, it can be a bit confusing, and it requires us to make variables public that may not really need to be.
It also get a lot messier using Update Polling when we want to only do things on a specific situation. For updating the player HP UI, we may not mind doing it every frame, but imagine we want to play a sound effect when the player takes damage too, suddenly this method becomes much more complicated.
Events
If you’ve never coded an event, you’ve probably at least hooked into one before.
One built in Unity event I’ve written about recently is the SceneManager.sceneLoaded event.
This event fires whenever a new scene is loaded.
You can register for the sceneLoaded event and react to it like this.
using UnityEngine; using UnityEngine.SceneManagement; public class SceneLoadedListener : MonoBehaviour { private void Start() { SceneManager.sceneLoaded += HandleSceneLoaded; } private void HandleSceneLoaded(Scene arg0, LoadSceneMode arg1) { string logMessage = string.Format("Scene {0} loaded in mode {1}", arg0, arg1); Debug.Log(logMessage); } }
Each event can have a different signature, meaning the parameters the event will pass to your method can vary.
In the example, we can see that the sceneLoaded event passes two parameters. The parameters for this event are the Scene and the LoadSceneMode.
Creating your own Events
Now, let’s see how we can build our own events and tie them into the example before.
using UnityEngine; namespace UsingEvents { public class Player : MonoBehaviour { public delegate void PlayerTookDamageEvent(int hp); public event PlayerTookDamageEvent OnPlayerTookDamage; public int HP { get; set; } private void Start() { HP = 10; } public void TakeDamage() { HP -= 1; if (OnPlayerTookDamage != null) OnPlayerTookDamage(HP); } } }
In this example, we create a new delegate named PlayerTookDamageEvent which takes a single integer for our HP value.
Then we use the delegate to create an event named OnPlayerTookDamage.
Now, when we take damage, our Player class actually fires our new event so all listeners can deal with it how they like.
We have to check our event for null before calling it. If nothing has registered with our event yet, and we don’t do a null check, we’ll get a null reference exception.
Next, we need to register for this newly created event. To do that, we’ll modify the PlayerHPBar script like this.
using UnityEngine; using UnityEngine.UI; namespace UsingEvents { public class PlayerHPBar : MonoBehaviour { private Text _text; private void Awake() { _text = GetComponent<Text>(); Player player = FindObjectOfType<Player>(); player.OnPlayerTookDamage += HandlePlayerTookDamage; } private void HandlePlayerTookDamage(int hp) { _text.text = hp.ToString(); } } }
To test our event, let’s use this PlayerDamager.cs script.
using UnityEngine; using System.Collections; namespace UsingEvents { public class PlayerDamager : MonoBehaviour { private void Start() { StartCoroutine(DealDamageEvery5Seconds()); } private IEnumerator DealDamageEvery5Seconds() { while (true) { FindObjectOfType<Player>().TakeDamage(); yield return new WaitForSeconds(5f); } } } }
This script calls the TakeDamage() method on the Player every 5 seconds.
TakeDamage() then calls the OnPlayerTookDamage event which causes our PlayerHPBar to update the text.
Let’s see how this looks in action.
We can see here that the players HP is decreasing and the text is updating.
Sidebar – Script Execution Order
You may have noticed something strange though. The first value shown is -1. This caught me off guard the first time, but the cause is visible in the code.
Before you continue reading, take a look and see if you can find it.
….
In our Player.cs script, we set the HP to 10 in the Start() method.
Our PlayerDamager.cs script also starts dealing damage in the Start() method.
Because our script execution order isn’t specified, the PlayerDamager script happens to be running first.
Since an int in c# defaults to a value of Zero, when TakeDamage() is called, the value changes to -1.
Fix #1
There are a few ways we can fix this.
We could change the script execution order so that Player always executes before PlayerDamager.
In the Script Execution Order screen, you can set the order as a number. Lower numbered scripts are run before higher numbered scripts.
Fix #2 – Better
While this would work, there’s a much simpler and cleaner option we can use.
We can change the Player.cs script to set our HP in the Awake() method instead of Start().
Awake() is always called before Start(), so script execution order won’t matter.
Back to Events
So now we have our event working, but we haven’t quite seen a benefit yet.
Let’s add a new requirement for our player. When the player takes damage, let’s play a sound effect that indicates that they were hurt.
PlayerImpactAudio
To do this, we’ll create a new script named PlayerImpactAudio.cs
using UnityEngine; namespace UsingEvents { [RequireComponent(typeof(AudioSource))] public class PlayerImpactAudio : MonoBehaviour { private AudioSource _audioSource; private void Awake() { _audioSource = GetComponent<AudioSource>(); FindObjectOfType<Player>().OnPlayerTookDamage += PlayAudioOnPlayerTookDamage; } private void PlayAudioOnPlayerTookDamage(int hp) { _audioSource.Play(); } } }
Notice on line 13, we register for the same OnPlayerTookDamage event that we used in the PlayerHPBar.cs script.
One of the great things about events is that they allow multiple registrations.
Because of this, we don’t need to change the Player.cs script at all. This means we’re less likely to break something.
If you’re working with others, you’re also less likely to need to do a merge with another developers code.
We’re also able to more closely adhere to the single responsibility principal.
The single responsibility principle states that every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class. All itsservices should be narrowly aligned with that responsibility.
Robert C. Martin expresses the principle as follows:[1] “A class should have only one reason to change.”
The GameObject & AudioSource
You may have also noticed line 5 which tells the editor that this component requires another component to work.
Here, we’re telling it that we need an AudioSource on the gameobject. We do this because line 11 looks for an AudioSource to play our sound effect from.
For this example, we’ve created a new gameobject and attached both our PlayerImpactAudioSource.cs script and an AudioSource.
Then we need to assign an AudioClip. As you can see in the example, I’ve recorded my own sound effect and named it “oww”.
Now when we hit play, a sound effect triggers every time the TakeDamage() method is called.
Actions – Use these!
If this is all new to you, don’t worry, we’re almost done, and it gets easier.
Actions were added to c# with .net 2.0. They’re meant to simplify events by removing some of the ceremony around them.
Let’s take another quick look at how we defined the event in our Player.cs script.
public delegate void PlayerTookDamageEvent(int hp); public event PlayerTookDamageEvent OnPlayerTookDamage;
First, we define the event signature, declaring that our event will pass one integer named “hp”.
Then we declare the event so that other code can register for it.
With Actions, we can cut that down to one line.
using System; using UnityEngine; namespace UsingActions { public class Player : MonoBehaviour { public Action<int> OnPlayerTookDamage; public int HP { get; set; } private void Start() { HP = 10; } public void TakeDamage() { HP -= 1; if (OnPlayerTookDamage != null) OnPlayerTookDamage(HP); } } }
That’s all there is to it. Nothing else needs to change. All the other scripts work exactly the same. We’ve simply reduced the amount of code needed for the same effect.
While this is great for the majority of events, there is one reason you may want to still use the occasional event. That would be when your event has many parameters that can be easily confused with each other. My recommendation for that situation however is to re-think your events and see if the amount of data you’re passing is larger than it needs to be. If you really need to pass a lot of data to an event though, another great option is to create a new class or struct and fill it with your data, then pass that into the event.
Final Tip
Before I go, it’s also worth mentioning that you can have multiple parameters to an Action. To do this, simply comma separate your parameter types like this.
public Action<int, string, MyCustomClass> OnSomethingWithThreeParameters { get; set; }
If you have questions or comments about Events or using Action, please leave a comment or send me an email.