HOWTO: Control your game UI in Unity with events

Enable greater reuse and system maintainability.

The need to provide a distinct boundary between the user interface and business logic is a common problem that spans applications. I will introduce you to one very simple approach of implementing the Observer pattern in your code through events and delegates.

Intro

There are a lot of examples on the Internet about creating simple demos of platformer games, JRPGs, shooters, etc. Few of them focus on the GUI or the so-called HUD (head-up display) by which information is visually relayed to the player as part of the game’s user interface.

In this article, I am describing one of the many ways to update your UI through events.

You are most probably already using events to connect your UI with your code; you "listen" for event triggers such as OnClick, OnMouseOver, etc. So now we will make it bi-directional and I will show you how to update the UI when something in our game world happens.

The idea is to follow the Observer Pattern. The paradigm can be loosely translated for our specific case to something like this:

Objects deriving from the UI class files have the single responsibility to manage only their own UI components by hooking to the events that are triggered in the game world.

Objects deriving from the world components we create (our domain or "business" logic) do not care about the UI (or anyone else but themselves, those bastards) and will fire events at whoever listens.

Without the benefit of separation between the UI and the rest of the application, modification of either portion may adversely impact the whole. This approach ensures that a crisp boundary exists between objects, enabling greater reuse and system maintainability.

Getting Started

Imagine you already have created your map, player, movement, obstacles, collectables and what not. Now all you must do is to create your UI or HUDDue to the limited volume of the article and scope of the topic, we must jumpstart to that moment in time.

To do so, you must download and import this FREE project from the asset store (or use your own test-projects if you feel a bit more advanced). It has everything you will need.

Getting Started

Create a new Unity 2D project (the same principles apply for 3D games as well).

Download and import the asset from the Unity asset store. You should now have a folder in your Assets named HyperLuminal.

Navigate to the Scenes sub directory.

You now have a choice. Either copy and paste the DayTime_DemoScene to work cleanly, or edit the scene directly.

Scene Setup

First, test your Scene if it runs as it is. It should provide you with a character you can move around and collect blue gems with.

NB! In case you cannot move left and right (but only up and down), do not worry. Move one or two of the collectable_blue gameobjects in front or behind the player character on the map (they are under CollectableHolder gameobject in the scene hierarchy). Just drag and drop them.

Now, let us create our UI. Create a Text UI gameobject at the bottom of the scene hierarchy. This will automatically create a Canvas and an Event System for you.

Next, create a new Camera gameobject right above your Canvas. Name it UI Camera. Set its settings as shown in Fig 1. Do not forget to remove its auido listener! Your main camera already contains one and it will trigger a notice in your console.

Change the Canvas’s Render Mode to Sceen Space – Camera. After that, drag and drop the UI Camera gameobject on the Render Camera field.

Now, let us go back to our Text gameobject. Rename it to Score. Position it at the top of your Canvas to stretch from left to right (hold alt + shift and click the top center box inside the RectTransform). Set its height to 200. Change the default text to "Score: 0" and its color to white. Set the font size to 72 and center alignment both horizontal and vertical. It should look like Fig 3:

If you test the Scene right now, it must display the "Score: 0" in the top-middle of your screen while you play.

After that, create and add a class named Score to the Score Gameobject.

Once you are done, go and create a class named CollectableEvents and add it to all collectable_blue gameobjects in the Scene. If you notice, all collectable_blue gameobjects already have an attached script called CollecatableBounce. Our first stop would be to put our script logic right in that file. However, in order to keep our code separated from the tutorial’s code, we will do it in a new file.

The Scripting

Let us continue with some C# goodness.

The CollectableEvents class

using UnityEngine; public class CollectableEvents : MonoBehaviour { public delegate void OnPickupDelegate(int newVal); public event OnPickupDelegate OnPickup; void OnTriggerEnter2D(Collider2D collider) { if (collider.gameObject.name == "PlayerCharacter") { if(OnPickup != null) OnPickup(5); } } }

We create a delegate method OnPickupDelegate . A delegate is a type that represents references to methods with a particular parameter list and return type. When you instantiate a delegate, you can associate its instance with any method with a compatible signature and return type. You can invoke (or call) the method through the delegate instance.

We also declare an event of the same type and call it OnPickup . This is the method we want to "fire" when the event of collecting a gem happens. To intercept when exactly that happens we must look at how our Scene is set up. In this Scene’s case it’s a generic solution - Unity colliders that trigger events when they interact. OnTriggerEnter2D is the event we can hook to and apply our code. If our collectable collides with a gameobject named PlayerCharacter , then we want to fire our OnPickup event.

Notice how we check if OnPickup is not null, and only then we fire the event with our score pickup. The reason is that events are actually placeholders for a list of delegates to be called whenever the event is triggered. That is why you can use "+=" when you subscribe to an event - you are adding your handler to a list of methods that are going to be called. But if nobody has subscribed yet (i.e. there is nowhere in your code where it says Foo.InitComplete += SomeHandler; ), then the list of delegates to call will be empty, and, therefore, trying to access it will cause a null reference exception.

The Score class

using UnityEngine; using UnityEngine.UI; public class Score : MonoBehaviour { private int _scoreValue = 0; private Text _scoreText { get { return GetComponent<Text>(); } } public GameObject collectables; private void Awake() { SetupListeners(); } void SetupListeners() { foreach (Transform collectable in collectables.transform) { if(collectable.GetComponent<CollectableEvents>() != null) collectable.GetComponent<CollectableEvents>().OnPickup += UpdateScore; } } void UpdateScore(int value) { _scoreValue += value; _scoreText.text = "Score: " + _scoreValue; } }

Our Score class has 3 main properties:

_scoreValue where we will store our current score and increment it in time;

where we will store our current score and increment it in time; _scoreText the UI text component attached to our Score gameobject;

the UI text component attached to our Score gameobject; collectables the parent gameobject of all our collectables

While the first two properties are somewhat self-explanatory, the third one needs some clarification. In our Score script, we want to subscribe and listen to all events that any collectable on the Scene would fire. While this is not the greatest solution performance wise, it will do the trick for the purpose of our example.

The Awake() method is part of our gameobject’s life-cycle flow where we can set up some foundation code for our listeners.

UpdateScore() method receives a value and increments our _scoreValue with it. After that, it updates the UI Text with the new information.

SetupListeners() traverses through each of the collectable gameobjects (an easy way to do this by looping through each existing Transform child in our parent placeholder), and subscribes to its CollectableEvents OnPickup event. Whenever OnPickup "happens" we will call our UpdateScore. With our delegate from earlier, we make sure we pass all necessary parameters.

That’s it! Before testing our result, we have to drag and drop our CollectableHolder gameobject (the parent of all our collectables) to our public field in Score script, like so:

Final Thoughts

Test your Scene and experience the UI updating itself whenever you pick up a collectable.

Again, there are many ways to achieve the same result. However, remember the principle of single responsibility: "A class should have only one reason to change." While it is not always feasible to aim for that, following this rule will make your code a bit cleaner and tidier.