Observer Pattern in Unity
Observer pattern in Unity
Today I’m going to cover how to quickly and easily understand and implement the observer pattern in Unity. This pattern has a few alternative names including event-listener and publisher-subscriber.
First, why should this technique be used? Without getting too deep the answer is clean code and SOLID principles. In short, these techniques make your code easier to understand, scalable, reusable, maintainable, and so forth.
The publisher-subscriber pattern is lengthy when described in words, but quite simple when demonstrated with examples in Unity. I would consider it an essential programming pattern to know and memorize. The easiest way to understand and learn the pattern is by looking at the problem it solves.
Observer pattern example
Consider the following example where you have two scripts:
Script #1 (Resource Manager): The Resource Manager script tracks money and gets called whenever money is added or subtracted. Again, it is the source of truth with regard to how much money the player has at any given moment.
Script #2 (UI): The UI script should show on the screen how much money the player has at any given moment.
The Resource Manager script is simple enough. It would have a private variable representing the amount of money available and it would likely have a few public methods to add or subtract money.
So what’s the problem? Well, the UI script needs to update the amount of money available on the screen and the amount of money does not change very often. We also want to avoid tightly coupled code; scripts that reference each other.
Dissecting the example
The first “wrong” approach would be for the Resource Manager script to have a reference to the UI script and to directly call a method in the UI script whenever the amount of money changes. The Resource Manager script should not need to know anything about how the UI scripts works internally; this also gets out of control once additional scripts for things like players, enemies, and additional resources are added.
The second “wrong” approach is for the UI script to constantly call a method in the Resource Manager script every frame in the Update method. This is an improvement, but it’s hugely wasteful because the quantity of money available doesn’t change very often; certainly not 60 to 100 times per second. You can imagine how performance might be impacted by this sort of approach.
The solution is the observer pattern. The UI script should have a reference to the Resource Manager script. The Resource Manager script knows when the amount of money changes; it has methods specifically to add and subtract money, remember? The Resource Manager should notify any other script that wants to know about changes to money. Finally, the Resource Manager script should not need to know anything about the scripts that want information about changes to money from it.
Let’s look at how to achieve this desired outcome. This is outlined below.
- Resource Manager script
- Declare a public event handler
- Raise the event when the amount of money changes
- UI script
- Get a reference to the Resource Manager script
- Subscribe to the event
- Fire a method when the event is raised
Pseudo code examples
Events may or may not pass data to the listener. Raising an event without passing any data is simpler. We’ll look at both techniques starting with the simpler version. Let’s start with some extremely reduced pseudo code just to outline the concept.
Example 1: Raising events without sending data.
Publisher script (Resource Manager)
// required for events using System; // declare the event (EventHandler is a delegate) public event EventHandler OnMoneyChange; // raise the event and notify any subscribers OnMoneyChange?.Invoke( this, EventArgs.empty );
Subscriber script (UI)
// required for events using System; // subscribe to the publisher by providing a callback method Broadcaster.OnMoneyChange += MethodToFireInListener; // define the callback method to fire when the event is raised private void MethodToFireInListener( object sender, System.EventArgs e ) { // do something }
Example 2: Raising events that pass data.
Publisher script (Resource Manager)
// required for events using System; // declare the event and provide a custom data type public event EventHandler<OnMoneyChangeEventArgs> OnEnemyDeath; // define the custom data type to pass via the event public class OnMoneyChangeEventArgs : EventArgs { // define anything you want to pass public int someInt; } // raise the event and notify any subscribers // populate the data to pass to the subscribers OnMoneyChange?.Invoke( this, new OnMoneyChangeEventArgs { someInt = 1234; } );
Subscriber script (UI)
// required for events using System; // subscribe to the publisher by providing a callback method Broadcaster.OnMoneyChange += MethodToFireInListener; // define the callback method to fire when the event is raised private void MethodToFireInListener( object sender, Broadcaster.OnMoneyChangeEventArgs e ) { // do something with the data Debug.Log( “someInt: ” e.someInt ); }
How events work
EventHandler is a ready-made delegate defined in the system namespace. Delegates are a collection of call-back methods. You can define your own, but it adds another layer of complexity and the pre-made one works fine for most use cases. Subscribers or listeners can add and remove call-back methods to the delegate. When the event is raised by the publisher and the delegate is executed all of the call-back methods the delegate references get fired.
As a general disclaimer, what I’m teaching here is not the only way, the best way, the most efficient way, or the definitive 200 IQ PhD level solution; it’s a simple, high level, easy-to-understand explanation to get you quickly solving real world problems. Please see the suggested reading list below for a deeper dive. I teach this way because information overload is a huge problem when it comes to learning computer programming.
Just for completeness I am providing some more complete examples in the off chance the abbreviated examples above are not clear. This could prove helpful for some learners.
Complete Unity code example
Raising events without sending data.
Publisher script (attached to a 3d cube object)
using UnityEngine; using System; public class Publisher : MonoBehaviour { public static Publisher Instance { get; private set; } public event EventHandler OnSpacebarDown; private bool isSmall; private void Awake() { Instance = this; } private void Update() { if ( Input.GetKeyDown( KeyCode.Space ) ) { isSmall = !isSmall; if ( isSmall ) transform.localScale = new Vector3( 0.5f, 0.5f, 0.5f ); else transform.localScale = new Vector3( 1.0f, 1.0f, 1.0f ); OnSpacebarDown?.Invoke( this, EventArgs.Empty ); } } }
Subscriber script (attached to a different 3d cube object)
using UnityEngine; using System; public class Subscriber1 : MonoBehaviour { private bool isRotating = false; private void Start() { Publisher.Instance.OnSpacebarDown += Publisher_OnSpacebarDown; } private void Publisher_OnSpacebarDown( object sender, EventArgs e ) { isRotating = !isRotating; } private void Update() { if ( isRotating ) { transform.Rotate( new Vector3( 25.0f, 0.0f, 0.0f ) * Time.deltaTime ); } } }
Reading list
- Events (C# Programming Guide). Microsoft .NET documentation. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/events/
- Delegates (C# Programming Guide). Microsoft .NET documentation. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/
- Design Principles and Design Patterns. Robert C. Martin. http://docs.google.com/a/cleancoder.com/viewer?a=v&pid=explorer&chrome=true&srcid=0BwhCYaYDn8EgODUxZTJhOWEtMTZlMi00OWRiLTg0ZmEtZWQ5ODRlY2RmNDlk&hl=en
Clinical Pharmacist - Hospital - Full-Time $20K retention bonus Broward County, South Florida
Clinical Pharmacist, Pool 2:30p-11p (50538) BHCS- #50538
Clinical Pharmacist, Pharmacy, Pool Varied Days, BHMC (45376)- #45376
Clinical Pharmacist-Pharmacy $20K Sign-On Bonus BHN FT Varied (50842)- #50842