Chapter VIII

From toy to game

Character playing videogame

As the chapter title suggests, it's time to turn our toy into a game.

But before we switch to a new file, let's make one last change so we can continue testing in toy.js. We'll add a debug property to the scene and set it to true in toy.js.

→ scene.js
Copy
constructor ({debug}) {
    // ... omitted for brevity
    this.debug = debug
}
→ toy.js
Copy
const scene = new Scene({debug: true})

Now we'll only show hitboxes if the debug mode is turned on.

→ utils.js
Copy
export function drawHitBox (ctx, scene, element) {
    const {hitBox} = element

    if (scene.debug && hitBox) {
        // ... omitted for brevity
    }
}

We're ready to switch to game.js now! Copy the contents of toy.js into game.js.

You may have noticed that game.js is pretty simple, and that's a good thing. It's best to keep your entry point as simple as possible. In our case, it's only used to set up how the game is launched and how the player interacts with it.

In index.html, change the entry point to game.js.

→ index.html
Copy
<script src="scripts/game.js" type="module"></script>

Game Over

We already know when the player collides with an obstacle. Let's make the game stop when that happens.

First, we'll add a new piece of information to the scene: whether the game is over or not. If it is, we'll stop updating the scene.

→ scene.js
Copy
gameOver () {
    this.ended = true
}

update () {
    if (this.ended) {
        return
    }

    // ...
}

Now we'll call the function to end the game when the player collides with an obstacle.

→ scene.js
Copy
checkCollisions () {
    // ...

    if (collided) {
        this.gameOver()
    }
}

Difficulty

Right now, the camera moves at the same speed throughout the game, which makes it too easy once you get the hang of it.

We'll gradually increase the speed over time to make the game more challenging.

We'll also add a speed limit to prevent the camera from going too fast.

→ scene.js
Copy
constructor () {
    // ...

    this.camera = {
        x:        0,
        y:        0,
        width:    7,
        height:   4,
        speed:    3,
        maxSpeed: 10
    }
}

update (deltaTime) {
    // ...

    const {camera} = this

    camera.x      += camera.speed * deltaTime
    camera.speed  += 0.05 * deltaTime
    camera.speed   = Math.min(camera.speed, camera.maxSpeed)
}

Score

Adding a score is a good way to make the player want to play again.

We'll calculate the score based on the time elapsed and the speed of the camera.

→ scene.js
Copy
get score () {
    return Math.floor(this.elapsedTime * this.camera.speed)
}

To display the score, we're going to use HTML since it's good at displaying text and placing it correctly. As long as the text isn't too stylized and is embedded in the game, it should work well.

In the index.html file, let's add a div element to display the score.

→ index.html
Copy
<div id="fallen_hero">
    <canvas class="game_canvas" width="700" height="400">
    </canvas>
    <div class="game_score">0</div>
</div>

And position it using CSS.

→ style.css
Copy
#fallen_hero .game_score {
    position: absolute;
    right: 1em;
    top: 0.5em;
    font: 3em monospace;
    font-weight: bold;
}

We can now update the display of the score on each frame. Except when the game is over to avoid using resources unnecessarily.

→ game.js
Copy
async function init () {
    const score = document.querySelector('#fallen_hero .game_score')
    // ...

    startAnimationLoop(function (deltaTime) {
        // ...

        if (!scene.ended) {
            score.innerHTML = scene.score
        }
    })
}

Replay screen

When the game is over, it's not very practical to have to refresh the page to start over.

There are several ways to start a new game. One way is to create a reset method on the scene that resets all the elements of the game. Another way is to create a new scene in the game.js file. In our case, creating a new scene is the easiest solution.

But first, let's create a game over screen in HTML and CSS with a button to start a new game.

Game Over Screen
→ index.html
Copy
<div id="fallen_hero">
    <canvas class="game_canvas" width="700" height="400"></canvas>
    <div class="game_score">0</div>
    <div class="game_over">
        <div class="game_over_visual">
            <img src="images/game_over.png" alt="Game Over">
        </div>
        <div class="game_over_replay">
            <img src="images/replay.png" class="game_replay_button" alt="Restart">
        </div>
    </div>
</div>
→ style.css
Copy
#fallen_hero .game_over {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
}

#fallen_hero .game_over .game_over_visual {
    display: flex;
    justify-content: center;
    margin-top: 3em;
}

#fallen_hero .game_over .game_over_visual img {
    width: 200px;
}

#fallen_hero .game_over .game_over_replay {
    display: flex;
    justify-content: center;
    margin-top: 2em;
}

#fallen_hero .game_over .game_over_replay img {
    width: 80px;
    border-radius: 50%;
    padding: 0.5em;
    cursor: pointer;
}

#fallen_hero .game_over .game_over_replay img:hover {
    border-bottom: 0.25em solid black;
}

We now need to give our game.js file a way to know when the game is over. To do this, we're going to use a callback function.

This function will be responsible for displaying the game over screen and allowing the player to start a new game.

We'll refactor our game.js file so that we can easily create a new instance of the scene and pass in the callback function.

→ game.js
Copy
async function init () {
    const score    = document.querySelector('#fallen_hero .game_score')
    const canvas   = document.querySelector('#fallen_hero .game_canvas')
    const gameOver = document.querySelector('#fallen_hero .game_over')
    const replay   = document.querySelector('#fallen_hero .game_replay_button')
    const ctx      = canvas.getContext('2d')

    setScale(ctx, 100)

    const images = await loadImages(imagePaths)

    let scene = createScene()

    function createScene () {
        gameOver.style.display = 'none'

        return new Scene({
            debug: false,
            onGameOver: displayGameOver
        })
    }

    function restartScene () {
        scene = createScene()
    }

    function displayGameOver () {
        gameOver.style.display = 'block'
    }

    replay.addEventListener('click', restartScene)

    // ...
}
  • The createScene function creates a new scene and hides the game over screen.
  • We connect a onGameOver callback that simply displays the game over screen.
  • Finally, we connect the restartScene function to the replay button.
→ scene.js
Copy
constructor ({debug, onGameOver}) {
    // ...
    this.onGameOver = onGameOver
}

gameOver () {
    if (!this.ended) {
        this.ended = true
        this.onGameOver()
    }
}

Well done!

If you've been following along while trying to reproduce the game on your own, don't stop there. There are still lots of things to refine, add, and improve.

You can also freely use the code (tools are meant to be reused) and graphics assets for your own projects if you wish.

Source code
Next: Chapter 9
End