Making a 3D web runner game #9: Handling the game over logic

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 _gameOver() function:

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 this.running flag:

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 none:

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 grid mode:

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 onclick property 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 _reset().

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 _reset() method:

In the constructor, we replace the data initialisation by a call to the new _reset() method.

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 _initializeScene():

We also need to make sure that we reset the info displays and that we move our this.time and this.clock definitions from the _createGrid() method inside this _reset():

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 true.

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 _initializeScene() function:

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 this.objectsParent position.

First, let’s move all of our current code in the !replay block:

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:

In this 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! 🙂

Conclusion

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!

Leave a Reply

Your email address will not be published. Required fields are marked *