In Part 2, I showed you how to add game objects and draw them to the canvas, how to handle game-state and how to move the player around. In this final part, we will allow the player and invaders to shoot each other, add a simple high-score and a GameOver-screen.
Adding Bullets
To get started, we will add our third and last Game component. Inside the GameComponents directory, add a new file Bullet.js:
export default class Bullet {
constructor(args) {
this.position = args.position;
this.speed = args.speed;
this.radius = args.radius;
this.delete = false;
this.onDie = args.onDie;
this.direction = args.direction;
}
die() {
this.delete = true;
}
update() {
if (this.direction === "up") {
this.position.y -= this.speed;
} else {
this.position.y += this.speed;
}
}
render(state) {
if(this.position.y > state.screen.height || this.position.y < 0) {
this.die();
}
const context = state.context;
context.save();
context.translate(this.position.x, this.position.y);
context.fillStyle = '#FF0';
context.lineWidth = 0,5;
context.beginPath();
context.arc(0, 0, 2, 0, 2 * Math.PI);
context.closePath();
context.fill();
context.restore();
}
}
As expected, this class works very similarly to our other game-components. We initialize the basic properties like position, speed and move direction and provide a update
and render
method to update the position of the bullet and draw it on the canvas.
Additionally, we call its die
method once it reaches the end of the screen.
Allowing the Player to Shoot
Now, we can use this new Bullet
inside the Ship
and Invader
classes. We will start with the ship
by adding an array of bullets in the constructor:
constructor(args) {
this.bullets = [];
this.lastShot = 0;
}
(Don’t forget to import ‘./Bullet’ first)
I also added a lastShot
property which we will use soon to control the number of bullets that can be shot in a given time period.
Next, add the following code inside the update
method to allow the player to actually shoot:
if (keys.space && Date.now() - this.lastShot > 250) {
const bullet = new Bullet({
position: { x: this.position.x, y : this.position.y - 5 },
speed: 2.5,
radius: 15,
direction : "up"
});
this.bullets.push(bullet);
this.lastShot = Date.now();
}
Pretty straight-forward! We check if the space key is pressed and at least 250ms have passed since the last bullet was fired. Feel free to customize this limit. Then, we add a new bullet at the ship
’s position and set its direction to “up”, so it moves upwards away from the player and towards the enemies. Finally, we add it to the bullets
array and update the lastShot
property.
Now, we only need a method to update and draw the bullets:
renderBullets(state) {
let index = 0;
for (let bullet of this.bullets) {
if (bullet.delete) {
this.bullets.splice(index, 1);
} else {
this.bullets[index].update();
this.bullets[index].render(state);
}
index++;
}
}
As you can see, we simply loop through the bullets
array and call each bullet’s update
and render
methods. When a bullet is deleted, we remove it from the array via the splice
method.
Now, we only have to call this method at the end of the ship
's render
method:
this.renderBullets(state)
Reload the app and you should see small bullets fly from the ship
when you press the space key!
Allowing the Invaders to Shoot
Now we have to implement the same logic for the invaders. Again, we will start by adding two new properties to the constructor of Invader.js:
constructor (args) {
....
this.bullets = [];
this.lastShot = 0;
}
The update
method will be very similar. The only two changes we have to make are to change the direction of the bullets from “up” to “down” and we have to find a new condition that triggers the shooting since the invaders aren’t player controlled.
To keep things simple, we will replace the key-check with a randomizer and simply append that to our lastShot
condition. With that, the full update
method of the Invaders looks like this:
update() {
if (this.direction === Direction.Right) {
this.position.x += this.speed;
} else {
this.position.x -= this.speed;
}
let nextShot = Math.random() * 5000
if (Date.now() - this.lastShot > 250 * nextShot) {
const bullet = new Bullet({
position: { x: this.position.x, y : this.position.y - 5 },
speed: 2.5,
radius: 15,
direction : "down"
});
this.bullets.push(bullet);
this.lastShot = Date.now();
}
}
(The changed lines are highlighted.)
Finally, we can copy and paste the renderBullets
method from the ship
class and call it in the render
method. (It makes a lot of sense to extract some base-classes here for all the common logic. But since we are focusing on ReactJS, I leave that to you.)
You should now have invaders that shoot back at you!
Collision Checks
To make our game objects interact with each other, we have to add basic collision checking. To do so, we can add the following two functions in a new Helper.js class:
export function checkCollisionsWith(items1, items2) {
var a = items1.length - 1;
var b;
for(a; a > -1; --a){
b = items2.length - 1;
for(b; b > -1; --b){
var item1 = items1[a];
var item2 = items2[b];
if(checkCollision(item1, item2)){
item1.die();
item2.die();
}
}
}
}
export function checkCollision(obj1, obj2) {
var vx = obj1.position.x - obj2.position.x;
var vy = obj1.position.y - obj2.position.y;
var length = Math.sqrt(vx * vx + vy * vy);
if(length < obj1.radius + obj2.radius) {
return true;
}
return false;
}
The first function takes two arrays of game objects and checks each item from the first list for collisions with each item from the second list. If there is a collision, we call the die
method of the affected objects.
The second method calculates the Euclidean distance between two objects. If it is smaller than the sum of their radiuses, both objects overlap with each other and we have a collision.
To use these new methods, import them at the top of the App.js file:
import { checkCollisionsWith } from './Helper';
In the update
method, add the following lines inside the this.state.gameState === GameState.Playing
condition to hook up the collision checks:
checkCollisionsWith(this.ship.bullets, this.invaders);
checkCollisionsWith([this.ship], this.invaders);
for (var i = 0; i < this.invaders.length; i++) {
checkCollisionsWith(this.invaders[i].bullets, [this.ship]);
}
As you can see, I added one check for the bullets of the players and the invaders, one for the ship
and the invaders and one for the bullets of each invader
and the ship
. In each of these cases, either the affected invader
or the ship
will be destroyed.
Now, we have to implement the die
method for the ship
and the invaders
. For the invaders
, we will simply set their delete
property to true
, so inside the Invader
class, we only have to add the following lines:
die() {
this.delete = true;
this.onDie();
}
If the player gets destroyed, however, we want to clear the screen of all objects and set the game state to GameOver
. Since we have to access properties from App.js, we will add this method inside that class and then pass it to the onDie
callback when we create the ship
in startGame
:
die() {
this.setState({ gameState: GameState.GameOver });
this.ship = null;
this.invaders = [];
this.lastStateChange = Date.now();
}
startGame() {
let ship = new Ship({
radius: 15,
speed: 2.5,
onDie: this.die.bind(this),
....
}
Finally, we have to add a die
method inside Ship.js to call the onDie
method:
die() {
this.onDie();
}
Start the app and you should now be able to really fight the invaders
! When an invader
gets hit, it will be removed from the game. If the player gets hit, the entire screen will be cleared and we are ready to transition to the GameOver
screen.
Game Over Screen
Once the player or all invaders are destroyed, we should show a GameOver
screen. First, we will add a new GameOverScreen.js class to our ReactComponents directory:
import React, { Component } from 'react';
export default class GameOverScreen extends React.Component {
constructor(args) {
super(args);
this.state = { score: args.score };
}
render() {
return (
<div>
<span className="centerScreen title">GameOver!</span>
<span className="centerScreen score">Score: { this.state.score }</span>
<span className="centerScreen pressEnter">Press enter to continue!</span>
</div>
);
}
}
and the following CSS in App.css:
.pressEnter {
top: 45%;
font-size: 26px;
color: #ffffff;
}
.score {
top: 30%;
font-size: 40px;
color: #ffffff;
}
for some basic styling.
The GameOver
screen works like the Titlescreen.js class. We display some text and style it with CSS. In addition to that, I added a state variable score
which will tell the player, how well he performed. We will provide that value from App.js.
Next, in the render
method of App.js, we will add the following line to display the GameOverScreen
only in the actual GameOver
state:
render() {
return (
<div>
{ this.state.gameState === GameState.StartScreen && <TitleScreen /> }
{ this.state.gameState === GameState.GameOver && <GameOverScreen score= { this.state.score } /> }
<canvas ref="canvas"
width={ this.state.screen.width * this.state.screen.ratio }
height={ this.state.screen.height * this.state.screen.ratio }
/>
</div>
);
}
(Again, don’t forget to add an import
statement for the GameOverScreen
component!)
To use the score
variable, we have to first add to our state by adding the following line to the initialization logic of the state in the constructor:
score: 0
To easily increase the score
, we will first encapsulate the logic that sets the state
into a new function:
increaseScore() {
this.setState({ score: this.state.score + 500});
}
and then bind this function to the onDie
parameter of our Invaders
in createInvaders
:
....
const invader = new Invader({
position: { x: newPosition.x, y: newPosition.y },
onDie: this.increaseScore.bind(this, false)
});
....
At the same time, we want to reset the score each time startGame
is called, so the score doesn’t accumulate over time:
....
this.setState({
gameState: GameState.Playing,
score: 0
});
Finally, add the following three lines to the update
method:
....
if (this.state.gameState === GameState.GameOver && keys.enter) {
this.setState({ gameState: GameState.StartScreen});
}
This will allow us to transition from the GameOver
-screen back to the Start
-screen.
Conclusion
That’s it! We have completed our simple Space Invaders clone. Time to play around with it and show it your friends. Also, I hope I inspired you to dive deeper into game development, JavaScript, and ReactJS.
If you have any questions, problems or feedback, please let me know in the comments.