Phaser. Unit 7. Dino game clone (part I)

Introduction

In this unit we will build a clone of Google Chrome “secret” Dino game (the original game can be revealed by entering “chrome://dino” in the address bar). We will use simple web technologies such as HTML, CSS, JavaScript and Phaser 3, so that any user can play on any device just using any browser (not only Chrome).

The result is a game which looks almost the same as the original one using less than 10% lines of code. As it can be seen from the Chromium source code, the original game is made of more than 4000 lines of code, whereas the one in this unit has been developed using around 300 lines of code. This means that Phaser could be a much better option than simple JavaScript when thinking about game development.

Some features

This version of the game clones almost every feature of the original one:

  • The speed of the game is increased gradually.
  • Both cactuses and birds will appear randomly to hurt you.
  • Two scores are shown: current and highest.
  • Each time the user reaches 100 points, the score blinks and a sound is played.
  • It can be played on desktop computers using both the mouse and the keyboard:
    • By clicking on the screen to jump or to restart the game.
    • Using the up arrow key or the space bar to jump, the down arrow key to duck under the enemy, and the enter key to restart the game.
  • It can be played on mobile devices by touching the screen, the same way as any single button game.
  • The game is automatically adjusted to fit the screen size, even if it changes after the page is loaded (for example, in case the user rotates the device).
  • Vibration is activated on mobile devices when the game over screen appears.

CSS code (styles.css)

We are going to insert some CSS code to remove any margin and padding, and also to remove the scrollbars, so that the whole window size is used when the game is started:

html, body {
  margin: 0px;
  padding: 0px;
  overflow: hidden;
}

HTML code (index.html)

We are going to use a single HTML file to link all the files in the project. This is going to be the “index.html” file:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="description" content="Dino game clone using Phaser">
  <meta name="keywords" content="HTML, CSS, JavaScript, Phaser">
  <meta name="author" content="Fernando Ruiz Rico">
  
  <title>Dino Phaser</title>

  <link href="styles.css" rel="stylesheet">
</head>

<body>
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/phaser.min.js"></script>
  <script src="preload.js"></script>
  <script src="create.js"></script>
  <script src="update.js"></script>
  <script src="config.js"></script>
</body>

</html>

Constants and Phaser configuration (config.js)

We are going to define some constants (i.e. screen width and height and text styles) so that we can use them inside every JavaScript file. We will also define a function to set the initial values of some variables before the game is started, together with the usual Phaser configuration, which must be performed as usual.

Moreover we are adding a piece of code to check whether the screen is being resized (i.e. when the device is rotated or the window size is adjusted) so that the whole game is reloaded to fit the new values of the screen width and height.

// Fill the whole width and height of the screen
const WIDTH = window.innerWidth;
const HEIGHT = window.innerHeight;

// Time in milliseconds for each obsticle (lower = faster)
const OBSTICLE_TIME = 750;

// Text style to be used when printing texts (scores, etc.)
const TEXT_STYLE = { fill: "#535353", font: '900 35px Courier', resolution: 10 };

// Variables to be initialized each time the game is restarted
function initVariables() {
  this.isGameRunning = false;
  this.gameSpeed = 10;
  this.respawnTime = 0;
  this.score = 0;
}

// Phaser initialization
let game = new Phaser.Game({
  type: Phaser.AUTO,
  width: WIDTH,
  height: HEIGHT,
  pixelArt: true,
  transparent: true,
  physics: {
    default: 'arcade',
    arcade: {
      debug: false
    }
  },
  scene: { preload: preload, create: create, update: update }
});

// Reload the game when the device orientation changes or the user resizes the window
let resizing = false;
window.addEventListener('resize', () => {
  if (resizing) clearTimeout(resizing);
  resizing = setTimeout(() => window.location.reload(), 500);
});

The assets

We will need some images, sprites and sounds. You may use any assets you like, but we are providing the original ones so that you can easily develop the same game as the one available in Google Chrome. Some examples are shown below, and also a zip file containing all the assets can be downloaded here.

Images

Cloud
Cactuses
Game over
Restart

Sprites

Dino waiting and running
Bird enemy

Sounds

Hit
Jump
Reach

Loading all the assets (preload.js)

So that we can use all those images, sprites and sounds, they have to be loaded previously. That is the purpose of the “preload()” function.

Inside the “initSounds()” and “createAnims()” we will set the volume of the sounds and the frames of the animations respectively.

// Load all the audios, images and sprites
function preload() {
  this.load.audio('jump', 'assets/jump.m4a');
  this.load.audio('hit', 'assets/hit.m4a');
  this.load.audio('reach', 'assets/reach.m4a');

  this.load.image('ground', 'assets/ground.png');
  this.load.image('dino-idle', 'assets/dino-idle.png');
  this.load.image('dino-hurt', 'assets/dino-hurt.png');
  this.load.image('restart', 'assets/restart.png');
  this.load.image('game-over', 'assets/game-over.png');
  this.load.image('cloud', 'assets/cloud.png');

  this.load.image('obsticle-1', 'assets/cactuses_small_1.png');
  this.load.image('obsticle-2', 'assets/cactuses_small_2.png');
  this.load.image('obsticle-3', 'assets/cactuses_small_3.png');
  this.load.image('obsticle-4', 'assets/cactuses_big_1.png');
  this.load.image('obsticle-5', 'assets/cactuses_big_2.png');
  this.load.image('obsticle-6', 'assets/cactuses_big_3.png');

  this.load.spritesheet('dino', 'assets/dino-run.png', { frameWidth: 88, frameHeight: 94 });
  this.load.spritesheet('enemy-bird', 'assets/enemy-bird.png', { frameWidth: 92, frameHeight: 77 });
}

// Set the volume of each audio
function initSounds() {
  this.jumpSound = this.sound.add('jump', { volume: 0.75 });
  this.hitSound = this.sound.add('hit', { volume: 0.75 });
  this.reachSound = this.sound.add('reach', { volume: 0.75 });
}

// Define all the animations from the previously loaded spritesheets
function createAnims() {
  this.anims.create({
    key: 'dino-waiting',
    frames: this.anims.generateFrameNumbers('dino', { start: 0, end: 1 }),
    frameRate: 1,
    repeat: -1
  })

  this.anims.create({
    key: 'dino-run',
    frames: this.anims.generateFrameNumbers('dino', { start: 2, end: 3 }),
    frameRate: 10,
    repeat: -1
  })

  this.anims.create({
    key: 'enemy-bird-anim',
    frames: this.anims.generateFrameNumbers('enemy-bird', { start: 0, end: 1 }),
    frameRate: 6,
    repeat: -1
  })
}

Creating the scores, the clouds, the dino, and the game over message (create.js)

  • Scores: Two boxes are used to print both the current and highest scores on the right hand side of the screen.
  • Clouds: A loop will be used to create 3 clouds at once printing the same image on random positions.
  • Dino: The dino will collide with the ground below, and it will jump using the keys defined here (in our case we will use both the up cursor key and the space bar). The mouse functionality is already available by default.
  • Game over: A message will be shown when the user collides with either the birds or the cactuses. After the user clicks on it, this message will be hidden using the “setAlpha(0)” function call.
// Score text on the right side of the screen
function createScore() {
  this.scoreText = this.add.text(WIDTH - 25, HEIGHT / 3, "", TEXT_STYLE).setOrigin(1, 1);
  this.highScoreText = this.add.text(0, HEIGHT / 3, "", TEXT_STYLE).setOrigin(1, 1).setAlpha(0.75);
}

// Three clouds hidden at the beginning
function createClouds() {
  this.clouds = this.add.group();
  for (let i = 0; i < 3; i++) {
    const x = Phaser.Math.Between(0, WIDTH);
    const y = Phaser.Math.Between(HEIGHT / 3, HEIGHT / 2);
    this.clouds.add(this.add.image(x, y, 'cloud'));
  }
  this.clouds.setAlpha(0);
}

// Dino starts running and the the ground is created
function createGround() {
  this.startText.destroy();
  this.dino.setVelocityX(100);
  this.dino.play('dino-run', 1);

  const interval = setInterval(() => {
    this.ground.width += (WIDTH / 100);
    if (this.ground.width >= WIDTH) {
      clearInterval(interval);
      this.dino.setVelocityX(0);
      this.scoreText.setAlpha(1);
      this.clouds.setAlpha(1);
      this.isGameRunning = true;
    }
  }, 50);
}

// Dino waiting animation, keyboard initilization and starting text
function createDino() {
  this.dino = this.physics.add.sprite(0, HEIGHT / 1.5, 'dino-idle').setSize(50).setGravityY(5000).setOrigin(0, 1);
  this.dino.play('dino-waiting', true);

  this.physics.add.collider(this.dino, this.belowGround);
  this.physics.add.collider(this.dino, this.obsticles, () => stopGame.call(this));

  this.cursors = this.input.keyboard.createCursorKeys();
  this.spaceBar = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);

  this.startText = this.add.text(WIDTH - 15, HEIGHT / 1.5, "Press to play", TEXT_STYLE).setOrigin(1);
}

