Click here to Skip to main content
15,884,960 members
Articles / Web Development / React

Game Programming using JavaScript, React, Canvas2D and CSS – Part 3 (Final Part)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (4 votes)
22 Dec 2017CPOL6 min read 6.3K   1  
How to allow the player and invaders to shoot each other, add a simple high-score and a GameOver-screen

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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
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:

JavaScript
die() {
  this.setState({ gameState: GameState.GameOver });
  this.ship = null;
  this.invaders = [];
  this.lastStateChange = Date.now();
}
JavaScript
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:

JavaScript
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.

Image 1

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:

JavaScript
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:

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:

JavaScript
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:

JavaScript
increaseScore() {
    this.setState({ score: this.state.score + 500});
  }

and then bind this function to the onDie parameter of our Invaders in createInvaders:

JavaScript
....
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:

JavaScript
....
this.setState({
   gameState: GameState.Playing,
   score: 0
});

Finally, add the following three lines to the update method:

JavaScript
....
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Germany Germany
Hi there 🙂
My name is Philipp Engelmann, I work as a web developer at FIO SYSTEMS AG in Leipzig. I am interested in C#, Python, (REST-)API-Design, software architecture, algorithms and AI. Check out my blog at https://cheesyprogrammer.com/

Comments and Discussions

 
-- There are no messages in this forum --