Adding a User Interface to our prototype
Every game needs a user interface to provide information and feedback to your players.
Start by creating a UI text field. This will automatically create a UI canvas as well.
In Unity 2021.2 only the textmesh pro version is available, so this may look different than what you see in a previous version of unity.
We also get a popup regarding textmesh pro. Go ahead and click “Import TMP Essentials” and then close the window.
There are several ways to work with the UI canvas.
I’m going to set my canvas to Screen Space — Camera so I can move the text around like a game object on my screen.
Make sure to assign your Main Camera game object to the Canvas component.
In a previous article, we added the UI sorting layer as the very top layer.
Here we make use of it by selecting our canvas game object and setting the sorting layer to “UI”.
The canvas will now always render in front of anything else in our game.
We do have an issue in the sense that it is inconvenient.
While we could work with our canvas and it render correctly despite it’s massive appearance in the scene view, we are going to bring it down to size.
On the Canvas game object, set the Plane Distance to 10.
Now we are ready to work and as you can see above, dragging around our text game object works as expected in a reasonably sized scene view.
After renaming our text to “Score_text” we set the anchor position to be top right.
This will keep the text in the top right corner if the screen size changes.
Now if you resize the game window, you’ll notice the text stays in the top right corner but doesn’t get bigger or smaller with the player screen.
To fix this, we select our Canvas game object and set the Canvas Scaler component to “Scale with screen size”.
Setting up the UI Manager
We need a way to code changes to our Canvas. Afterall, the score should change, right?
So we’ll make a UI Manager game object via create empty.
Next, we create a UIManager script in our scripts folder and assign to the UI Manager game object.
Tracking the player score
Let’s open our Player script in Visual Studio.
We add the _playerScore variable of integer type to hold our player score value.
Now, let’s open our UIManager script.
The first thing we need to do in here is add a using statement, “using TMPro;”.
This will give us access to the TextMeshPro code library and allow us to store UI elements in code.
We’ll start by add _scoreText to our variables as type “TMP_Text” and we’ll use SerializeField so that we can assign a game object to it.
Drag and drop the Score_text game object onto the UIManager (Script) component in the appropriate field.
Sometimes you gotta roll with the changes
Now since scripts attached to game objects have easier access to parented or childed game objects and their scripts or components, and our UI manager script will be controlling our Canvas child game objects, let’s go ahead and add our UI Manager script to the Canvas game object.
Then delete the UI Manager game object.
We want to set a default score of 0 on our Score text field, so we add that to our start() method.
We also create an UpdatePlayerScore() method that accepts an int value argument of playerScore.
All we want this method to do is update the score text field as shown above.
Since our player score is stored as an integer in the player script and we need to pass that onto our UIManager script on our Canvas game object, we’ll need a way to store the Canvas game object and the UIManager script on our player script.
We do this in the Start() method with a GetComponent assignment.
Also, we must remember our using statement for textmeshpro!
In case we accidentally run the game without the Canvas assigned or something goes wrong, we do a null check and give ourselves a nice easy to identify debug message.
We also add a new method, UpdatePlayerScore() with and int argument for the points we want to add to our player score.
Once _playerScore is updated, we want to display it on our UI so we call the UpdatePlayerScore() method of the UIManagerScript.
I know, not very original method naming. If it seems confusing, feel free to rename one of the methods as you desire.
Drag the Canvas game object onto the Player(Script) component of the Player game object in the appropriate field.
So now we have a pipeline, so to speak, to store our player score in the player script, to update that score from outside the player script, and to update our user interface through our UIManager script when the player score is updated in the player script.
So, how do we determine score?
Defeating enemies of course!
So, let’s open our Enemy script where we detect collisions and kill our enemy game objects using the OnTriggerEnter2D() method.
First, we add the necessary variables and then we assign the script variable in our Start() method based on the _PlayerGO variable we will assign in the Unity editor.
This approach doesn’t work! But why?
Well, simply the enemy game object doesn’t always exist in the game hierarchy. We spawn them on demand, destroy them, etc all the time.
So, we can’t assign the Player game object to the prefab because Unity isn’t sure that they will exist together when the enemy prefab is instantiated.
We made a wrong turn, but progress isn’t a straight line.
We’ll fix it and move on.
So, now what?
Instead of attempting to assign _playerGO in the start() method through an easy drag and drop approach in the Unity editor, we will need to use code to find the Player game object.
We’ll use GameObject.FindGameObjectWithTag to search for our player game object which have tagged as “Player”.
If you get compiler error, make sure you didn’t use “FindGameObjectsWithTag”.
That “s” will make it return an array which we can’t assign to a single game object.
Now when we run our game, we can click on our enemy spawned game object and see that the player game object is properly assigned!
Adding points to the player for enemies destroyed
Since this enemy script is unique to this enemy game object or enemy type, we can tee ourselves up for later by giving it a score value.
Any time this particular enemy game object dies, we’ll use the score value to add that score to the player score.
Game designers will be able to play with this value in Unity for testing purposes as well.
We update our OnTriggerEnter2D() method, specifically when it hits a laser and is destroyed.
With our player game object variable, _playerGO, now working we can simply call the UpdatePlayerScore() method from the player script here and update it.
We pass in the value of this enemy with our _scoreValue variable.
Is our score value updating on screen now?
Sweet, its working!
Let’s add one more thing to our score system.
I want the player to lose score points when they lose a life.
We could reduce the player score from our enemy script using the triggered collision event, but since we already have a lose life method that triggers on collision within the player script, this is the way to go imho.
Its always easier to call on methods from the same script than other scripts and as your projects get more complex, these links between one script to another script can sometimes get broken or become unnecessary.
So why not avoid a future issue if we can.
If we run our game and immediately make the player lose a life, we see a -1000 value score.
On to the next challenge
With our scoring system working and displaying in our UI now, let’s move onto another UI challenge.
Adding a life tracker to the UI
We have some sprites to represent our player lives in the GameDevHQ provided assets folder.
You could also use any sprites you desire if you do not have this.
We have 4 available sprites for our purposes to represent no lives up to 3 lives.
Create an image game object from the UI dropdown on the Canvas game object and rename it Lives_display.
Assign the 3 lives sprite as the “Source Image” since this will be our default starting value.
Also, click the “Preserve Aspect” check box beneath the image source so that our image doesn’t stretch.
Let’s move our Lives_display sprite to the top left corner of the screen and anchor it there.
Make sure to set it’s anchor property to top-left.
Also, lets rename Live_display to Lives_display_img so that the object type is obvious just by looking at the name.
In our UIManager script, let’s add an array of type TMP_Sprite called _livesSprites to store the sprite images that correlate with our player lives value.
We also need to add a using UnityEngine.UI statement and an image variable that will hold the actual displayed sprite of player lives.
Now we assign Lives_display_img to our UI Manager script component.
Next we assign our sprite images to the UIManager Script component and directly into this array we just created.
I want a one-to-one relationship of values, so element 0, or LivesSprites(0), will match our zero lives image for instance.
Add UpdateNumOfLivesDisplay() method
In our UIManager script we add a new public method, UpdateNumOfLivesDisplay so that we can call it from our player script.
In our Player script, we update the LoseLife() method to call on UpdateNumOfLivesDisplay and we pass the current number of player lives as an argument.
Run the game and everything should be working!
That’s it for now, next time we’ll work on a game over screen!