How to Remake Chrome's Dino Game in PhaserJS: Part II.

This is the second part of the Remake Dino in PhaserJS. If you missed out on the first part, you can reach it here. We've left off the previous part with managing to draw the dinosaur onto the scene and making it run, using the generated animations. If you're looking for the full project in one piece, you can get the code from the GitHub repository. In this tutorial, we are going to cover the following topics:

Table of Contents

  1. Making Dino Move
  2. Setting a State for the Game
    1. Updating the UI
  3. Spawning Stars
  4. Spawning Cactuses
  5. Updating the Score
  6. Summary

Making Dino Move

Let's start by giving dino the ability to jump when we press the spacebar. Create a new method in our Player class called update and add the following:

constructor(scene, x, y) {
    ...
    this.isDead = false;
    return this;
}

update(input) {
    if (!this.isDead && this.sprite.body.onFloor()) {
        this.sprite.play('run', true);
    }

    if ((input.space.isDown && this.sprite.body.onFloor())) {
        this.sprite.setVelocityY(-500);
        this.sprite.play('idle', true);
    }
}
objects/Player.js
Copied to clipboard!

Note that I've defined another property for the class in the constructor called isDead. This will check if the player has died. We only want to allow movements if the player is still alive, so we need to include it in our if checks. This update method does two things:

Make sure you check if the body is on the floor for the jump as well, otherwise you can press jump again mid-air.

So where is our input coming from? We can set it up in the create method of our dino scene:

create() {
    this.player = new Player(this, 25, 460);
    this.inputs = this.input.keyboard.createCursorKeys();
}
dino.js
Copied to clipboard!

This will return a key state object, that we can use in the update method of our Player class. At the end of the previous part, we've made the dinosaur run using the update method of our scene. Replace that code with the following now:

update() {
-   this.player.sprite.play('run', true);
+   this.player.update(this.inputs);
}
dino.diff
Copied to clipboard!

Now you should be able to press space to make the dinosaur jump. There's only one problem. The "Press space to play" text is still showing, so let's fix that by setting up the game state.

The dino jumping in the canvas.

Setting a State for the Game

To keep track of the different states in the game, let's create a new state object in the constructor of our Dino scene:

constructor() {
    super('Dino');

    this.state = {
        started: false
        gameOver: false
    };
}
dino.js
Copied to clipboard!

For now, all we need are two flags to check if the game has been started, or if the game is over. Both of them will be false initially. We can use these flags in the update method to decide if we want to start updating the player or not:

update() {
    if (this.inputs.space.isDown && !this.state.started && !this.state.gameOver) {
        this.state.started = true;
    }

    if (this.state.started) {
        this.player.update(this.inputs);
    }
}
dino.js
Copied to clipboard!

This will ensure that the dinosaur will start in a standing position and will only start running once the game has been started, which we can do by pressing the space bar. But the "Press space to play" is still there, so let's introduce another flag to our state and get rid of it.

this.state = {
    started: false,
    gameOver: false,
    UIUpdated: false
};
dino.js
Copied to clipboard!

Inside our update method, we can check if the UI has already been updated or not. The reason we need this flag is because the update method runs constantly, and we want to avoid keep updating the UI once it has already been updated.

if (this.state.started) {
    this.player.update(this.inputs, delta);

    if (!this.state.UIUpdated) {
        this.updateUI();
    }
}
dino.js
Copied to clipboard!

#Upda

Updating the UI

As you can see, I've outsourced the functionality to a method called updateUI, so let's see what is inside of it:

updateUI() {
    hidePressToPlay();
    hideGameOver();

    showScore();

    this.state.UIUpdated = true;
}
dino.js
Copied to clipboard!

We want a couple of elements to be updated. Namely, we want to hide the "Press space to play" text, and the "Game Over" as well in case the player has died. Then we want to show the score and set the UIUpdated flag to true to avoid keep updating the UI. To make updating the UI elements easier, we can outsource these functionalities as well to separate files. Inside the top of your imports, add the following two lines:

import { showScore } from './ui/score'
import { hidePressToPlay, hideGameOver } from './ui/gameState'
dino.js
Copied to clipboard!

