Let’s keep working on our 3D runner game and create some UI elements!
This article is also available on Medium.
In the last episode, we saw how to collide with our objects in the level (both obstacles and bonuses) and we prepared a very basic draft of UI to show the current health and score values in the top-left corner of the screen.
But this is not very pretty, nor very readable! So, today, we’re going to improve this UI, add a little info panel that clearly tells the player about the current state of the game, and then create an intro panel with a “Start” button so that the game only really starts when the player clicks it!
This tutorial is available either in video format or in text format — see below 🙂
Designing our info panel
First, let’s focus on our info panel. What I want to have in the end is a simple black area with a light border and a bunch of data inside it: the current traveled distance, the current health (named the “ship’s integrity”) and the current score:
There are two ways to go about this: we could have our
Game class create all the DOM elements on-the-fly, dynamically, so that the
index.html is completely agnostic of the game it is running; or we could define some additional elements in our HTML structure and reference them in our
Game instance to update them later on.
The first option is pretty interesting to make a generic game framework but it’s a bit harder for a first go – so let’s stick with the explicit DOM sketching.
Note: if you’re interested in learning how to create the DOM dynamically, there will be a bonus episode at the end of this series dedicated to transforming our handmade HTML structure into a dynamically generated one! 🙂
Adding the DOM elements in our HTML file
To begin with, let’s create all the DOM elements that we’ll need in our
So let’s open up the
index.html and go back to our “info” container div. We want to reorganise it a bit to get the following info:
- first a little title that says “Captain’s Log”, to make it more pleasing for the player than just a blob of data
- then, a divider that we will style to show as a simple white line in a minute
- then, the score: we want to show it with a label to clearly state what this number is – so we’ll wrap our “score” div into another div; this div will have the ID “score-row” and we’ll write the label in our HTML file as static DOM content
- we’ll do the same for the travelled distance just below
- then we’ll add another divider
- and finally, we want to show the health
For now, we are printing the health as a number, just like the score or the current distance. But this is not very intuitive for the player: we should be able to get at a glance what our current integrity is!
The most readable way to display this kind of data is via an input of “range” type: this creates a slider that can be filled or emptied to properly represent the remaining amount of health points.
All in all, this gives us the following HTML content:
For the health input, let’s also set its minimum and maximum values to 0 and 100, and add a little label above it to tell the player it’s the current ship integrity. Then, to make sure the player can’t modify this input by hand, let’s disable it.
Don’t worry about the colours: we’ll see very soon how to change the CSS to avoid the grey tint! 😉
In the previous episode, we saw that we can access DOM elements using the
document.getElementById() built-in method. So let’s go ahead and make another reference to our distance div that we’ll call
Also, we have to make sure that the content is properly initialised. Indeed, for now, the health and score values are empty until we collide with an objet and modify this value. This is not ideal because it means the layout suddenly changes when we get our first collision!
To fix this, we just have to update the inner content of our divs in the initialisation function with our start values:
Note that because we’ve changed our
divHealth to be an input, we need to update its
value property now.
This means we have to also change our
_checkCollisions() function to make it update this DOM element the right way:
Ok – we’ve taken care of updating the score and the health displays in our
_checkCollisions() method, so the only info we still need to update is the current traveled distance. This is something that will change every frame, so we’ll want to use our
_updateInfoPanel() function to modify it.
To know the current traveled distance, we simply need to use the current position of the objects parent anchor – because remember that our ship isn’t moving, so we can only take this object’s position as reference!
If I save this, you see that I now have a nice info panel in the top-left corner that updates regularly.
But it’s not great to have all those decimal places on the distance value: let’s format our text a bit! We’ll use the
toFixed() method that takes a number and converts it to a string with the given number of decimals. Here, I don’t want any decimals, so I’ll just put 0.
This way, I get a nice integer that is rounded off and that doesn’t take too much space on the screen:
Styling the elements with CSS
Now that the HTML skeleton and the JS logic are in place, let’s style these elements and make something a bit prettier!
First, I want my container to have a black background and a border, plus some padding so that the text is not too close to the border. I’ll also use a monospace font to get more of a “computer/sci-fi” vibe.
Note: you can of course use custom fonts that you place in your project’s folder if you want, but I’ll just stick with the browser default to keep things simple 🙂
Then, let’s take care of our score and distance rows. We want the contents to be inlined, instead of having the value break to the next line. To do this, we can just apply a
display: flex style on both our divs… and we now have inlined labels with their matching values on the right 😉
Of course, we can also style the value divs themselves to add a bit of spacing on the left, using the
Let’s continue with our dividers!
What we want for those divs is to have a simple rectangle that is a few pixels high, that fills the container in width and that has a light colour. We should also make sure that there is a bit of spacing around it so it is not so condensed in the layout: we can add margins on the Y axis and keep the X margins to 0.
While we’re at it, let’s also use a bold style for the title div.
And finally, last but not least, we need to style our input! Styling input ranges can be a bit convoluted because you have to access and modify the style of elements that are not selectable in the DOM: those elements are basically “encompassed” by the input itself and so you need to look on the Internet to find the reference to these pseudo-elements (for example on the MDN docs).
For example, this set of 3 blocks styles my input for the Mozilla Firefox browser:
Here I’m setting the properties of the slider global element (
-moz-range-track), then hiding the handle (
-moz-range-thumb) and picking a blue colour for the part that is currently filled (
However, you also have to remember to style the input for other browsers that are based on the webkit (namely Chrome, Brave, Edge or Safari). The code is a bit more complex but you can find lots of threads on the net that explain how to style a range input for all browsers:
Note: those properties are marked as experimental in the MDN docs, so they might not work for all users, but they’re supported by all the major browsers in their latest versions so I’m going to assume it’s enough for our project!
At that point, we have a nice styled info block in the top-left corner of our screen that shows all the relevant data to the player 🙂
Adding the intro panel
The final thing we want to do in this tutorial is create an intro panel so that, at first, the game is frozen, and that it only starts when we click on the “Start” button.
Let’s start by creating this panel and adding some CSS styles. I’ll just create a big overlay wrapper in my
index.html, with a button inside. I’ll also display the name of our game, “Hyperspeed”, above the button:
Then, in my CSS file, I’ll centre everything in the panel using the
display: grid and
place-items: center properties. Also, I’ll make sure to keep it transparent for now – this is just temporary, to check that my game doesn’t start too soon:
To avoid this large space between my title and my button, I can group them into a single wrapping div. Then, I can also set the vertical alignment and add some styling to the title to create a better layout:
If I save this, you see that I have my “Start” button in the middle of the screen…
… but the problem is that the game has actually started in the background: the ship is already moving and our travelled distance keeps on increasing!
To have the game be frozen initially and only start when we click the button, we’ll need to add a new variable to our
Game instance: a “running” boolean flag (set to
We’ll check this flag in the
update() function: we only want to run the update logic if the game is running. Else, we’ll return immediately to abort, so nothing will happen:
If you save this, you’ll see that when you reload your page, the game is now frozen!
So, then: we want the button to start the game. We’ll once again get a reference to our DOM element using the
document.getElementById() method to get a handle on our button; then, we want to set its
onclick attribute: that’s the function that is run when you click the button. In that callback, we want to do two things – first, we have to set the
this.running variable to
true, second, we have to hide the intro panel:
If you click on the “Start” button, you’ll see that the game now starts running and the intro panel disappears 🙂
To wrap this up, we can finish styling our intro panel and the button.
For the panel, we’ll use a black background to keep this “sci-fi” vibe. I’ll also add a
z-index property to force it to be on top of the rest of the UI and mask everything. For the button, I’ll do something very similar to the info container and make sure that its background colour changes a bit when I hover or press it (so that it’s clear it’s clickable):
If I save this, you see that my panel now covers the entire screen and we only see the start button. Then, when I click it, just like before, the intro panel disappears and the game starts!
Of course, we could still improve this UI in many ways. For example, it would be nice to have the intro panel fade out instead of just disappearing like this…
Note: this basic version is good enough for now, but if you’re interested in implementing this advanced intro panel, the bonus episode on UI at the end of this series will also show you how to make the panel fade out 🙂
In the next episode, we’ll work on the game over part: we’ll see what we need to do if we reach 0 health points and how to offer the player a way to replay instantly…