Canyon Runner Repl.it Tutorial
In this repl.it tutorial, we’ll build a canyon running game with JavaScript. The idea is to pilot a ship through a winding canyon, with the player’s score increasing the longer they can avoid crashing into the sides. The canyon veers and narrows, so it gets tricky!
Building this game will involve coding domain objects (pieces the game has to have in order to be the kind of game it is: in our case, a ship and a canyon) as well as implementation details (for example, how to capture user input from the keyboard). The major steps will be:
- Set up the canvas (that is, our video game screen) and draw our ship
- Write code to allow the player to control the ship
- Draw the canyon the player must navigate
- Set up a loop to repeatedly advance the ship through the canyon and update our canvas drawing
- Write code to detect collisions between the ship and the canyon wall
- End the game loop when the ship crashes and display the player’s score
We’re going to move pretty quickly, so feel free to discuss on repl talk if you have any questions or want to ask for a more step-by-step explanation.
As idle as a painted ship // Upon a painted ocean
First, we’ll want to create a Repl.it project using the HTML, CSS, and JS template, since we’ll have one HTML page, one CSS file, and one JS file for our game logic.
Here’s the setup for our `index.html` and `index.css`:
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Canyon Runner</title>
<link href="index.css" rel="stylesheet" type="text/css">
</head>
<body>
<canvas id="screen"></canvas>
<script src="index.js"></script>
</body>
</html>
index.css
canvas {
background-color: #fee4ca;
border: 1px solid black;
display: block;
margin: 0 auto;
}
And now: our ship! (We’ll also add a Game
class, which is in charge of coordinating all the pieces of our game.) From now on, we’ll be adding code only to the index.js file. Start by adding the following (and try typing it out rather than copying and pasting):
class Ship {
constructor(ctx, canvas) {
this.ctx = ctx;
this.canvas = canvas;
this.front = 230;
this.back = 250;
this.left = 240;
this.center = 250;
this.right = 260;
this.draw(0, 0);
} draw(deltaX, deltaY) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.beginPath();
this.ctx.strokeStyle = "#49b04f";
this.ctx.fillStyle = "#d2ecd2";
this.ctx.lineWidth = 5;
this.ctx.moveTo(this.center + deltaX, this.front + deltaY);
this.ctx.lineTo(this.left + deltaX, this.back + deltaY);
this.ctx.lineTo(this.right + deltaX, this.back + deltaY);
this.ctx.closePath();
this.ctx.stroke();
this.ctx.fill();
}
}class Game {
constructor(canvas) {
this.canvas = canvas;
this.ctx = this.canvas.getContext("2d");
this.canvas.height = 500;
this.canvas.width = 500;
this.ship = new Ship(this.ctx, this.canvas);
} start() {
this.ship.draw(0, 0);
}
}let canvas = document.getElementById("screen");
let game = new Game(canvas);game.start();
Great! We have a ship, but like our poetic quote says, it’s idle — we have no way to move it (yet). Let’s fix that next.
Up, Up, Down, Down, Left, Right, Left, Right
In order to move our ship around, we’ll have to add code that accepts keyboard input (in our case, the four arrow keys) and uses that input to move the ship. Let’s add the below code just after our Ship
class:
let keys = [];
let deltaX = 0;
let deltaY = 0;const DIRECTIONS = Object.freeze({
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
});const handleKeyDown = function(ship, ctx, canvas) {
return function(e) {
keys[e.keyCode] = true; if (keys[DIRECTIONS.LEFT]) { deltaX -= 5; }
if (keys[DIRECTIONS.UP]) { deltaY -= 5; }
if (keys[DIRECTIONS.RIGHT]) { deltaX += 5; }
if (keys[DIRECTIONS.DOWN]) { deltaY += 5; } e.preventDefault(); ship.draw(deltaX, deltaY);
};
};const handleKeyUp = function(e) {
keys[e.keyCode] = false;
};
We’ll also want to update our Game
class’ start()
function to register these event handlers:
start() {
addEventListener("keydown", handleKeyDown(this.ship, this.ctx, this.canvas), false);
addEventListener("keyup", handleKeyUp, false); this.ship.draw(0, 0);
}
If we run our code now, click into the canvas, and use the arrow keys, our ship should move around! Our complete code should now look like this:
class Ship {
constructor(ctx, canvas) {
this.ctx = ctx;
this.canvas = canvas;
this.front = 230;
this.back = 250;
this.left = 240;
this.center = 250;
this.right = 260;
this.draw(0, 0);
} draw(deltaX, deltaY) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.beginPath();
this.ctx.strokeStyle = "#49b04f";
this.ctx.fillStyle = "#d2ecd2";
this.ctx.lineWidth = 5;
this.ctx.moveTo(this.center + deltaX, this.front + deltaY);
this.ctx.lineTo(this.left + deltaX, this.back + deltaY);
this.ctx.lineTo(this.right + deltaX, this.back + deltaY);
this.ctx.closePath();
this.ctx.stroke();
this.ctx.fill();
}
}let keys = [];
let deltaX = 0;
let deltaY = 0;const DIRECTIONS = Object.freeze({
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
});const handleKeyDown = function(ship, ctx, canvas) {
return function(e) {
keys[e.keyCode] = true; if (keys[DIRECTIONS.LEFT]) { deltaX -= 5; }
if (keys[DIRECTIONS.UP]) { deltaY -= 5; }
if (keys[DIRECTIONS.RIGHT]) { deltaX += 5; }
if (keys[DIRECTIONS.DOWN]) { deltaY += 5; } e.preventDefault(); ship.draw(deltaX, deltaY);
};
};const handleKeyUp = function(e) {
keys[e.keyCode] = false;
};class Game {
constructor(canvas) {
this.canvas = canvas;
this.ctx = this.canvas.getContext("2d");
this.canvas.height = 500;
this.canvas.width = 500;
this.ship = new Ship(this.ctx, this.canvas);
} start() {
addEventListener("keydown", handleKeyDown(this.ship, this.ctx, this.canvas), false);
addEventListener("keyup", handleKeyUp, false); this.ship.draw(0, 0);
}
}let canvas = document.getElementById("screen");
let game = new Game(canvas);game.start();
The Canyon
Now we’ve got a moving ship, but nowhere to maneuver around. Let’s add a Canyon
class to the top of our index.js file, above our Ship
class:
class Canyon {
constructor(ctx, canvas) {
this.ctx = ctx;
this.canvas = canvas; this.left = 50;
this.right = 450; this.leftWall = 0;
this.rightWall = this.canvas.width;
this.canyonMap = []; this.initializeMap();
} getVectors() {
// -1 for left, 0 for straight, 1 for right.
let leftDirection = Math.floor(Math.random() * Math.floor(3)) - 1;
let rightDirection = Math.floor(Math.random() * Math.floor(3)) - 1; if (leftDirection !== 1 && this.left <= this.leftWall + 20) {
// Bounce off the left side of the screen.
leftDirection = 1;
} else if (rightDirection !== -1 && this.right >= this.rightWall - 20) {
// Bounce off the right side of the screen.
rightDirection = -1;
} const magnitude = 2.5; return [leftDirection * magnitude, rightDirection * magnitude];
} initializeMap() {
// 500px high canvas, each segment is 10px high.
for (let i = 0; i < 500; i += 10) {
let [leftVector, rightVector] = this.getVectors();
this.canyonMap.push([this.left + leftVector, this.right + rightVector]);
}
} draw() {
for (let i = 0; i < this.canyonMap.length; i++) {
// Left canyon wall.
this.ctx.beginPath();
this.ctx.strokeStyle = "#e58618";
this.ctx.lineWidth = 5;
this.ctx.moveTo(this.canyonMap[i][0], i * 10);
this.ctx.lineTo(this.canyonMap[i][0], i * 10 + 10);
this.ctx.closePath();
this.ctx.stroke(); // Right canyon wall.
this.ctx.beginPath();
this.ctx.strokeStyle = "#e58618";
this.ctx.lineWidth = 5;
this.ctx.moveTo(this.canyonMap[i][1], i * 10);
this.ctx.lineTo(this.canyonMap[i][1], i * 10 + 10);
this.ctx.closePath();
this.ctx.stroke();
}
}
}
There’s a lot going on here, so let’s unpack it a little. The constructor()
function sets up some useful fields, like this.leftWall
and this.rightWall
, that we use later to make sure our canyon walls don’t wander outside the bounds of the canvas. The initializeMap()
function creates the starting canyon walls, relying on getVectors()
to randomly draw them (using our maximum left and right values to make sure we draw inside the canvas). Just like we do for our ship, we include a draw()
method for our canyon so we can display it on the canvas.
Our code should now look like this:
class Canyon {
constructor(ctx, canvas) {
this.ctx = ctx;
this.canvas = canvas; this.left = 50;
this.right = 450; this.leftWall = 0;
this.rightWall = this.canvas.width;
this.canyonMap = []; this.initializeMap();
} getVectors() {
// -1 for left, 0 for straight, 1 for right.
let leftDirection = Math.floor(Math.random() * Math.floor(3)) - 1;
let rightDirection = Math.floor(Math.random() * Math.floor(3)) - 1; if (leftDirection !== 1 && this.left <= this.leftWall + 20) {
// Bounce off the left side of the screen.
leftDirection = 1;
} else if (rightDirection !== -1 && this.right >= this.rightWall - 20) {
// Bounce off the right side of the screen.
rightDirection = -1;
} const magnitude = 2.5; return [leftDirection * magnitude, rightDirection * magnitude];
} initializeMap() {
// 500px high canvas, each segment is 10px high.
for (let i = 0; i < 500; i += 10) {
let [leftVector, rightVector] = this.getVectors();
this.canyonMap.push([this.left + leftVector, this.right + rightVector]);
}
} draw() {
for (let i = 0; i < this.canyonMap.length; i++) {
// Left canyon wall.
this.ctx.beginPath();
this.ctx.strokeStyle = "#e58618";
this.ctx.lineWidth = 5;
this.ctx.moveTo(this.canyonMap[i][0], i * 10);
this.ctx.lineTo(this.canyonMap[i][0], i * 10 + 10);
this.ctx.closePath();
this.ctx.stroke(); // Right canyon wall.
this.ctx.beginPath();
this.ctx.strokeStyle = "#e58618";
this.ctx.lineWidth = 5;
this.ctx.moveTo(this.canyonMap[i][1], i * 10);
this.ctx.lineTo(this.canyonMap[i][1], i * 10 + 10);
this.ctx.closePath();
this.ctx.stroke();
}
}
}class Ship {
constructor(ctx, canvas) {
this.ctx = ctx;
this.canvas = canvas;
this.front = 230;
this.back = 250;
this.left = 240;
this.center = 250;
this.right = 260;
this.draw(0, 0);
} draw(deltaX, deltaY) {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.beginPath();
this.ctx.strokeStyle = "#49b04f";
this.ctx.fillStyle = "#d2ecd2";
this.ctx.lineWidth = 5;
this.ctx.moveTo(this.center + deltaX, this.front + deltaY);
this.ctx.lineTo(this.left + deltaX, this.back + deltaY);
this.ctx.lineTo(this.right + deltaX, this.back + deltaY);
this.ctx.closePath();
this.ctx.stroke();
this.ctx.fill();
}
}let keys = [];
let deltaX = 0;
let deltaY = 0;const DIRECTIONS = Object.freeze({
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
});const handleKeyDown = function(ship, ctx, canvas) {
return function(e) {
keys[e.keyCode] = true; if (keys[DIRECTIONS.LEFT]) { deltaX -= 5; }
if (keys[DIRECTIONS.UP]) { deltaY -= 5; }
if (keys[DIRECTIONS.RIGHT]) { deltaX += 5; }
if (keys[DIRECTIONS.DOWN]) { deltaY += 5; } e.preventDefault(); ship.draw(deltaX, deltaY);
};
};const handleKeyUp = function(e) {
keys[e.keyCode] = false;
};class Game {
constructor(canvas) {
this.canvas = canvas;
this.ctx = this.canvas.getContext("2d");
this.canvas.height = 500;
this.canvas.width = 500;
this.ship = new Ship(this.ctx, this.canvas);
this.canyon = new Canyon(this.ctx, this.canvas);
} start() {
addEventListener("keydown", handleKeyDown(this.ship, this.ctx, this.canvas), false);
addEventListener("keyup", handleKeyUp, false); this.ship.draw(0, 0);
this.canyon.draw(this.ctx);
}
}let canvas = document.getElementById("screen");
let game = new Game(canvas);game.start();
But there’s a bug: our canyon walls now vanish when our ship moves! This is because the this.ctx.clearRect()
call in the ship’s draw()
function clears the canvas before redrawing the ship. Let’s fix that and tie all the pieces of our game together by introducing the main game loop.
Loop!
Just like the “loop” in REPL, we’ll want to set up a repeating cycle so we can respond to user input. Let’s use JavaScript’s setInterval()
function to do that, continuously updating our ship’s position and its flight through the canyon:
class Canyon {
constructor(ctx, canvas) {
this.ctx = ctx;
this.canvas = canvas; this.left = 50;
this.right = 450; this.leftWall = 0;
this.rightWall = this.canvas.width;
this.canyonMap = [];
this.veering = null; this.initializeMap();
} getVectors(direction) {
let leftDirection, rightDirection; // Handle veering.
if (direction) {
leftDirection = direction;
rightDirection = direction;
} else {
// -1 for left, 0 for straight, 1 for right.
leftDirection = Math.floor(Math.random() * Math.floor(3)) - 1;
rightDirection = Math.floor(Math.random() * Math.floor(3)) - 1;
} if (leftDirection !== 1 && this.left <= this.leftWall + 20) {
// Bounce off the left side of the screen.
leftDirection = 1;
} else if (rightDirection !== -1 && this.right >= this.rightWall - 20) {
// Bounce off the right side of the screen.
rightDirection = -1;
} const magnitude = 2.5; return [leftDirection * magnitude, rightDirection * magnitude];
} initializeMap() {
// 500px high canvas, each segment is 10px high.
for (let i = 0; i < 500; i += 10) {
let [leftVector, rightVector] = this.getVectors();
this.canyonMap.push([this.left + leftVector, this.right + rightVector]);
}
} updateMap() {
let direction = this.veering && this.veering.direction;
let [leftVector, rightVector] = this.getVectors(direction); this.canyonMap.pop();
this.canyonMap.unshift([this.left + leftVector, this.right + rightVector]); this.left += leftVector;
this.right += rightVector; // 1% chance of veering.
if (Math.random() <= 0.01) {
this.veering = {
// 50/50 chance of veering left or right.
direction: Math.random() <= 0.5 ? -1 : 1,
duration: 20,
};
} if (this.veering) {
if (this.veering.duration === 0) {
this.veering = null;
return;
} this.veering.duration--;
}
} draw() {
this.updateMap(); for (let i = 0; i < this.canyonMap.length; i++) {
// Left canyon wall.
this.ctx.beginPath();
this.ctx.strokeStyle = "#e58618";
this.ctx.lineWidth = 5;
this.ctx.moveTo(this.canyonMap[i][0], i * 10);
this.ctx.lineTo(this.canyonMap[i][0], i * 10 + 10);
this.ctx.closePath();
this.ctx.stroke(); // Right canyon wall.
this.ctx.beginPath();
this.ctx.strokeStyle = "#e58618";
this.ctx.lineWidth = 5;
this.ctx.moveTo(this.canyonMap[i][1], i * 10);
this.ctx.lineTo(this.canyonMap[i][1], i * 10 + 10);
this.ctx.closePath();
this.ctx.stroke();
}
}
}class Ship {
constructor(ctx, canvas) {
this.ctx = ctx;
this.canvas = canvas;
this.front = 230;
this.back = 250;
this.left = 240;
this.center = 250;
this.right = 260;
this.draw(0, 0);
} draw(deltaX, deltaY) {
this.ctx.beginPath();
this.ctx.strokeStyle = "#49b04f";
this.ctx.fillStyle = "#d2ecd2";
this.ctx.lineWidth = 5;
this.ctx.moveTo(this.center + deltaX, this.front + deltaY);
this.ctx.lineTo(this.left + deltaX, this.back + deltaY);
this.ctx.lineTo(this.right + deltaX, this.back + deltaY);
this.ctx.closePath();
this.ctx.stroke();
this.ctx.fill();
}
}let keys = [];
let deltaX = 0;
let deltaY = 0;let intervalId;const DIRECTIONS = Object.freeze({
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
});const handleKeyDown = function(ship, ctx, canvas) {
return function(e) {
keys[e.keyCode] = true; if (keys[DIRECTIONS.LEFT]) { deltaX -= 5; }
if (keys[DIRECTIONS.UP]) { deltaY -= 5; }
if (keys[DIRECTIONS.RIGHT]) { deltaX += 5; }
if (keys[DIRECTIONS.DOWN]) { deltaY += 5; } e.preventDefault(); ship.draw(deltaX, deltaY);
};
};const handleKeyUp = function(e) {
keys[e.keyCode] = false;
};class Game {
constructor(canvas) {
this.canvas = canvas;
this.ctx = this.canvas.getContext("2d");
this.canvas.height = 500;
this.canvas.width = 500;
this.ship = new Ship(this.ctx, this.canvas);
this.canyon = new Canyon(this.ctx, this.canvas);
this.score = 0;
} start() {
// Register event listeners.
addEventListener("keydown", handleKeyDown(this.ship, this.ctx, this.canvas), false);
addEventListener("keyup", handleKeyUp, false);
// Run the main game loop.
this.loop();
} displayScore() {
alert(`Score: ${this.score}`);
} loop() {
intervalId = setInterval(() => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ship.draw(deltaX, deltaY);
this.canyon.draw(this.ctx); }, 10);
}
}let canvas = document.getElementById("screen");
let game = new Game(canvas);game.start();
Note that we’ve added let intervalId;
just after let deltaY = 0;
. We want to keep a reference to the interval ID so we can cancel it when the game ends. Check out the MDN documentation for setInterval()
linked above; the MDN docs are great for improving you understanding of JavaScript.
Collision Detection
We’re nearly done! Now we’ll want to write some code to make sure we end the game when the ship crashes (that is, when any part of the ship touches the canyon walls). Let’s go ahead and update our loop()
function, like so:
loop() {
intervalId = setInterval(() => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ship.draw(deltaX, deltaY);
this.canyon.draw(this.ctx); for (let i = 0; i < this.canyon.canyonMap.length; i++) {
// If the ship is even with this part of the wall...
if (this.ship.back + deltaY === i * 10) {
// ...AND the left side of the ship is over the left wall
// OR the right side of the ship is over the right wall...
if (this.ship.left + deltaX <= this.canyon.canyonMap[i][0] || this.ship.right + deltaX >= this.canyon.canyonMap[i][1]) {
// Game over!
this.end();
return;
}
}
} this.score++;
}, 10);
}
Conclusion
Finally, we’ll add scorekeeping logic to our game, completing our code:
class Canyon {
constructor(ctx, canvas) {
this.ctx = ctx;
this.canvas = canvas; this.left = 50;
this.right = 450; this.leftWall = 0;
this.rightWall = this.canvas.width;
this.canyonMap = [];
this.veering = null; this.initializeMap();
} getVectors(direction) {
let leftDirection, rightDirection; // Handle veering.
if (direction) {
leftDirection = direction;
rightDirection = direction;
} else {
// -1 for left, 0 for straight, 1 for right.
leftDirection = Math.floor(Math.random() * Math.floor(3)) - 1;
rightDirection = Math.floor(Math.random() * Math.floor(3)) - 1;
} if (leftDirection !== 1 && this.left <= this.leftWall + 20) {
// Bounce off the left side of the screen.
leftDirection = 1;
} else if (rightDirection !== -1 && this.right >= this.rightWall - 20) {
// Bounce off the right side of the screen.
rightDirection = -1;
} const magnitude = 2.5; return [leftDirection * magnitude, rightDirection * magnitude];
} initializeMap() {
// 500px high canvas, each segment is 10px high.
for (let i = 0; i < 500; i += 10) {
let [leftVector, rightVector] = this.getVectors();
this.canyonMap.push([this.left + leftVector, this.right + rightVector]);
}
} updateMap() {
let direction = this.veering && this.veering.direction;
let [leftVector, rightVector] = this.getVectors(direction); this.canyonMap.pop();
this.canyonMap.unshift([this.left + leftVector, this.right + rightVector]); this.left += leftVector;
this.right += rightVector; // 1% chance of veering.
if (Math.random() <= 0.01) {
this.veering = {
// 50/50 chance of veering left or right.
direction: Math.random() <= 0.5 ? -1 : 1,
duration: 20,
};
} if (this.veering) {
if (this.veering.duration === 0) {
this.veering = null;
return;
} this.veering.duration--;
}
} draw() {
this.updateMap(); for (let i = 0; i < this.canyonMap.length; i++) {
// Left canyon wall.
this.ctx.beginPath();
this.ctx.strokeStyle = "#e58618";
this.ctx.lineWidth = 5;
this.ctx.moveTo(this.canyonMap[i][0], i * 10);
this.ctx.lineTo(this.canyonMap[i][0], i * 10 + 10);
this.ctx.closePath();
this.ctx.stroke(); // Right canyon wall.
this.ctx.beginPath();
this.ctx.strokeStyle = "#e58618";
this.ctx.lineWidth = 5;
this.ctx.moveTo(this.canyonMap[i][1], i * 10);
this.ctx.lineTo(this.canyonMap[i][1], i * 10 + 10);
this.ctx.closePath();
this.ctx.stroke();
}
}
}class Ship {
constructor(ctx, canvas) {
this.ctx = ctx;
this.canvas = canvas;
this.front = 230;
this.back = 250;
this.left = 240;
this.center = 250;
this.right = 260;
this.draw(0, 0);
} draw(deltaX, deltaY) {
this.ctx.beginPath();
this.ctx.strokeStyle = "#49b04f";
this.ctx.fillStyle = "#d2ecd2";
this.ctx.lineWidth = 5;
this.ctx.moveTo(this.center + deltaX, this.front + deltaY);
this.ctx.lineTo(this.left + deltaX, this.back + deltaY);
this.ctx.lineTo(this.right + deltaX, this.back + deltaY);
this.ctx.closePath();
this.ctx.stroke();
this.ctx.fill();
}
}let keys = [];
let deltaX = 0;
let deltaY = 0;let intervalId;const DIRECTIONS = Object.freeze({
LEFT: 37,
UP: 38,
RIGHT: 39,
DOWN: 40,
});const handleKeyDown = function(ship, ctx, canvas) {
return function(e) {
keys[e.keyCode] = true; if (keys[DIRECTIONS.LEFT]) { deltaX -= 5; }
if (keys[DIRECTIONS.UP]) { deltaY -= 5; }
if (keys[DIRECTIONS.RIGHT]) { deltaX += 5; }
if (keys[DIRECTIONS.DOWN]) { deltaY += 5; } e.preventDefault(); ship.draw(deltaX, deltaY);
};
};const handleKeyUp = function(e) {
keys[e.keyCode] = false;
};class Game {
constructor(canvas) {
this.canvas = canvas;
this.ctx = this.canvas.getContext("2d");
this.canvas.height = 500;
this.canvas.width = 500;
this.ship = new Ship(this.ctx, this.canvas);
this.canyon = new Canyon(this.ctx, this.canvas);
this.score = 0;
} start() {
// Register event listeners.
addEventListener("keydown", handleKeyDown(this.ship, this.ctx, this.canvas), false);
addEventListener("keyup", handleKeyUp, false);
// Run the main game loop.
this.loop();
} displayScore() {
alert(`Score: ${this.score}`);
} loop() {
intervalId = setInterval(() => {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ship.draw(deltaX, deltaY);
this.canyon.draw(this.ctx); for (let i = 0; i < this.canyon.canyonMap.length; i++) {
// If the ship is even with this part of the wall...
if (this.ship.back + deltaY === i * 10) {
// ...AND the left side of the ship is over the left wall
// OR the right side of the ship is over the right wall...
if (this.ship.left + deltaX <= this.canyon.canyonMap[i][0] || this.ship.right + deltaX >= this.canyon.canyonMap[i][1]) {
// Game over!
this.end();
return;
}
}
} this.score++;
}, 10);
} end() {
removeEventListener("keydown", handleKeyDown, true);
removeEventListener("keyup", handleKeyDown, true);
clearInterval(intervalId);
this.displayScore();
}
}let canvas = document.getElementById("screen");
let game = new Game(canvas);game.start();
And that’s it! You now have your very own canyon runner game in just over 200 lines of JavaScript. How high a score can you get?
If you’re ready for more of a challenge, feel free to fork this REPL and consider adding the ability to pause and unpause the game, add more interesting behaviors (like different canyon layouts or enemy ships), or even add sound effects and music! Here’s a more advanced version of this game that a friend of mine made a few years ago; you can play it here.
I hope you enjoyed this tutorial!