Let’s continue our 3D runner game and work on the game over logic 🙂
This article is also available on Medium.
In the previous episode, we added a UI panel to give the player some info on the current state of the game, and an intro panel so that the game doesn’t start immediately, but only when we click on the “Start” button.
Today, let’s shift gears and work on our game over logic!
This tutorial is available either in video format or in text format — see below 🙂
Triggering the game over
In this tutorial, we’re going to see what we need to do when we hit an obstacle and reach zero healthpoints. Because in this case, in addition to updating the UI, we also need to end the game, meaning that:
- the game should be frozen, just like at the very beginning
- we’ll tell the player their current score and distance
- we’ll let them click on a “Replay” button to directly restart a new game
The first step is easy enough: to trigger the game over, we’ll just go to our
_checkCollisions() method and, when we hit an obstacle, if it decreases the health to 0 or less, we’ll call our
Now, in this method, we can take care of our game over logic.
Designing the game over logic
The first thing we have to do is set our
this.running flag to
false so that the game stops when we hit the last obstacle. Then, we’ll show a specific game over UI and, if the player clicks on the “Replay” button, we’ll reset the game and start a new session.
Freezing the game
Let’s start with our
Creating the game over UI
Now, time to work on our UI!
To begin with, let’s go to our
index.html and add the new panel HTML elements. It’s going to be pretty similar to the intro panel, except that we need to replace the IDs. Also, we’ll add additional row divs in this panel to show the final score and distance:
On the CSS side, we can re-use some of the styles we had before and simply assign them to our new IDs too. For the game over panel score and distance divs, it’s the same thing as the row divs in our info panel – we use a flex display and we add a margin on the values:
Something important though is to remember to initially hide our game over panel. So, just below the style that I share between the two panels, I’ll add one specifically for the game over panel that sets its display to
Now that the elements are ready, we can use our JS to set them up properly when we reach the game over point. So in our
Game instance, we’ll once again use the
document.getElementById() built-in function to get references to the DOM elements. We can store these references in the constructor of our
Game instance along with the others. I’ll add three variables for three elements: the game over panel itself, the game over score display and the game over distance display.
Then, going back to our
_gameOver() function, let’s use those variables: first, we’ll set the text of the score and distance divs with the same variables as during the usual runtime logic; second, we’ll show the game over panel. This just means turning its display style back to
To test this out, let’s change our initial health and set it to 10 so that, as soon as we hit an obstacle, we have a game over. We see that now, our panel suddenly pops on the screen when I hit the obstacle and loose my remaining healthpoints…
Emphasising the moment with a little delay
But this is a bit abrupt: rather than the panel appearing instantly when I hit 0 healthpoints, it would be better to have a little delay (with the game already frozen) and only then show the panel. This way, it will be clear to the player that a special state has been reached – namely the game over – and emphasise this particular moment.
To do this, we’ll use another JS built-in, the
setTimeout() function. This method allows you to wait for a given delay (in milliseconds) before running a function. So, here, instead of just showing the panel like this, I’ll wrap it inside a
setTimeout() of 1 second – so a thousand milliseconds:
Now if I play my game again, you see that when I reach the game over, the screen freezes for a second and only then does the panel appear.
Restarting a new game
Resetting the data
At that point, we’ve successfully made an end screen that sums up your results for this game. But there is nothing that lets you replay, for now.
To have a real replay feature, we need to do two things:
- connect the “Replay” button
onclickproperty to a function that updates the running flag to true and hides the game over panel
- make sure that, when the game is restarted, it is indeed back in its initial state!
Let’s take care of the first item since it’s really quick to do: just like we did for the start button, we’ll assign the
onclick property of the button in our constructor, using the “replay-button” ID to grab the DOM element:
The second item is a bit more complex. We want to re-initialise all the parameters of our
Game instance and return to the initial state, so basically we want to re-do part of our initialisation logic. To better factorise the code and avoid discrepancies, it’s safer to create a single util method that is called both from the constructor (for the first init) and then from the
_gameOver() method (for all subsequent replays).
Let’s create this new function in our
Game class and call it
Most of this function is just an extraction of what we’re currently doing in the constructor. Roughly put, apart from the DOM references,
onclick definitions, and event listeners hooking, we can move everything else down to our new
In the constructor, we replace the data initialisation by a call to the new
Though to avoid null references, we’ll also make the scene and camera instance variables, and then add the
this prefix inside the
_reset() function when we call
We also need to make sure that we reset the info displays and that we move our
this.clock definitions from the
_createGrid() method inside this
Now, we can also call this method in our game over logic:
Ok so – if I run this, what will happen?
Well – we do get a reset of the distance, the score, the health, the X and Z translations and so on, but as you can see we actually have multiple scenes at the same time! With this code, we get two spaceships, two grids and a lot of obstacles and bonuses in the distance.
That’s because, for now, we are essentially re-populating our scene as if it was empty when we click on the “Replay” button, because our
_reset() function doesn’t know that the scene is already filled with 3D objects!
Preparing the 3D scene for the first load or for replay
To fix this issue, we have to pass an additional parameter to our
_reset() function: whether or not this is a reset. When we call it from the constructor, it will be
false (because it’s the initial set up), and then when we call if from the
_gameOver() function, we’ll pass
The whole data assignment part is the same no matter the mode we’re in. It’s only the 3D scene initialisation that is impacted. So let’s just transfer this parameter directly to the
Now, in this function, we need to add an if-statement to separate the first-load logic from the replay one. In the first case, we have to create all the elements and place them, but in the second case, we just need to re-pool the objects and reset the
First, let’s move all of our current code in the
Now, let’s take care of the other block.
We’ll once again use the Three.JS
traverse() method to go through our
this.objectsParent hierarchy. But this time, we want to do something both on the children and on the object itself, so let’s add an
else after our check on the variable type:
else block, we’ll just reset the position of the object, which here is the anchor itself, to zero. Then, in the
if block, we’ll check the type of the object and call the corresponding setup function. This time, we’ll use the default call with zeroed-out reference positions, and just pass in the object to setup:
And tadaa! 🙂
We now have a basic game over routine that is triggered whenever we hit an obstacle and reach zero healthpoints or less, shows a panel with the current traveled distance and score, and allows the players to restart a new game instantly by clicking the “Replay” button.
Next time, we’ll continue improving our game and add something essential: sounds and music!