Dinosaur Game Clone
Fork this repl to begin: https://replit.com/@buckldav/DinoGameP5Starter#index.html. Here’s what we will be doing:
- Adding more enemies.
- Enemies spawn in random positions.
- Enemies speed up as the game goes along.
- Enemies move across the screen.
- If an enemy collides with the player, the game ends.
- Adding game over functionality (i.e. end screen, message to player, etc.).
How it Works
1. Multiple Script Files
In index.html
, we have 5 different script files loaded. We could combine all of these scripts to one file, but it’s a good idea in large projects to organize and compartmentalize code.
<!-- Files are loaded from top to bottom. -->
<script src="globals.js"></script>
<script src="enemy.js"></script>
<script src="environment.js"></script>
<script src="player.js"></script>
<script src="script.js"></script>
2. Globals
Some data is needed throughout the whole program. It’s also convienent to keep similar data together in one spot (i.e. colors) so that if you want to change the color scheme of your entire app or game, you don’t need to comb through files to try to find what you need.
In globals.js
an object called GLOBALS
contains colors and a few positions of other objects.
// The const keyword is used instead of let if the data doesn't change
const GLOBALS = {
GRAVITY: 3,
JUMP_STRENGTH: -30,
DRAG: 3,
GROUND_H: 90,
PLAYER_H: 30,
COLORS: {
SKY: "#87CEEB",
GROUND: "gray",
PLAYER_FILL: "red",
PLAYER_STROKE: "black",
COLLISION_BOX: "pink",
SCORE: "black",
},
}
3. Classes Make Objects
Also in the globals.js
file is a class Box
. Classes are templates for creating objects. Here’s how the Box class does that.
class Box {
constructor(x, y, w, h) {
// Class variables start with 'this.'
this.x = x
this.y = y
this.w = w
this.h = h
}
}
// Create a Box object
// The Box's constructor takes in the parameters
let box = new Box(0, 10, 20, 30)
// box is a Box object
console.log(box)
{
x: 0,
y: 10,
w: 20,
h: 30,
}
console.log(box.y)
10
Boxes keep track of position for the object and its collision in our game.
4. The Other JavaScript Files
File | Description |
---|---|
enemy.js | The Enemy class. Enemies can have different pictures. Enemies also end the game if they collide with the player. |
environment.js | The Environment class. Contains a ground box to prevent the player from falling forever. Add other background elements in addition to the sky if you’d like. |
globals.js | Contains global constants and the functions updateScore() , showCollisionBox() , and gameOver() . |
player.js | The Player class. Update the player’s position and check for collisions. |
script.js | Contains the game logic, which is executed in p5.js’ functions (setup() , draw() , keyPressed() ). |
Make an Enemy Randomly Spawn and Move
To figure out how to move the enemy, perhaps we should reference the player’s update()
function to see how the player moves.
// Player.js
update(env) {
// gravity and jumping
if (this.jumping || !this.isGrounded(env)) {
this.box.y += this.velY
this.velY += GLOBALS.GRAVITY
this.jumping = false
}
}
The key part is this line:
this.box.y += this.velY
The velY
(y velocity, can be +, -, or 0) of the player gets added to the y position of box. We can use this same idea in the Enemy class’s code to update its position and move it across the screen.
Try to add an update()
function to the Enemy class and call it in draw()
in script.js
for each enemy.
Answer
// Enemy.js
class Enemy {
constructor(img) {
this.img = img
this.box = new Box(
40, // x
windowHeight - 250, // y
GLOBALS.ENEMY_W, // w
GLOBALS.ENEMY_H // h
)
// Change the velocity.
this.vel = 1
}
draw() {
// showCollisionBox(this.box)
image(this.img, this.box.x, this.box.y, this.box.w, this.box.h)
}
// Add this function
update() {
// Move the box to the left
this.box.x -= this.vel
}
}
Then, we need to call the update()
function in the main script.js
for each enemy.
// script.js
// in the draw() function's for loop:
for (let i = 0; i < enemies.length; i++) {
enemies[i].draw()
if (player.isBonk(enemies[i])) {
// ends the program
gameOver()
}
// Add this
enemies[i].update()
}
Code at this point: https://replit.com/@buckldav/DinoGameP5EnemyMove.
Spawn the Enemy Off Screen
How could you change the Enemy.js
constructor so that the Enemy spawns off screen to the right?
Answer
Here’s one way to go about it:
// Enemy.js
constructor(img) {
this.img = img
// The x position of the box needs to start off screen (windowWidth + GLOBALS.ENEMY_W).
this.box = new Box(
windowWidth + GLOBALS.ENEMY_W,
windowHeight - 250,
GLOBALS.ENEMY_W,
GLOBALS.ENEMY_H
)
// Try out other numbers for the velocity.
this.vel = 3
}
How would you make the enemy spawn at a random y value? Recall the Snowflakes Project if needed.
Answer
Here’s one way to go about it:
// Enemy.js
constructor(img) {
this.img = img
// Calculate the y with GROUND_Y or PLAYER_Y somehow to make it measure off of where those are.
this.box = new Box(
windowWidth + GLOBALS.ENEMY_W,
windowHeight - random(GLOBALS.GROUND_H, GLOBALS.GROUND_H + 100),
GLOBALS.ENEMY_W,
GLOBALS.ENEMY_H
)
this.vel = 3
}
Spawn More Enemies
Here are some questions that need to be answered.
- How to make a lot of enemies quickly?
- Should we make all the enemies at the beginning or make them along the way?
Answer
For question 2, there is no right answer here, both have merit. The answer to 2 influences the answer to 1. In this walkthrough, I’ll show you the “make them along the way” approach.
In script.js
, check out the setup()
function. Near the bottom, there is the line that spawns an enemy.
// script.js
// array.push adds an element to the array
enemies.push(new Enemy(enemyImg1))
If we use this line in the draw()
function, enemies will spawn every frame.
// script.js
function draw() {
env.draw()
player.draw()
for (let i = 0; i < enemies.length; i++) {
enemies[i].draw()
if (player.isBonk(enemies[i])) {
gameOver()
}
enemies[i].update()
}
// NEW CODE: spawn an enemy
enemies.push(new Enemy(enemyImg1))
updateScore()
player.update(env)
}
This may create too many enemies.
To solve this, let’s add an if statement that will only add an enemy if the current frame is a multiple of something. To do this, we can use the modulo operator which can get the remainder of division. If the remainder is zero, we can spawn an enemy. Also, our score
variable is
// script.js
function draw() {
// ...
// The for loop is right here
// You'll need to make the variable GLOBALS.SPAWN_RATE in globals.js
if (score % GLOBALS.SPAWN_RATE === 0) {
enemies.push(new Enemy(enemyImg1))
}
// updateScore()
// ...
}
// globals.js
const GLOBALS = {
// ...
SPAWN_RATE: 50,
// ...
}
Here’s the code at this point: https://replit.com/@buckldav/DinoGameP5EnemySpawn.
Speed Enemies Up
In Enemy.js
, how could you use the score variable so that the velocity of new enemies increases as the score gets higher?
Answer
Here’s one way to go about it:
// Enemy.js
constructor(img) {
this.img = img
this.box = new Box(
windowWidth + GLOBALS.ENEMY_W,
windowHeight - random(GLOBALS.GROUND_H, GLOBALS.GROUND_H + 100),
GLOBALS.ENEMY_W,
GLOBALS.ENEMY_H
)
// Just this line needs modified. The number "1000" can be whatever you want.
this.vel = 3 + score / 1000
}
You might consider making that number 1000
a constant in your globals.js
. This practice is called avoiding “magic numbers” (numbers that show up out of nowhere in your code). Same thing with the other numbers (250
and 3
). Here’s mine:
// Enemy.js
constructor(img) {
this.img = img
this.box = new Box(
windowWidth + GLOBALS.ENEMY_W,
random(GLOBALS.GROUND_Y - GLOBALS.ENEMY_RANGE, GLOBALS.GROUND_Y),
GLOBALS.ENEMY_W,
GLOBALS.ENEMY_H
)
this.vel = GLOBALS.ENEMY_V + score * GLOBALS.ACCELERATION
}
Game Over Screen
In globals.js
, there is a gameOver()
function where you can add background, text, etc. for when the game ends. Try adding everything yourself!
Final Code
The code below doesn’t have a game over screen, but everything else is there. Try adding your own enemy sprites. Change the jump strength and other globals. Get creative, and good luck!
https://replit.com/@buckldav/DinoGameP5EnemyFinal#enemy.js.