- Create Files & Initial Setup
- Create the start screen and start game button
- Create Game constructor, Player constructor, Enemy constructor and get elements needed for the game start
- Add Player movement functionality and Instantiate the player object
- Create the game loop using requestAnimationFrame() (updating positions, collisions, drawing players and enemies)
- Implement the collision checking function
- Implement the Game Over logic/sequence (remove game screen, show game over screen, restart, show score)
mkdir eternal_enemies && cd eternal_enemies
mkdir src css
touch index.html src/main.js src/game.js src/player.js src/enemy.js css/style.css
code .
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Canvas Code Along</title>
<link rel="stylesheet" href="./css/style.css" />
</head>
<body>
<script src="./src/main.js"></script>
<script src="./src/game.js"></script>
<script src="./src/player.js"></script>
<script src="./src/enemy.js"></script>
</body>
</html>
/* ---- CSS reset ---- */
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
padding: 0;
}
/* ---- typography ---- */
body {
color: #111;
font-family: Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 147%;
}
p {
margin: 0;
}
/* ---- layout ---- */
html,
body {
height: 100vh;
overflow: hidden;
}
.container {
margin: 0 20px;
}
@media (min-width: 768px) {
.container {
max-width: 728px; /* Resizes the container to 728px for large screens */
margin: 0 auto;
}
}
/* ---- components ---- */
.game {
text-align: center;
display: flex;
flex-direction: column;
height: 100%;
}
.game header {
height: 50px;
display: flex;
align-items: center;
}
.game .lives {
flex: 1;
}
.game .score {
flex: 1;
}
.game .label {
font-weight: bold;
}
.game .value {
font-style: italic;
}
.game .canvas-container {
flex: 1;
}
Outline the functions that we will need for creating different game screens and use to start and end the game
"use strict";
let game; // instance of the Game
let splashScreen; // Start Game Screen
let gameOverScreen;
// Creates DOM elements from a string representation
function buildDom() {}
// -- splash (start) screen
function createSplashScreen() {}
function removeSplashScreen() {}
// -- game screen
function createGameScreen() {}
function removeGameScreen() {}
// -- game over screen
function createGameOverScreen(score) {}
function removeGameOverScreen() {}
// -- Setting the game state - start or game over
function startGame() {}
function endGame() {}
// buildDom
function buildDom(htmlString) {
const div = document.createElement("div");
div.innerHTML = htmlString;
return div.children[0];
}
// createSplashScreen()
// -- splash screen
function createSplashScreen() {
splashScreen = buildDom(`
<main>
<h1>Eternal Enemies</h1>
<button>Start</button>
</main>
`);
document.body.appendChild(splashScreen);
const startButton = splashScreen.querySelector("button");
startButton.addEventListener("click", function () {
console.log("You clicked Start!");
// Here we start the game
});
}
// removeSplashScreen()
function removeSplashScreen() {
// remove() is the DOM method that removes the Node from the page
splashScreen.remove();
}
// Run the function `createSplashScreen` once all of the resources are loaded
window.addEventListener("load", createSplashScreen);
// createGameScreen()
// -- game screen
function createGameScreen() {
gameScreen = buildDom(`
<main class="game container">
<header>
<div class="lives">
<span class="label">Lives:</span>
<span class="value"></span>
</div>
<div class="score">
<span class="label">Score:</span>
<span class="value"></span>
</div>
</header>
<div class="canvas-container">
<canvas></canvas>
</div>
</main>
`);
document.body.appendChild(gameScreen);
return gameScreen;
}
// removeGameScreen()
function removeGameScreen() {
gameScreen.remove();
}
// startGame()
function startGame() {
removeSplashScreen();
createGameScreen();
}
function createSplashScreen() {
// ...
// ...
const startButton = splashScreen.querySelector("button");
//startButton.addEventListener('click', function() {
//console.log('You clicked Start!');
//});
startButton.addEventListener("click", startGame); // <- UPDATE
}
Create Game constructor, Player constructor, Enemy constructor and get elements needed for the game start
"use strict";
class Game {
constructor() {
this.canvas = null;
this.ctx = null;
this.enemies = [];
this.player = null;
this.gameIsOver = false;
this.gameScreen = null;
this.score = 0;
}
// Create `ctx`, a `player` and start the Canvas loop
start() {}
startLoop() {}
checkCollisions() {}
gameOver() {}
updateGameStats() {}
}
function startGame() {
removeSplashScreen();
createGameScreen();
game = new Game(); // <- UPDATE ADD
game.gameScreen = gameScreen; // <- UPDATE ADD
// Start the game
}
This method will be the initiator of every new game, and does the following :
- saves reference to the Lives and Score
.value
<span>
elements, for later updates. - saves the reference to canvas created by
createGameScreen()
inmain.js
, for use when animating. - set canvas size to cover the screen
- creates the new player object for the current game
- and starts the rendering loop which will be running the animation
// start() method on the Game prototype
start() {
// Save reference to canvas and container. Create ctx
this.canvasContainer = document.querySelector('.canvas-container');
this.canvas = this.gameScreen.querySelector('canvas');
this.ctx = this.canvas.getContext('2d');
// Save reference to the score and lives elements
this.livesElement = this.gameScreen.querySelector('.lives .value');
this.scoreElement = this.gameScreen.querySelector('.score .value');
// Set the canvas dimensions to match the parent
this.containerWidth = this.canvasContainer.offsetWidth;
this.containerHeight = this.canvasContainer.offsetHeight;
this.canvas.setAttribute('width', this.containerWidth);
this.canvas.setAttribute('height', this.containerHeight);
// Create a new player for the current game
this.player = {};
// Add event listener for moving the player
// ..
// Start the canvas requestAnimationFrame loop
this.startLoop();
};
function startGame() {
removeSplashScreen();
// later we need to add clearing of the gameOverScreen
game = new Game();
game.gameScreen = createGameScreen();
game.start(); // <-- UPDATE
// End the game
}
"use strict";
class Player {
constructor(canvas, lives) {
this.canvas = canvas;
this.ctx = this.canvas.getContext("2d");
this.lives = lives;
this.size = 100;
this.x = 50;
this.y = canvas.height / 2;
this.direction = 0;
this.speed = 5;
}
setDirection(direction) {}
handleScreenCollision() {}
removeLife() {}
draw() {}
didCollide(enemy) {}
}
// setDirection() - method on the Player prototype
setDirection (direction) {
// +1 down -1 up
if (direction === 'up') this.direction = -1;
else if (direction === 'down') this.direction = 1;
};
// handleScreenCollision() - method on the Player prototype
handleScreenCollision () {
this.y = this.y + this.direction * this.speed;
const screenTop = 0;
const screenBottom = this.canvas.height;
const playerTop = this.y;
const playerBottom = this.y + this.size;
if (playerBottom > screenBottom) this.direction = -1;
else if (playerTop < screenTop) this.direction = 1;
};
// removeLife() - method on the Player prototype
removeLife () {
this.lives -= 1;
};
// draw() - method on the Player prototype
draw() {
this.ctx.fillStyle = "#66D3FA";
// fillRect(x, y, width, height)
this.ctx.fillRect(this.x, this.y, this.size, this.size);
}
"use strict";
class Enemy {
constructor(canvas, y, speed) {
this.canvas = canvas;
this.ctx = canvas.getContext("2d");
this.size = 20;
this.x = canvas.width + this.size;
this.y = y;
this.speed = speed;
}
draw() {}
updatePosition() {}
isInsideScreen() {}
}
// draw() - method on the Enemy prototype
draw () {
this.ctx.fillStyle = '#FF6F27';
// fillRect(x, y, width, height)
this.ctx.fillRect(this.x, this.y, this.size, this.size);
};
// updatePosition() - method on the Enemy prototype
updatePosition () {
this.x = this.x - this.speed;
};
// isInsideScreen() - method on the Enemy prototype
isInsideScreen () {
// if x plus half of its size is smaller then 0 return
return this.x + this.size / 2 > 0;
};
3. Bind event callback handleKeyDown
, as the context (this
value) of event callbacks is the global Window
object.
// start() - method on the Game prototype
start () {
// ...
// ...
// ...
// this.player = {};
this.player = new Player(this.canvas, 3); // <-- UPDATE
// Event listener callback function
function handleKeyDown(event) {
if (event.key === "ArrowUp") {
this.player.setDirection("up");
} else if (event.key === "ArrowDown") {
this.player.setDirection("down");
}
}
const boundHandleKeyDown = handleKeyDown.bind(this);
document.body.addEventListener("keydown", boundHandleKeyDown);
// Any function provided to eventListener is always invoked by the `window` global object
// Therefore, we need to bind `this` to the `game` object,
// to prevent `this` from referencing the `window` object
// Start the canvas requestAnimationFrame loop
this.startLoop();
};
When the game starts, we can press UP and DOWN keys and print direction to the console, but we will not see the player ?
...
Create the game loop using requestAnimationFrame()
(updating positions, collisions, drawing players and enemies)
// startLoop() - method on the Game prototype
startLoop () {
const loop = function() {
console.log('in loop');
// EVERYTHING HAPPENS HERE !
if (!this.gameIsOver) {
window.requestAnimationFrame(loop);
}
}.bind(this);
// As loop function will be continuously invoked by
// the `window` object- `window.requestAnimationFrame(loop)`
// we have to bind the function so that value of `this` is
// pointing to the `game` object, like this:
// var loop = (function(){}).bind(this);
window.requestAnimationFrame(loop);
};
// startLoop() - method on the Game prototype
startLoop () {
const loop = function() {
// 1. UPDATE THE STATE OF PLAYER AND ENEMIES
// 0. Our player was already created - via `game.start()`
// 1. Create new enemies randomly
// 2. Check if player had hit any enemy (check all enemies)
// 3. Update the player and check if player is going off the screen
// 4. Move existing enemies
// 5. Check if any enemy is going of the screen
// 2. CLEAR THE CANVAS
// 3. UPDATE THE CANVAS
// Draw the player
// Draw the enemies
// 4. TERMINATE LOOP IF GAME IS OVER
if (!this.gameIsOver) {
window.requestAnimationFrame(loop);
}
}.bind(this);
// As loop function will be continuously invoked by
// the `window` object- `window.requestAnimationFrame(loop)`
// we have to bind the function so that value of `this` is
// pointing to the `game` object, like this:
// var loop = (function(){}).bind(this);
window.requestAnimationFrame(loop);
};
// startLoop() - method on the Game prototype
startLoop () {
const loop = function() {
// 1. UPDATE THE STATE OF PLAYER AND ENEMIES
// 0. Our player was already created - via `game.start()`
// 1. Create new enemies randomly
if (Math.random() > 0.98) {
var randomY = this.canvas.height * Math.random();
var newEnemy = new Enemy(this.canvas, randomY, 5);
this.enemies.push(newEnemy);
}
// 2. Check if player had hit any enemy (check all enemies)
this.checkCollisions();
// 3. Update the player and check if player is going off the screen
this.player.handleScreenCollision();
// 4. Move existing enemies
// 5. Check if any enemy is going of the screen
this.enemies = this.enemies.filter(function(enemy) {
enemy.updatePosition();
return enemy.isInsideScreen();
});
// 2. CLEAR THE CANVAS
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// 3. UPDATE THE CANVAS
// Draw the player
this.player.draw();
// Draw the enemies
this.enemies.forEach(function(enemy) {
enemy.draw();
});
// 4. TERMINATE LOOP IF GAME IS OVER
if (!this.gameIsOver) {
window.requestAnimationFrame(loop);
}
}.bind(this);
// As loop function will be continuously invoked by
// the `window` object- `window.requestAnimationFrame(loop)`
// we have to bind the function so that value of `this` is
// pointing to the `game` object, like this:
// var loop = (function(){}).bind(this);
window.requestAnimationFrame(loop);
};
If we run the game now, we will see that the movement and animations work fine, except for the collisions (which we didn't implement yet).
Our loop is calling the checkCollisions
but we don't have any functionality in that function. Let's update it.
// checkCollisions() - method on the Game prototype
checkCollisions () {
this.enemies.forEach( function(enemy) {
// We will implement didCollide() in the next step
if ( this.player.didCollide(enemy) ) {
this.player.removeLife();
console.log('lives', this.player.lives);
// Move the enemy off screen, to the left
enemy.x = 0 - enemy.size;
if (this.player.lives === 0) {
this.gameOver();
}
}
}, this);
// We have to bind `this`
// as array method callbacks `this` value defaults to undefined.
};
// didCollide() - method on the Player prototype
didCollide(enemy) {
const playerLeft = this.x;
const playerRight = this.x + this.size;
const playerTop = this.y;
const playerBottom = this.y + this.size;
const enemyLeft = enemy.x;
const enemyRight = enemy.x + enemy.size;
const enemyTop = enemy.y;
const enemyBottom = enemy.y + enemy.size;
// Check if the enemy sides intersect with any of the player's sides
const crossLeft = enemyLeft <= playerRight && enemyLeft >= playerLeft;
const crossRight = enemyRight >= playerLeft && enemyRight <= playerRight;
const crossBottom = enemyBottom >= playerTop && enemyBottom <= playerBottom;
const crossTop = enemyTop <= playerBottom && enemyTop >= playerTop;
if ((crossLeft || crossRight) && (crossTop || crossBottom)) {
return true;
}
else {
return false;
}
};
Implement the game over logic/sequence (remove game screen, show game over screen, restart, show score)
The gameOver
method will be used to set the gameIsOver
flag, and to invoke the function gameEnd
from the main.js
which removes the game screen and creates game over screen.
// gameOver() - method on the Game prototype
gameOver () {
// flag `gameIsOver = true` stops the loop
this.gameIsOver = true;
console.log('GAME OVER');
// Call the `endGame` function from `main` to remove the Game screen
// and show the Game Over Screen
endGame(); // <--- UPDATE
};
Implement the function endGame
in main.js
used to remove the current game screen and display the game over screen.
// main.js endGame()
// -- game over
function endGame(score) {
// Remove the game screen
gameScreen.remove(); // remove() is the DOM method which removes the DOM Node
// Create and display game over screen
gameOverScreen = buildDom(`
<main>
<h1>Game over</h1>
<p>Your score: <span>${score}</span></p>
<button>Restart</button>
</main>
`);
// Add event listener to the Restart button
var button = gameOverScreen.querySelector("button");
button.addEventListener("click", startGame);
document.body.appendChild(gameOverScreen);
}
function createGameOverScreen() {
gameOverScreen = buildDom(`
<main>
<h1>Game over</h1>
<p>Your score: <span></span></p>
<button>Restart</button>
</main>
`);
const button = gameOverScreen.querySelector("button");
button.addEventListener("click", startGame);
document.body.appendChild(gameOverScreen);
}
// main.js removeGameScreen()
function removeGameScreen() {
gameScreen.remove();
}
// main.js endGame()
// -- game over
function endGame() {
// <--- REFACTOR FUNCTION
removeGameScreen(); // <--- UPDATE
createGameOverScreen(); // <--- UPDATE
}
// main.js removeGameOverScreen()
function removeGameOverScreen() {
if (gameOverScreen !== undefined) {
gameOverScreen.remove();
}
}
// main.js startGame()
function startGame() {
removeSplashScreen();
removeGameOverScreen(); // <-- UPDATE
createGameScreen();
game = new Game();
game.gameScreen = gameScreen;
game.start();
}
// updateGameStats() - method on the Game prototype
updateGameStats () {
this.score += 1;
this.livesElement.innerHTML = this.player.lives;
this.scoreElement.innerHTML = this.score;
};
// game.js
// startLoop() - method on the Game prototype
startLoop () {
var loop = function() {
...
...
...
// 4. TERMINATE LOOP IF GAME IS OVER
if (!this.gameIsOver) {
window.requestAnimationFrame(loop);
}
// 5. Update Game data/stats
this.updateGameStats(); // <- UPDATE
}.bind(this);
// As loop function will be continuously invoked by
// the `window` object- `window.requestAnimationFrame(loop)`
// we have to bind the function so that value of `this` is
// pointing to the `game` object, like this:
// var loop = (function(){}).bind(this);
window.requestAnimationFrame(loop);
};
// game.js
// gameOver() - method on the Game prototype
gameOver() {
this.gameIsOver = true;
// Call the `endGame` function from `main` to remove the Game screen
// and show the Game Over Screen
endGame(this.score); // <--- UPDATE
}
// main.js
// endGame()
function endGame(score) {
removeGameScreen();
createGameOverScreen(score);
}
function createGameOverScreen(score) { // <-- UPDATE
gameOverScreen = buildDom(`
<main>
<h1>Game over</h1>
<p>Your score: <span> ${score} </span></p>
<button>Restart</button>
</main>
`);