Game programming using Javascript, React, Canvas2D and CSS – Part 2

In the last post we set up our react-app, added an InputManager class to handle user input and added a first component to render a title-screen.
In this part, we will implement state management to switch from the title screen to the playing-screen, add a player-controlled ship-class and finally some invaders for the player to fight!

State management

To easily switch between the different components of our game, we need some sort of game state management. In a more complex game, it makes sense to create a separate class for that purpose. To keep things simple, we will keep all state management logic inside our App.js class. First, we will add a new enum that represents the different game states above the class definition:

const GameState = {
   StartScreen : 0,
   Playing : 1,
   GameOver : 2
};

Now we can add a member variable of this enum to our state in the constructor of App.js. We will set it to StartScreen initially:

constructor() {
    super();
    this.state = { 
       input: new InputManager(),
       screen: {
         width: width,
         height: height,
         ratio: ratio        
      },
      gameState: GameState.StartScreen
    };
}

Finally we can update our render() method, to only draw the TitleScreen in the inital state:

render() {
    return (
      <div>
        { this.state.gameState === GameState.StartScreen && <TitleScreen /> }   
        ...
      </div>
    );
  }

As you can see, I prepended the gameState condition to the jsx-code for drawing the title-screen, thus the title screen will be rendered only in the initial state.

The game loop

To actually switch from the initial state to the Playing state, we have to wait for the user to press the Enter button. Since we have to react to user-input continuously, we will create a game loop that runs as long as the application itself is running. To do so, we will add an update method, that continuously calls itself after each run:

  update(currentDelta) {
    requestAnimationFrame(() => {this.update()});
  } 

As you can see, we simply call the requestAnimationFrame() method to call the update() method again after completion.
To start the requestAnimationFrame loop, we have to call it once our component is loaded. The best place for this is the componentDidMount method provided by React:

componentDidMount() {
    this.state.input.bindKeys();
    requestAnimationFrame(() => {this.update()});   
  }

As you can see, we simply added the last line from update to the end of this method.
Now we can finally start reacting to user input:

update() {
    const keys = this.state.input.pressedKeys;
    if (this.state.gameState === GameState.StartScreen && keys.enter) {
      this.startGame();
    }
    requestAnimationFrame(() => {this.update()});
  }

First, we get the keys from our input-manager and store it in a local variable for easy access. Next, we check if the game is in StartScreen state and the user pressed the enter button. If so, we will call a new helper method to start the game:

startGame() {
    this.setState({
      gameState: GameState.Playing
    }); 
  }

We will revisit this method later to add more initialization code. For now, it is only responsible for changing to the playing state.
Now, if you start the application and press the Enter key, the title screen should disappear.

Adding the player

Now it’s time to add our first game object, the player! To keep all our game objects in the same location, we will first create a new folder in the src directory called GameComponents. In there, we will define a new class Ship.js:

export default class Ship {
    constructor(args){
       this.position = args.position;       
       this.speed = args.speed;
       this.radius = args.radius;  
       this.delete = false;
       this.onDie = args.onDie;
   }
}

Most of the properties should be self-explanatory. When the ship is destroyed, delete will be set to true and onDie will be called.
As in App.js we will now create and implement the update method, which is responsible for updating the position of the ship according user input:

update(keys) {
     if (keys.right) {
         this.position.x += this.speed;
     } else if (keys.left) {
	  this.position.x -= this.speed;
     }	
}

As you can see, we simply increment/decrement the x coordinate of the position. This way the ship will move left or right depending on the pressed keys, which are passed as a parameter.
Now we only have to implement the render method to actually draw the ship:

render(state) {
    const context = state.context;
    context.save();
    context.translate(this.position.x, this.position.y);
    context.strokeStyle = '#ffffff';
    context.fillStyle = '#ffffff';
    context.lineWidth = 2;
    context.beginPath();
    context.moveTo(0, -25);
    context.lineTo(15, 15);
    context.lineTo(5, 7);
    context.lineTo(-5, 7);
    context.lineTo(-15, 15);
    context.closePath();
    context.fill();
    context.stroke();
    context.restore();
}

Here we are using the basic functionality of HTML Canvas to draw some lines in the shape of the ship and change it’s color to white to distinguish it from the black canvas.
Finally, we have to ensure our ship stays within the frame. To do so, we will simply set the ships position.x to 0 when it leaves the screen on the right side and to the width of the screen when it leaves on the left side:

if(this.position.x > state.screen.width) { 
   this.position.x = 0;
} else if(this.position.x < 0) {
   this.position.x = state.screen.width;
}

Now we are ready to integrate our new Ship.js class into App.js. First we have to add the context property in the constructor, so it can be used by the Ships render method:

...
context: null
};
this.ship = null;

I also added a new property to hold our ship instance, which I also set to null for now.
Then we can initialize the context in the componentDidMount method:

componentDidMount() {
    this.state.input.bindKeys();
    const context = this.refs.canvas.getContext('2d');
    this.setState({ context: context });
    requestAnimationFrame(() => {this.update()});    
  }

and the ship in the startGame method:

startGame() {
     let ship = new Ship({
      radius: 15,
      speed: 2.5,
      position: {
        x: this.state.screen.width/2,
        y: this.state.screen.height - 50
      }});
    this.ship = ship;
    this.setState({
      gameState: GameState.Playing
    });
  }

I set x and y position so that the ship will be drawn in the lower middle of the screen. Feel free to play around with these parameters.
We will provide the onDie callback later. For now, lets add the missing calls to our ships update and render methods. Let’s take a look at the adjusted update method:

