We will follow another amazing Tutorial by Ania Kubów to build our very own Space Invaders game. If you didn’t do so yet head to her Youtube channel and subscribe or give her a like because her content is amazing.
As usual we will use codesandbox.io for our implementation. The entrypoint for our website is our index.html
file.
In this file we need to add a link to our styles.css
and a reference to our app.js
. In order to check that our linking
works fine I will add some dummy code to show an alert in our JavaScript file alert("Hey Space Invaders")
and a body { background-color: hotpink }
to our CSS file.
Now when we refresh or open our HTML file we should see a pink background a pop up greeting the Space Invaders.
The main thing we need in our game is our game grid - ours will have a size of 15 X 15. Therefore, we need 225 cells. A cell will be represented by a div tag.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="app.js"></script>
<link rel="stylesheet" type="text/css" href="styles.css"/>
<title>Space Invaders</title>
</head>
<body>
<div class="grid">
<div class="cell"></div>
...
<div class="cell"></div>
</div>
</body>
</html>
Next up we will define the size of our grid and make it visible by applying some background colors. We want every cell to have a size of 20 x 20 pixels therefor our whole grid will have a width and height of 15 * 20 = 300 pixels. We will also set up some styling for our cells so we see their placement.
.grid {
height: 300px;
width: 300px;
background: lightblue;
}
.cell {
border: solid lightgray 1px;
height: 18px;
width: 18px;
}
When we refresh our page now, we will see that our cells actually fall out of our grid. In order to correct that we will use flex-wrap on the grid, which tells the child elements to wrap if necessary (instead of overflowing like we’ve seen before).
.grid {
display: flex;
flex-wrap: wrap;
}
Now we should end up with a grid that resembles this structure:
00 01 02 03 04 05 06 07 08 09 10 11 12 13 14
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
...
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
The first thing we want to is display our player. In the beginning of our game our player will be at the middle of one of the bottom rows. We will create a variable to keep its position in our JavaScript file so that we can later on modify it. The player will be displayed on the grid by just setting a CSS class on a specific grid cell.
let cells = document.querySelectorAll('.grid .cell');
let playerPosition = 202;
cells[playerPosition].classList.add('player');
💡 With document.querySelectorAll
you can select and modify HTML elements based on a CSS selector. querySelector
will return the first matching element
whereas querySelectorAll
will return a collection of all matching elements.
When we open our index file in the browser now, we get an error:
Uncaught TypeError: Cannot read property 'classList' of undefined
at app.js:4`
When we set a breakpoint we actually see, that our cells
list is empty. That is because we need to wait for our HTML to be built correctly before we select the elements with the selector.
A convenient way to achieve this is to listen to the DOMContentLoaded
event.
document.addEventListener('DOMContentLoaded', () => {
let cells = document.querySelectorAll('.grid .cell');
let playerPosition = 202;
cells[playerPosition].classList.add('player');
});
💡 document.addEventListener
receives two parameters - the first one is the event to listen to, and the second is a function that will be invoked whenever the specified event occurs.
Now if we take a look in our console our error will be gone and we will see one cell has an addional “player” class if we inspect our HTML markup in the browser. To make this player visible we can add a simple styling:
.player {
background-color: darkblue;
}
Now we see one cell in a dark blue color, this is our player.
Next we will change our player to actually be a spaceship and the background of our game to black to simulate the space.
.grid {
background: black;
}
.cell {
height: 20px;
width: 20px;
}
.player {
background: url(assets/player_01.png) no-repeat center center;
background-size: contain;
}
Next we will implement our moveLeft and moveRight functions so that we can move our player to the left and right when we press those arrow keys:
function moveLeft() {
cells[playerPosition].classList.remove('player');
playerPosition -= 1;
cells[playerPosition].classList.add('player');
}
function moveRight() {
cells[playerPosition].classList.remove('player');
playerPosition += 1;
cells[playerPosition].classList.add('player');
}
By adding an event listener to the keydown event we can specify a function that will always be called when any key on the keyboard is pressed. This event has a property keyCode
which we can use to only react on certain key presses.
document.addEventListener('keydown', function keyDownListener(e) {
if (e.keyCode === 39) { // arrow right
moveRight();
} else if (e.keyCode === 37) { // arrow left
moveLeft();
}
});
Now we can already properly move to the left and to the right - but we can move outside of our grid and cause an error in our program, so we need to add a check in the moveLeft/moveRight function to only move if we are not yet touching the edge. We can easily identify if one of our displayed elements touches the left side by calculating the aboslute positions and using the modulo function.
function touchingRightEdge() {
return playerPosition % 15 === 14;
}
function moveRight() {
if (!touchingRightEdge()) {
// remove player class, move player, add player class
}
}
function touchingLeftEdge() {
return playerPosition % 15 === 0;
}
function moveLeft() {
if (!touchingLeftEdge()) {
// remove player class, move player, add player class
}
}
For a start we will show three rows with 10 invaders in each row. Let’s start by defining the Array of how those invaders are placed in reference to each other. Additionally we will define a position that will be modified when the invaders and will be used to draw the invaders on our game grid.
let invaders = [
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
30, 31, 32, 33, 34, 35, 36, 37, 38, 39
];
let invadersPosition = 3;
We wil use the invadersPosition
variable to calculate our absolute position in the cells array and we can place the
space invaders by adding a css class invader
to the respective cells.
function drawInvaders() {
invaders.forEach(relativePosition => cells[relativePosition + invadersPosition].classList.add('invader'));
}
function undrawInvaders() {
invaders.forEach(relativePosition => cells[relativePosition + invadersPosition].classList.remove('invader'));
}
When we call our drawInvaders()
function once in the beginning of our script and add some styling to our class, the invaders should show up.
.invader {
background-color: purple;
}
We can improve our styling by applying images for our alien invaders. As they are a little bit big I also changed the size of the cells to 18 px with a 2 px border. As we have three different invaders I also split it into three css classes.
function drawInvaders() {
invaders.forEach(relativePosition => {
if (relativePosition < 10) {
cells[relativePosition + invadersPosition].classList.add('invader-1');
} else if (relativePosition < 25) {
cells[relativePosition + invadersPosition].classList.add('invader-2');
} else {
cells[relativePosition + invadersPosition].classList.add('invader-3');
}
});
}
function undrawInvaders() {
invaders.forEach(relativePosition => {
cells[relativePosition + invadersPosition].classList.remove('invader-1');
cells[relativePosition + invadersPosition].classList.remove('invader-2');
cells[relativePosition + invadersPosition].classList.remove('invader-3');
})
}
.invader-1 {
background: url(assets/enemy_01.png) no-repeat center center;
background-size: contain;
}
.invader-2 {
background: url(assets/enemy_02.png) no-repeat center center;
background-size: contain;
}
.invader-3 {
background: url(assets/enemy_03.png) no-repeat center center;
background-size: contain;
}
Our alien invaders start at the top left edge, move to the right until they touch the right edge, move one row down, move to the left until we touch the left edge, move one row down and so an and so forth. Let’s implement the move function.
function moveInvaders() {
undrawInvaders();
if (direction === 'LEFT') {
if (touchingLeftEdge()) {
moveInvadersDown();
direction = 'RIGHT';
} else {
moveInvadersLeft();
}
} else if (direction === 'RIGHT') {
if (touchingRightEdge()) {
moveInvadersDown();
direction = 'LEFT';
} else {
moveInvadersRight();
}
}
}
function moveInvadersDown() {
undrawInvaders();
invadersPosition += 15;
drawInvaders();
}
function moveInvadersRight() {
undrawInvaders();
invadersPosition += 1;
drawInvaders();
}
function moveInvadersLeft() {
undrawInvaders();
S -= 1;
drawInvaders();
}
To get more of an arcade game feeling we will add a sound to every movement our alien makes. Let’s call this function playAlienCreak
and invoke it at the end of our moveInvaders
.
function playAlienCreak() {
let audio = new Audio('assets/alien.wav');
audio.play();
}
The game is over when any of the invaders touch our ship. After every invader movement we need to check if any of our invaders overlap with the current players position.
function checkGameOver() {
if (invaders.some(realtivePosition => realtivePosition + invadersPosition === playerPosition)) {
alert("An invader got you");
clearInterval(invaderMovement);
}
}
And because it’s fun let’s add another sound when we lose.
function playGameOver() {
let audio = new Audio('assets/game-over.mp3');
audio.play();
}
To give our spaceship any chance of winnig we will equip it with the ability to shoot lasers. Whenever we press the space bar we want a new laser to appear at the space ships position. then we want it to move upwards.
function shootLaser() {
let laserPosition = playerPosition;
function moveLaser() {
cells[laserPosition].classList.remove("laser");
laserPosition -= 15;
if (laserPosition < 0) {
clearInterval(laserId);
}
cells[laserPosition].classList.add("laser");
}
let laserId = setInterval(moveLaser, 500);
}
And the logic to call this function whenever the space bar is pressed is in our eventhandler for the keypress.
document.addEventListener('keydown', function keyDownListener(e) {
// ... handle other key codes
if (e.keyCode === 32) {
shootLaser();
}
});
Now we can shoot lasers but nothing happens yet when they touch an alien. We can simply check if the laser touches an invader if one of the invaders have the same position as the laser. When this is the case we hide the invader and remove it from our invaders list. Moreover we will increase a score to dispaly on our page.
let hitByLaser = invaders.find(invaderRelativePosition => invaderRelativePosition + invadersPosition === laserPosition);
if (hitByLaser != null) {
cells[hitByLaser + invadersPosition].classList.remove("invader");
cells[hitByLaser + invadersPosition].classList.remove("laser");
invaders = invaders.filter(invader => invader !== hitByLaser);
score += 1;
document.querySelector(".score").innerHTML = score;
clearInterval(laserId);
}