65.9K
CodeProject is changing. Read more.
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4 votes)

Dec 22, 2017

CPOL

6 min read

viewsIcon

6581

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:

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.