update(currentDelta) {
    const keys = this.state.input.pressedKeys;
    if (this.state.gameState === GameState.StartScreen && keys.enter) {
      this.startGame();
    }
    if (this.state.gameState === GameState.Playing) {
      clearBackground();
      if (this.ship !== undefined && this.ship !== null) {
        this.ship.update(keys);
        this.ship.render(this.state);        
      }
    }
    requestAnimationFrame(() => {this.update()});
  }

As you can see, I added a new condition for the Playing state because only in that case we want to update and render our game components. To keep things simple, we will call both the ships update and render methods here.
Finally, take a look at the clearBackground method, which cleans up the canvas before draw new content onto it:

clearBackground() {
    const context = this.state.context;
    context.save();
    context.scale(this.state.screen.ratio, this.state.screen.ratio);
    context.fillRect(0, 0, this.state.screen.width, this.state.screen.height);
    context.globalAlpha = 1;
}

Now, when you run the application and navigate to the Playing screen, you should see our ship and be able to control it with the left and right arrow buttons (OR A and D keys).

Adding enemies

In the last section of Part 2, I will show you how to add some simple invaders to our game. First, we will add a new Invader.js class inside the GameComponents directory. Since it is very similar to the Ship.js class, I will show you the entire code first and then walk you through it:

export const Direction = {
	  Left:  0,
	  Right: 1,
   };

export default class Invader { 
    constructor (args) {
      this.direction = Direction.Right;
      this.position = args.position;       
      this.speed = args.speed;
      this.radius = args.radius;  
      this.delete = false;
      this.onDie = args.onDie;
    }

    reverse() {
	if (this.direction === Direction.Right) {
            this.position.x -= 10;
	    this.direction = Direction.Left;
	} else {
	    this.direction = Direction.Right;
	    this.position.x += 10;
	    }
	}

    update() {
        if (this.direction === Direction.Right) {
	    this.position.x += this.speed;	
	} else {
	    this.position.x -= this.speed;
        }
    }

    render(state) {
        const context = state.context;
        context.save();
        context.translate(this.position.x, this.position.y);
        context.strokeStyle = '#F00';
        context.fillStyle = '#F00';
        context.lineWidth = 2;
        context.beginPath();
        context.moveTo(-5, 25);
        context.arc(0, 25, 5, 0, Math.PI);
        context.lineTo(5, 25);
        context.lineTo(5, 0);
        context.lineTo(15, 0);
        context.lineTo(15, -15);
        context.lineTo(-15, -15);
        context.lineTo(-15, 0);
        context.lineTo(-5, 0);
        context.closePath();
        context.fill();
        context.stroke();
        context.restore();
    }
}

The render works in the same way the ships render method works, except that it draws a differently shaped spaceship facing down the screen instead of up. The update again increments or decrements the positions x coordinate. However, instead of reacting to user input, we check the Direction-enum to see, where the invader should move. This way, instead of jumping to the other side of the screen, they will simply turn around when they reach one edge of the screen.
Back in App.js we will first add a new property to hold an array of invaders to our constructor:

...
this.ship = null;
this.invaders = [];

Now we will add three new methods: createInvaders, renderInvaders and reverseInvaders:

  createInvaders(count) {
    const newPosition = { x: 100, y: 20 };
    let swapStartX = true;

    for (var i = 0; i < count; i++) {
      const invader = new Invader({
         position: { x: newPosition.x, y: newPosition.y },
         speed: 1,
         radius: 50
      });

      newPosition.x += invader.radius + 20;

      if (newPosition.x + invader.radius + 50 >= this.state.screen.width) {
        newPosition.x = swapStartX ? 110 : 100;
        swapStartX = !swapStartX;
        newPosition.y += invader.radius + 20;
      }

      this.invaders.push(invader);
    }
  }

  renderInvaders(state) {
    let index = 0;
    let reverse = false;

    for (let invader of this.invaders) {
      if (invader.delete) {
        this.invaders.splice(index, 1);
      }
      else if (invader.position.x + invader.radius >= this.state.screen.width || 
               invader.position.x - invader.radius <= 0) {
        reverse = true;
      }
      else {        
        this.invaders[index].update();
        this.invaders[index].render(state);
      }
      index++;
    }

    if (reverse) {
      this.reverseInvaders();
    }
  }

  reverseInvaders() {
    let index = 0;
    for (let invader of this.invaders) {
      this.invaders[index].reverse();
      this.invaders[index].position.y += 50;
      index++;
    }
  } 

createInvaders initializes our invaders array and sets their positions. It places each invader on the right side of the previous one. If there is no space, the next invader will be drawn on a new row. renderInvaders contains the logic for drawing our invaders and keeping them inbound while moving. If an invader is deleted (delete === true), it will be removed from the array.
Finally, if any of our invaders reaches either edge of the screen, we call the reverseInvaders method which in turn calls the reverse method of each invader, thus changing its direction.
We will call createInvaders from our startGame method:

startGame() {
   ...
   this.createInvaders(27);
   this.setState({
   ...
}

Feel free to play around with the number of invaders you want to create.
Now we only have to add a call to renderInvaders to the update method:

update() {
    ...
    if (this.state.gameState === GameState.Playing) {
         ...
        this.renderInvaders(this.state);
    }
  }

That’s it! Re-run the application and you should now see a bunch of red invader-spaceships slowly moving down towards the player. They won’t interact with each other yet, but we will work on that in the next part.
Again, Thank you for reading this article 🙂 If you have any questions, problems or feedback, please let me know in the comments.