And let's create the appropriate files for them. We will need a ui folder with a score.js and gameState.js. For the scoring mechanism, we can create methods for showing/hiding the score and the highscore:

const score = {
    container: document.querySelector('.score'),
    current: document.querySelector('.current-score'),
    best: document.querySelector('.highscore')
}

export const showScore = () => score.container.classList.remove('hidden');
export const hideScore = () => score.container.classList.add('hidden');
export const showHighScore = () => score.best.classList.remove('hidden');
ui/score.js
Copied to clipboard!

Note that I've created a score object at the top of the file to hold references to DOM elements. This way if anything changes, we only need to update the selector in one place.

As for the gameState, we can take a similar approach; outsource the selectors into one place, and create separate functions for showing/hiding the elements:

const state = {
    pressToPlay: document.querySelector('.press-to-play'),
    gameOver: document.querySelector('.game-over')
}

export const showPressToPlay = () => state.pressToPlay.classList.remove('hidden');
export const hidePressToPlay = () => state.pressToPlay.classList.add('hidden');

export const showGameOver = () => state.gameOver.classList.remove('hidden');
export const hideGameOver = () => state.gameOver.classList.add('hidden');
ui/gameState.js
Copied to clipboard!

Spawning Stars

To make the scene more alive let's also add a couple of stars. For this, we already have them loaded inside of a spritesheet. To keep the logic of spawning stars separated from the rest of the game, let's create a new class for this, just as we did for the Player class:

class Star {
    constructor(scene) {
        const randomSprite = Math.floor(Math.random() * 2);
        const randomXPosition = Math.floor(Math.random() * 620) + 20;
        const randomYPosition = Math.floor(Math.random() * 400) + 20;

        this.sprite = scene.physics.add.staticSprite(randomXPosition, randomYPosition, 'tiles', randomSprite)
            .setScale(2);
    }
}

export default Star;
objects/Star.js
Copied to clipboard!

All we need this time is a constructor, that again, receives the scene and adds a staticSprite at a random x and y position. The reason we want a staticSprite is to make the stars immovable, and unaffected by gravity.

Note that using Math.random, we can also select a random sprite to alternate between the two types of stars we have. Also, to keep the stars in the scene, we want to use smaller numbers for Math.random, than the width and height of the canvas. Let's go back to our dino Scene and add a couple of them inside our create method:

import Star from './objects/Star'

class Dino extends Phaser.Scene {
    constructor() {
        super('Dino');

        this.state = {
            ...
            numberOfStars: 3
        };
    }

    create() {
        this.player = new Player(this, 25, 460);

        for (let index = 0; index < this.state.numberOfStars; index++) {
            new Star(this);
        }

        this.inputs = this.input.keyboard.createCursorKeys();
    }
}
dino.js
Copied to clipboard!

For this, we can introduce another state to define the number of stars as a config. If we take a look at our scene now, every time the game is refreshed, we should have 3 different stars drawn to the canvas at random locations:

Spawning stars into different positions into the canvas

Spawning Cactuses

The main elements are still missing from the scene: cactuses. Just as for the Player or the Star class, it's best to create one for the cactus as well:

class Cactus {
    constructor(scene) {
        this.scene = scene;
        this.sprite = scene.physics.add.sprite(700, 470, 'tiles', 2)
            .setScale(2)
            .setSize(5, 15)
            .setCollideWorldBounds(true);

        this.sprite.body.customBoundsRectangle.left = -100;
        this.sprite.body.customBoundsRectangle.right = 740;
    }
}

export default Cactus;
objects/Cactus.jsNote that the game width is 640, and the cactus is placed at 700
Copied to clipboard!

Unlike the stars, this should not be a staticSprite. We want it to be affected by gravity and collide with both the world bounds and the player. We also want to set a custom size using setSize to make the bounding box better fit the shape of the sprite.

The collision box of a cactus
If you set debug to true the collision box is unnecessarily large by default.

Note that we also want it to be placed outside of the world, outside of the player's view. Because of this, we need to extend the left and right side of the world using a customBoundsRectangle, otherwise, the cactus would collide with the world's bounds. Now to actually spawn the cactuses, we want to go back to our scene and add a couple of new elements to our state, and also extend the update method:

