How to Remake Chrome's Dino Game in PhaserJS: Part III.
This is the third, and last part of the Remake Dino in PhaserJS. If you missed out on the first part, you can reach it here, for the second part, you can follow this link. If you're looking for the full project in one piece, you can get the code from the GitHub repository.
We've left off the previous part with managing to spawn cactuses into the way of our dinosaur. In this tutorial, we will finish off the game by covering the remaining topics:
Table of Contents
Increasing the Game Speed
First of all, the game currently runs at a constant speed. There's no real challenge, the game doesn't get harder over time in any way. To fix that, let's introduce speed, just like the original game speeds up over time. For this, we are going to need two other state variables, called speed
, and speedLoop
:
this.state = {
...
speed: 1,
timer: {
speedLoop: 0,
cactusSpawnLoop: 0
}
};
While state.speed
will hold the current speed of the game, state.timer.speedLoop
will keep track of time to increase the speed by a fraction every 10 seconds. To increase it, we need to update the update
method in the following way:
update(time, delta) {
+ this.state.timer.speedLoop += delta;
this.state.timer.cactusSpawnLoop += delta;
if (this.inputs.space.isDown && !this.state.started && !this.state.gameOver) {
this.state.started = true;
}
if (this.state.started) {
this.player.update(this.inputs, delta);
if (!this.state.UIUpdated) {
this.updateUI();
}
if (this.state.timer.cactusSpawnLoop > this.state.cactusDistance) {
this.state.cactusDistance = Phaser.Math.Between(5000 / this.state.speed, 1000 / this.state.speed);
this.state.cactuses.push(new Cactus(this));
this.state.timer.cactusSpawnLoop = 0;
}
+ if (this.state.timer.speedLoop > 10000) {
+ this.state.timer.speedLoop = 0;
+ this.state.speed += .25;
+ }
}
}
Just like we did for the cactusSpawnLoop
, we need to add delta
time to the speedLoop
as well, to keep track of the time passed. Then when the game started, we can check if 10000 milliseconds have passed, and increase the speed by 0.25. On its own, this won't do much, we only have a speed inside the state that is increasing over time. However, we can use this number to increase difficulty. One way to do that is to reduce the distance between each cactus. To do that, modify cactusDistance
to the following:
if (this.state.timer.cactusSpawnLoop > this.state.cactusDistance) {
- this.state.cactusDistance = Phaser.Math.Between(5000, 1000);
+ this.state.cactusDistance = Phaser.Math.Between(5000 / this.state.speed, 1000 / this.state.speed);
this.state.cactuses.push(new Cactus(this));
this.state.timer.cactusSpawnLoop = 0;
}
We divide the time it takes for each cactus to spawn to reduce the distance between them. To further increase difficulty, you could also divide state.cactusDistance
by the speed
inside the if
statement. Of course, with higher difficulty comes a higher reward, so let's go inside the Player
class and update the scoring logic:
update(input, delta) {
this.timer += delta;
- if (this.timer > 100) {
+ if (this.timer > 100 / this.scene.state.speed) {
this.timer = 0;
updateScore(this.scene.state);
}
}
As time passes, the score will be updated at an increasing pace. The longer we survive, the more cactuses we will face, and the faster we will collect the score.
Game Over
Still, there's no price we pay, as we cannot die. So as a next step, let's add the necessary collision. We only need one between the dino and the cactuses. To do that, let's open up our Cactus
class, and add a new collider into the constructor
:
class Cactus {
constructor(scene) {
...
this.collider = scene.physics.add.collider(scene.player.sprite, this.sprite, this.gameOver, null, this);
}
gameOver() {
this.scene.player.die();
}
}
We can add colliders in Phaser by calling physics.add.collider
with a couple of arguments:
- Two objects to test the collision between, this time
player.sprite
and the cactus (this.sprite
). - A collide callback, which executes when the two objects collide. For this, we can create a
gameOver
method to end the game. - A process callback, which is executed when the two objects intersect. Very similar to the collide callback, but this function must return a boolean to indicate whether the two objects are intersecting or not.
- The context of the callback function, which should be
this
.
As for the gameOver
method, we have one single line: calling player.die
. Since we haven't added this function yet, let's go into our Player
class and do that now:
// Make sure you add `showHighScore` to your imports
import { updateScore, showHighScore } from '../ui/score'
die() {
this.isDead = true;
this.sprite.play('idle', true);
this.scene.state.started = false;
this.scene.state.gameOver = true;
showHighScore();
showGameOver();
}
This will stop the dinosaur from playing the running animation and sets all necessary states to stop the game. On its own, this will not stop the incoming fleet of cactuses, however. To do that, let's add a new method to our Cactus
class:
stop() {
this.sprite.setVelocityX(0);
}
This method is straightforward, it will set the velocity of the cactus back to zero. So where do we call this? Inside the update
method of our scene. Remember that we've set state.gameOver
to true
in the die
method of our player? We can use this flag to loop through each cactus we store in the state to stop them:
if (this.state.gameOver) {
this.state.cactuses.forEach(cactus => cactus.stop());
}
Storing the high score
If we run the game now, as soon as we hit a cactus, we should be greeted with a "Game Over". There's only one problem still. The high score remains at zero.
To fix this, let's add highScore
to our state, so we can keep track of it:
this.state = {
...
highScore: 0
};
We will also need a new method inside score.js
, in order to update it. This function will set it to the current score:
export const setHighScore = state => {
state.highScore = state.score;
score.best.innerText = `HI ${state.score.toString().padStart(6, '0')}`;
};
It needs to take the state
as an argument in order to update that as well. Just like we did for the score, we need to use padStart
here again. And to actually call it, we can add the following statement into the die
method of our player:
die() {
this.isDead = true;
this.sprite.play('idle', true);
this.scene.state.started = false;
this.scene.state.gameOver = true;
showHighScore();
showGameOver();
+ if (this.scene.state.score > this.scene.state.highScore) {
+ setHighScore(this.scene.state);
+ }
}
Restarting the Game
There's only one thing missing from the game, and that is resetting it, so we can go another round. For this, let's add a last check to the update
method of our Dino scene:
if (this.inputs.space.isDown && this.state.gameOver) {
this.restartGame();
}
We will be able to restart the game once the gameOver
state is set to true
, and we are pressing the space bar. Let's take a look at what we need to do in the restartGame
method:
restartGame() {
hideGameOver();
resetScore(this.state);
this.state.started = true;
this.state.gameOver = false;
this.state.speed = 1;
this.state.cactuses.forEach(cactus => cactus.sprite.destroy());
this.state.cactuses = [];
this.player.isDead = false;
}
We need to hide the "Game Over" text and reset the score — which we yet to define — as well as reset most of our state variables. Now we can finally make use of the cactuses
array. To free up memory, we can call sprite.destroy
to remove them, and also set the array to a new, empty one.
To further improve performance, we can destroy sprites as soon as they leave the scene. They will never come back, so there's no reason to keep them.
Also, make sure to reset the isDead
state of the player. So to reset the score, we need one last function for score.js
:
export const resetScore = state => {
state.score = 0;
score.current.innerText = '000000';
}
It simply resets the state and sets the UI back to all zeroes. Now we have a working dinosaur game with cactuses, animations, scoring, and collisions.
Conclusion
And with that, you've just finished your very first game in Phaser! 🎉 You've successfully created the base gameplay mechanics of Chrome's Dinosaur! If you've reached this far, congratulations! You've learned how to set up a new Phaser project with Parcel, how to set up animations, and add sprites to the game that are responding to user inputs. We've looked into how we can spawn enemies, and update the UI based on the game state.
If you would like to continue your journey in game development, make sure you check out how you can recreate the base gameplay mechanics of the famous Super Mario Bros. using Phaser:
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! 🎮
Rocket Launch Your Career
Speed up your learning progress with our mentorship program. Join as a mentee to unlock the full potential of Webtips and get a personalized learning experience by experts to master the following frontend technologies: