Getting Started with Unity (Part 7: Creating an Intuitive User Interface)
Any packaged game includes a number of UI elements — things like a welcome menu, a start button, player lives count, game over screen, and a restart button. Here I’ll cover some of the basics of creating an intuitive user interface that you can use in your own games.
Let’s start by creating a “Score” system to provide performance feedback to the user. First we right click in the Hierarchy to create a new object, then under UI we select Text. This will create our text object and two game objects — the Canvas and the EventSystem. The canvas can be thought of as the backbone or framework for your UI — all UI elements will exist inside of a canvas. The EventSystem allows us to create interactable elements such as buttons that can be coded to do certain behaviors such as loading a scene and starting our game.
Inside our new Text game object we can now change the “New Text” placeholder text to “Score:”, update its color to white, font size to 24, and move the box to the top right corner. Once placed we want to anchor it to that position on screen. An anchor tells the system that as the screen size is increased or decreased, to peg this UI element to a certain position or offset. In our case, since our text object is placed in the top right, it makes sense to anchor it to the top right corner. We can set an offset from the top right corner of -50 on the X and -50 on the Y. Without doing this, your UI element could end up off screen if you scale down your screen size to mobile for example.
We also want the text itself (and the overall canvas) to scale with the screen size. Again, without this our text could end up huge on a smaller mobile screen. We do this on the canvas level. Select the canvas game object and canvas scaler component, then change the UI Scale Mode from Constant Pixel Size to Scale With Screen Size.
Now we have a score text placeholder that will properly scale no matter what screen size we are playing our game on. Another important setting just to note here and not change in this case, is the Canvas Render Mode. It is set to Screen Space — Overlay by default, which, intuitively, means that the canvas UI will always be placed as an overlay on top of your viewable screen area. For any sort of full-screen “always-on” elements or interactable welcome menus this will be the setting you will want to use.
Now let’s arrange for us to be able to interact with the Score text and modify it dynamically as our player progresses through the game. We’ll create a UIManager script for this function and all things related to our UI. Inside the UIManager we create an int variable for score which will be used to update the UI panel and serialize it so we can confirm in the inspector if our value is updating.
Next we create a public function to increment the score whenever we call it:
By putting int scoreToAdd in the parentheses of the function, we can pass in a numeric value from another script. Say for example we had several types of enemies which were worth 10, 20, or 30 points each. When each of those enemies was destroyed we could pass in the appropriate value via the same AddScore() script.
Let’s now head to our enemy script which is where this function will be called. We set up a handle to the UIManager using GameObject.Find:
Finally, in our laser collision script (where our enemy dies) we include an additional line of code to add 10 points to the score:
We can test the game to be sure our serialized score variable is updating when we kill an enemy, and it is:
Now we need to update our Score text to append our score variable. To do this, we need to add the library for UnityEngine.UI to the UIManager script.
This will enable a new variable type of “Text”, which we can serialize. Then in the Inspector, drag and drop our Score object into the Score text element box.
Let’s set an initial value at void Start() so our score reads “Score: 0” when the game first runs:
Note that we can pass in both a string and an int value into the text field. Finally, we create a command to update the score text every time our score variable is updated:
An important note here — it is much more performant to put this score update text command in the AddScore() method than the Update() method. The Update method will be trying updating the score every frame even if it has not changed which is a waste of resources. Instead, inside AddScore() our command only runs when a change has been made to the score.
Let’s see how it works:
Awesome, our user can now keep track of their score which adds a sense of progression to our game. Let’s take care of the flip side — game over! First we need to let the player know how many lives they have.
Graphical Player Lives Display
We have a series of image assets to represent 3, 2, 1, or 0 lives remaining. To implement these into the canvas we need to right click on the Canvas object and select -> UI -> Image. Let’s name this Lives Display. Once created we can drag in our starting asset for 3 lives into the “Source Image” element.
We need to update a few settings here — first is to select “Set Native Size” and “Preserve Aspect” to fix our aspect ratio, and then let’s reduce the scale to 0.6 x 0.6. We’ll want this to be anchored to the top left so let’s drag it into position and set that as the anchor point with an offset of 100 on the X and -50 on the Y.
I think that looks pretty good. Now, to update our lives sprite based on lives we have left, we need to get access to the Image component of our LivesDisplay Game Object and swap the “Source Image” value to the corresponding asset.
The best way to set this up is with an array. An array allows you to store an indexed list of elements in a single container, which allows you to easily swap between elements inside the array. We set up an array by appending  to the variable name:
This serialized Sprite array variable allows us to now store a list of sprite images which we can assign from the inspector.
We can assign the appropriate sprite to each element in this array, and now we’re ready to set up the code to pull the sprites from this array to replace the on-screen display sprite as needed.
In our UIManager we first create a handle to the image component of the LivesDisplay game object using a serialized variable of type Image. Then we can create a new method to update lives:
When we call this method, we will pass in an int value (0,1,2, or 3) representing the number of lives remaining, and we store it as currentLives. Then we set the display sprite for our livesImage object to a value from our lifeSprites array. We have equated our number of lives left to our position in the array (element 0 in array = 0 lives left, element 3 = 3 lives left) so our passed-in currentLives variable can be directly passed along into the lifeSprites array reference.
Next we get a handle to the UIManager from our player script:
And then we can call the UpdateLives() method in our TakeDamage function, and pass in our playerLives variable:
The playerLives variable already exists and is updated every time we take damage. Now we can just pass it in to the UpdateLives() method and it will become the currentLives variable in the UIManager script. Everything should now be connected, let’s test it out!
Our lives graphics are updating correctly and our player disappears when we reach zero lives — everything is working as intended!
One last feature for our in-game UI — let’s create a game over panel when the player dies.
First we create a text element on our canvas and style the text as we would like it. Next we set the player script trigger — when player lives = 0 we will comunicate with the UIManager to set the game over text object to active.
With a little extra code we can set up a coroutine to give the game over text a little retro flair as well:
That’s a wrap! In the next guide I’ll cover loading scenes, creating a main menu, and how we can restart the game after our player dies. See ya there!