// Don't forget to import the class at the top of your file
import Cactus from './objects/Cactus'

this.state = {
    ...
    cactuses: [],          // An array to hold the cactuses
    cactusDistance: 2000,  // The distance in seconds between two cactuses
    timer: {
        cactusSpawnLoop: 0 // A timer to keep track of the time of last spawn
    }
};

update(time, delta) {
    this.state.timer.cactusSpawnLoop += delta;

    ...

    if (this.state.started) {
        ...

        if (this.state.timer.cactusSpawnLoop > this.state.cactusDistance) {
            this.state.cactusDistance = Phaser.Math.Between(5000, 1000);
            this.state.cactuses.push(new Cactus(this));
            this.state.timer.cactusSpawnLoop = 0;
        }
    }
}
dino.js
Copied to clipboard!

Note that the update method accepts two arguments: the current time, and delta time in milliseconds, which calculates the time since the last frame.

To create frame-rate independent movement, we want to increase our timer loop with the value of delta. If this timer is greater than the distance we've set in the state, we can push a new cactus to state.cactuses. The reason we want to hold cactuses in an array is that we also want to reference all of them at once when we need to stop them from moving when the game is over.

We also need to reset the timer and notice that we can randomly increase/decrease the distance from the next cactus using Phaser.Math.Between. This will give back a random number between 5000 and 1000.

The only thing missing is animating them. We want to move them from the right, all the way to the left. We can achieve this with this one line:

constructor(scene) {
    ...
    this.update();
}

update() {
    this.sprite.setVelocityX(-150);
}
Cactus.js
Copied to clipboard!
Animating cactuses from right to left
We are still missing collision detection between the dinosaur and the cactuses

Updating the Score

To finish off this tutorial, let's also update the score so at least we know how far we've got. To do this, let's add a new property to our state to keep track of the current score:

this.state = {
    ...
    score: 0
};
dino.js
Copied to clipboard!

Then open our Player class, and add a new property there as well, called timer:

class Player {
    constructor(scene, x, y) {
        this.scene = scene;
        this.timer = 0;
        ...
    }
}
Player.js
Copied to clipboard!

Inside the update method of the class, we can increase this timer with delta, just like we did for the cactuses, and update the score based on that:

// Don't forget to import the helper function at the top of your class
import { updateScore } from '../ui/score'

// Don't forget to pass `delta` to the `update` method here, and inside the scene
update(input, delta) {
    this.timer += delta;

    ...

    if (this.timer > 100) {
        this.timer = 0;
        updateScore(this.scene.state);
    }
}
Player.js
Copied to clipboard!

This will call the updateScore method every 100 milliseconds. But we haven't defined this function yet, so let's open score.js and add this new function:

export const updateScore = state => {
    const currentScore = Number(score.current.innerText);

    state.score = currentScore + 1;
    score.current.innerText = (currentScore + 1).toString().padStart(6, '0');
};
ui/score.js
Copied to clipboard!

This function will take the current state as a parameter, and updates both the DOM and the state with +1 as well. Note that we want to always have six digits for the score. This is why we need to use padStart, to prefix the current score with the necessary number of zeroes.

Increasing the score

Summary

And that is all for this tutorial. If you've reached this far, congratulations, you've learned a lot! ๐ŸŽ‰ We've looked into how to take user input, how to set state and update the UI of the game, and how to spawn different game objects.

This tutorial is broken down into three sections. Make sure you continue your journey with part III:

If you missed the first part, you can reach it here.

Do you have experience building platform games in PhaserJS? Let us know your thoughts and opinions about it in the comments below! Thank you for reading through, happy coding! ๐ŸŽฎ

Remove ads
Remove ads

๐Ÿ“š Get access to exclusive content

Want to get access to exclusive content? Support webtips with the price of a coffee to get access to tips, checklists, cheatsheets, and much more. โ˜•

Get access Support us
Remove ads Read more on
Remove ads
Remove ads
๐ŸŽ‰ Thank you for subscribing to our newsletter. x This site uses cookies We use cookies to understand visitors and create a better experience for you. By clicking on "Accept", you accept its use. To find out more, please see our privacy policy.