// Game over screen and pause until player starts the game again
function createGameOverScreen() {
  this.gameOverText = this.add.image(0, 0, 'game-over');
  this.restart = this.add.image(0, 50, 'restart').setInteractive();
  this.gameOverScreen = this.add.container(WIDTH / 2, HEIGHT / 2 - 50);
  this.gameOverScreen.add([this.gameOverText, this.restart]);

  this.input.keyboard.on('keydown_ENTER', () => startGame.call(this));
  this.restart.on('pointerdown', () => startGame.call(this));
}

// Define the physical ground and obsticles group
function createWorld() {
  this.ground = this.add.tileSprite(0, HEIGHT / 1.5, 88, 26, 'ground').setOrigin(0, 1);
  this.belowGround = this.add.rectangle(0, HEIGHT / 1.5, WIDTH, HEIGHT / 3).setOrigin(0, 0);
  this.physics.add.existing(this.belowGround);
  this.belowGround.body.setImmovable();
  this.obsticles = this.physics.add.group();
}

// Create all the elements in the game and initilize the score
function create() {
  initVariables.call(this);
  initSounds.call(this);
  createAnims.call(this);
  createWorld.call(this);
  createClouds.call(this);
  createScore.call(this);
  createDino.call(this);

  setInterval(() => updateScore.call(this), 100);
}

