Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML5

Game Programming using JavaScript, React, Canvas2D and CSS – Part 2

4.00/5 (3 votes)
22 Dec 2017CPOL6 min read 5.9K  
How to 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.

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:

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

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

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

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

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

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

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

JavaScript
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 to user input:

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

JavaScript
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 its 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 ship's 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:

JavaScript
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 Ship's render method:

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

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

JavaScript
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 positions 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, let's add the missing calls to our ship's update and render methods. Let’s take a look at the adjusted update method:

JavaScript
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 ship's update and render methods here.

Finally, take a look at the clearBackground method, which cleans up the canvas before draw new content onto it:

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

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

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

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

JavaScript
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 invader's 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:

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

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

License

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