Controlling the player and displaying the birds and the cactuses (update.js)

  • Starting the game and building the ground: The “createGround()” method will build the ground progresively, just once, when the game is started the very first time (after the user taps the screen or presses either the up arrow key or the space bar).
  • Restarting the game: After the game over message appears, the user may click on the screen to start a new game. This is done inside the “startGame()” method, which clears the message using the “destroy()” method call, restarts the score and resumes all the physics.
  • Updating the scores: The function “updateScore()” will not only update the current score, but it will also increment the game speed, and it will play a sound each time the user reaches 100 points, making the numbers blink at the same time. The “updateHighScore()” function will keep a record of the highest score each time a game is over.
  • Placing and moving the birds and the cactuses: The “placeObsticle()” function will create either birds or cactuses randomly, and the “update()” function will move everything to the left except the dino, which will always remain in the same position.
// Restart the game after dino is hurt
function startGame() {
  this.dino.setVelocityY(0);
  this.physics.resume();
  this.anims.resumeAll();
  this.obsticles.clear(true, true);
  this.gameOverScreen.destroy();
  this.isGameRunning = true;
}

// On game over a sound is played and all physics are paused
// The game over screen will be shown and mobile devices will vibrate
function stopGame() {
  initVariables.call(this);
  updateHighScore.call(this);
  this.hitSound.play();
  this.physics.pause();
  this.anims.pauseAll();
  this.dino.setTexture('dino-hurt');
  createGameOverScreen.call(this);
  if (window.navigator.vibrate) window.navigator.vibrate(50);
}

// Update the score and increase the game speed
function updateScore() {
  if (!this.isGameRunning) return;

  this.score++;
  this.gameSpeed += 0.01;

  if (this.score % 100 == 0) {
    this.reachSound.play();

    this.tweens.add({
      targets: this.scoreText,
      duration: 100,
      repeat: 3,
      alpha: 0,
      yoyo: true
    })
  }

  this.scoreText.setText(("0000" + this.score).slice(-5));
}

// Update the high score in case is higher than the previous one
function updateHighScore() {
  this.highScoreText.x = this.scoreText.x - this.scoreText.width - 30;

  const highScore = this.highScoreText.text.substr(this.highScoreText.text.length - 5);
  const newScore = Number(this.scoreText.text) > Number(highScore) ? this.scoreText.text : highScore;

  this.highScoreText.setText(`HI ${newScore}`);
}

// Place a new obsticle on the screen (either cactuses or birds)
function placeObsticle() {
  let obsticle;
  const obsticleNum = Phaser.Math.Between(1, 7);

  if (obsticleNum > 6) {
    obsticle = this.obsticles.create(WIDTH, HEIGHT / 1.5 - Phaser.Math.Between(20, 50), 'enemy-bird');
    obsticle.body.height /= 1.5
    obsticle.play('enemy-bird-anim', 1);
  } else {
    obsticle = this.obsticles.create(WIDTH, HEIGHT / 1.5, `obsticle-${obsticleNum}`)
  }

  obsticle.setOrigin(0, 1).setImmovable();
}

// Update the screen (obsticles, ground, clouds and dino) and check the keyboard and mouse (or touch screen) continuously
// Dino will start running and jump if the user taps the screen or presses either the arrow up key or the spacebar  
function update() {
  let jump = this.cursors.up.isDown || this.spaceBar.isDown || this.input.activePointer.isDown;

  if (this.isGameRunning) {
    this.ground.tilePositionX += this.gameSpeed;

    this.respawnTime += this.gameSpeed;
    if (this.respawnTime >= OBSTICLE_TIME) {
      this.respawnTime = 0;
      placeObsticle.call(this);
    }

    this.obsticles.getChildren().forEach(obsticle => {
      obsticle.x -= this.gameSpeed;
      if (obsticle.getBounds().right < 0) {
        obsticle.destroy();
      }
    })

    this.clouds.getChildren().forEach(cloud => {
      cloud.x -= 0.5;
      if (cloud.getBounds().right < 0) {
        cloud.x = WIDTH;
      }
    })

    if (this.dino.body.onFloor()) {
      if (jump) {
        this.jumpSound.play();
        this.dino.anims.stop();
        this.dino.setVelocityY(-1600);
        this.dino.setTexture('dino', 0);
      }
      else {
        this.dino.play('dino-run', true);
      }
    }
  }
  else if (jump && (this.ground.width < WIDTH)) {
    createGround.call(this);
  }
}

Proposed exercise: Using your own assets

Create a new version of the Dino game using your own assets (images, sprites and sounds). You can also change any other things you like.

Do not forget to create all the files required to run the code in this unit:

  • styles.css
  • index.html
  • preload.js
  • create.js
  • update.js
  • config.js
  • assets folder

You may find many free assets in OpenGameArt, and some new versions of the Dino game in github:

Enjoy the game!

You can enjoy playing this wonderful game online here.