A game with React? Episode 4: Enemy waves management and other cool improvements.

A game with React? Episode 4: Enemy waves management and other cool improvements.

Hello reaaaaaders!

Today we are taking our game to the next level by adding a few thing to make our "game" look more like a real one.

We have worked on the mechanics of the game and we will soon be improving them, but first let's wrap them in a a nice jewellery case :)

Adding a welcome screen

So first thing I want to do is stop the game from launching itself ruthlessly whenever we load the page. To do that, I'll add a "welcome screen" to the app, where you can view the controls and click on a start button whenever you are ready!

So let's create a new component and call it WelcomeScreen:

import { useDispatch } from 'react-redux'
import '../styles/welcomeScreen.scss'
import boratImage from '../images/borat.png'
import gwbushImage from '../images/gwbush.png'
import vsImage from '../images/vs.png'

const WelcomeScreen = (props) => {
    const dispatch = useDispatch()
    return <div className = "welcome-screen">
        <img  className = "welcome-screen__logo" src = {boratImage}/>
        <img  className = "welcome-screen__logo" src = {vsImage}/>
        <img  className = "welcome-screen__logo" src = {gwbushImage}/>
        <span className = "welcome-screen__text">Earth is under attack! A swarm of space Georges W. Bushes is trying to take over our planet! You are Borat and you were given a spaceship equiped with space rockets, made in Kazakhstan of course, the greatest nation in the world</span>
        <span className = "welcome-screen__text">To move, use keyboard arrows <button className = "welcome-screen__button-demo">{'<'}</button> and <button className = "welcome-screen__button-demo">{'>'}</button></span>
        <span className = "welcome-screen__text">To shoot, use the <button className = "welcome-screen__button-demo">{'SPACE'}</button> key!</span>
        <span className = "welcome-screen__text">If you can save us all, it's nice. I liiiiiike!</span>
        <button name="button" className = "welcome-screen__button-start" onClick = {(e) => {
            dispatch({ type: 'game/start', payload: true})
        }}>Start the game! :)</button>
    </div>
}

export default WelcomeScreen

I've put some stuff in there:

  • A welcome message.
  • Instructions about playing the game.
  • A start button which will set the game isStarted flag to true using dispatch.

For it to work, we need to add a new value inside the reducer's initial state, and create an action to change it when we need to.

const initialState = {
    boratPosition: {'x':'50', 'y':'90'},
    GWBushPosition: {'x':'50', 'y':'50'},
    enemies : [{'id': 0, 'x': '5', 'y':'10', "dead": false},
                {'id': 1,'x': '25', 'y':'10', "dead": false},
                {'id': 2, 'x': '45', 'y':'10', "dead": false},
                {'id': 3,'x': '65', 'y':'10', "dead": false},
                {'id': 4,'x': '85', 'y':'10', "dead": false},
                {'id': 5, 'x': '5', 'y':'20', "dead": false},
                {'id': 6,'x': '25', 'y':'20', "dead": false},
                {'id': 7, 'x': '45', 'y':'20', "dead": false},
                {'id': 8,'x': '65', 'y':'20', "dead": false},
                {'id': 9,'x': '85', 'y':'20', "dead": false}],
    projectiles :[],
    lifeCount: 10,
    killCount: 0, 
    gameStarted: false
}
   // Use the initialState as a default value
   export default function AppReducer(state = initialState, action) {
     // The reducer normally looks at the action type field to decide what happens  
     switch (action.type) {  
        /*some other actions*/
    case 'game/start' : {
        return produce(state, draft => {
            // Modify the draft however you want
            draft.gameStarted = true

        })   
    }

       default:      
       // If this reducer doesn't recognize the action type, or doesn't      
       // care about this specific action, return the existing state unchanged 
       return state 
   }}

And finally we set up a hook in app.js to have the rendering depend on gameStarted Value:

  • If GameStarted is false, the welcome screen will be rendered
  • If it is true, the Game grid will be rendered.
/*some other code*/

if (gameStarted === true) {
  return(
    <div className= "screen">
      <div className = "grid" >
        <MovingItem/>
            {reactElementArrayEnemies}
            {reactElementArrayProjectiles}
        <div className = "counters">
          <LifeCounter/>
          <KillCounter/>
        </div>
      </div>
    </div>
  )
  }
  else {
    return(
      <div className= "screen">
        <WelcomeScreen/>
      </div>
    )
  }

/*some other code*/

And there you have it, behold: the welcome screen.

Screenshot 2021-11-09 at 15-46-02 React Redux App.png

Let's build a wave management system!

Ok so now, we can choose when we start the game, but we are still not able to play it for more than 20 seconds (I mean we can if we fire an insane amount of rockets and slow it down enough, which is possible given the poor performances of the "engine").

So what i wanna do now, is add waves of enemies. I'm thinking simple system here, something like this:

  • Create an array of waves containing arrays of enemies in the store.
  • Whenever an entire wave has been killed, send a new wave of enemies.

So we add a waveCount value in the store, starting at 1. We also define multiple waves of enemies in the enemies.state value. Since this is starting to be a bit a mess, Im going to create a constant.js file where I will setup an export of the enemy waves, then import it into the reducer file:



export const enemyWaves = [[{'id': 0, 'x': '5', 'y':'10', "dead": false},
                {'id': 1,'x': '25', 'y':'10', "dead": false},
                {'id': 2, 'x': '45', 'y':'10', "dead": false},
                {'id': 3,'x': '65', 'y':'10', "dead": false},
                {'id': 4,'x': '85', 'y':'10', "dead": false},
                {'id': 5, 'x': '5', 'y':'20', "dead": false},
                {'id': 6,'x': '25', 'y':'20', "dead": false},
                {'id': 7, 'x': '45', 'y':'20', "dead": false},
                {'id': 8,'x': '65', 'y':'20', "dead": false},
                {'id': 9,'x': '85', 'y':'20', "dead": false}],
                [/*another wave of 10 enemies*/],
                [/*another wave of 10 enemies*/],
                [/*another wave of 10 enemies*/],
                [/*another wave of ten enemies*/]]

I just kept the first one here to show you how the data is formatted.

After that, we just need to update the reducer's initial state declaration with the new export:


import { enemyWaves } from './Constants';

/*then setup initial state*/

const initialState = {
    boratPosition: {'x':'50', 'y':'90'},
    GWBushPosition: {'x':'50', 'y':'50'},
    enemies : enemyWaves,
    projectiles :[],
    lifeCount: 10,
    killCount: 0,
    waveCount: 0,
    gameStarted: false
}

Then, we will obviously get a lot of errors that we need to fix, because in most place where we try to access an enemy in the code, we try to access enemies[index], where we now need to access enemies[waveIndex][enemyIndex] since enemies is now an array of arrays.

And to fix this, we need to be able to reference the wave Index, which will be initialized to 0 and incremented by one each time a wave has been wiped out by the player. You can see in the code above that I added a state.waveCount property, which will provide us with this information wherever we need it.

So what we will do here is change the architecture a bit, we will create a component called GameGrid, and it will be used to render... the game grid. So now, the app.js code will looks like this:

function App() {

        const storeGameStarted = state => state.gameStarted;
        const gameStarted = useSelector(storeGameStarted)

        const storeKillCount = state => state.killCount;
        const killCount = useSelector(storeKillCount)

        const storeWaveCount = state => state.waveCount;
        const waveCount = useSelector(storeWaveCount)

        const storeEnemyCount = state => state.enemies[waveCount].length;
        const enemyCount = useSelector(storeEnemyCount)

        const storeNbOfWaves = state => state.enemies.length;
        const nbOfWaves = useSelector(storeNbOfWaves)

        const dispatch = useDispatch()

        if (gameStarted === true) {
          if (killCount === enemyCount) {
            /*dispatch({ type: 'gameStarted/setValue', payload: false})*/
            if (waveCount != (nbOfWaves - 1)) {
            dispatch({ type: 'wave/add', payload: 1 })  
            dispatch({ type: 'killCount/add', payload: -10})
            }
          }

          if (waveCount != nbOfWaves -1) {
          return(
            <div className = "screen">
              <div className = "counters">
              <LifeCounter/>
              <KillCounter/>
              <WaveCounter/>
            </div>
            <GameGrid/>
            </div>
          )
          } else {
            return(
              <div className= "screen">
                <EndScreen/>
              </div>
            )
          }
        }
        else {
          return(
            <div className= "screen">
              <WelcomeScreen/>
            </div>
          )
        }
}

export default App;

This new App does less, it just handles the counters (I created another counter for waves) to display the game state to the player, and of course it still checks if the game has started to render either the welcome screen or the game grid.

The other thing it does, is that before it renders the GameGrid, it checks wether or not the KillCounter amounts to the number of enemies in the wave. If it does, then we update the state with a new waveCount.

Oh and I also added a simple end screen for when the player defeated the last wave.

Now let's look at the GameGrid Component:

import MovingItem from './MovingItem';
import Enemy from './Enemy';
import { useSelector, useDispatch } from 'react-redux';
import Projectile from './Projectile';


const GameGrid = () => {

    const storeWaveNumber = state => state.waveCount;
    const waveNumber = useSelector(storeWaveNumber)

    const storeEnemies = state => state.enemies;
    const enemies = useSelector(storeEnemies)

    const storeProjectiles = state => state.projectiles;
    const projectiles = useSelector(storeProjectiles)

    const reactElementArrayEnemies = enemies[waveNumber].map(enemy => {
    return <Enemy id = {enemy.id}/>
    })

    console.log(projectiles)

    let reactElementArrayProjectiles = [];

    if (projectiles.length > 0) {
        reactElementArrayProjectiles = projectiles.map(projectile => {
            if (projectile.dead === false) {
            return <Projectile id = {projectile.id}/>
            }
          })

    }    

    return(
          <div className = "grid" >
            <MovingItem/>
                {reactElementArrayEnemies}
                {reactElementArrayProjectiles}
          </div>

      )
}
export default GameGrid

See what we're doing here? First we set up two hooks on state values. The first is set on state.wavecount to get the current wave index. The second one gets the enemies array. Now we can now reference enemies[waveIndex] in the component, and spawn the enemies of the current wave. You can also see that in the grid rendering, I check if projectiles are flagged as dead or not. If they are, I just do not render them (Trying to improve the bad performance a little here :p)

There we go, we can now play until all the waves we declared in the initial state have been killed.

What I've done is I've changed the enemy component a little bit so I'm going to show it to you just below here:

/some other code/

if (currentEnemy.y < 100) {
            timeOutReference =  setTimeout(() => 
            dispatch({ type: 'enemy/descend', payload: 
            {'waveNumber': waveNumber, 'position': (parseInt(currentEnemy.y)+1).toString(), 'id': props.id }})
            , 100)
        } else {
            dispatch({ type: 'lifeCount/decrease', payload: true})
            dispatch({ type: 'killCount/add', payload: 1})
            dispatch({ type: 'enemy/setDead', payload: {'waveNumber': waveNumber, 'dead': true, 'id': props.id }})

        }
/*some more code*/

See? now we check if the enemy has reached the bottom of the screen. It it did, then we decrease lifeCount. For the moment, we still increase killCount, but we could count those death in a specific state value if we wanted.

Then in App.js we setup a hook on lifeCount, and whenever it reaches 0 (Meaning the player is dead), we render the endScreen. Now we just need to tweak the endscreen a little bit so it is able to return either a victory or a death message.

const EndScreen = (props) => {
    const dispatch = useDispatch()

    const storeLifeCount = state => state.lifeCount;
    const lifeCount = useSelector(storeLifeCount);

    if (lifeCount > 0) {
        return <div className = "welcome-screen">
            <img  className = "welcome-screen__logo welcome-screen__logo--big" src = {boratImage}/>
            <span className = "welcome-screen__text">WOW Dude. You saved the earth. Now humans can kill each other in peace and harmony</span>
            <button name="button" className = "welcome-screen__button-start" onClick = {(e) => {
                /*dispatch({ type: 'state/reset', payload: "" })*/
            }}>SAVE THE EARTH!</button>
        </div>
    }
    else {
        return <div className = "welcome-screen">
            <img  className = "welcome-screen__logo welcome-screen__logo--big" src = {youLoseImage}/>
            <span className = "welcome-screen__text">WOW Dude. You suck. Everyone's dead now. Children and everything. Also, it's your fault</span>
        </div>
    }
}

The result:

game.png

Screenshot 2021-11-10 at 16-31-11 React Redux App.png

Screenshot 2021-11-10 at 16-30-24 React Redux App.png

So of course, this is only the beginning as next time we will make our enemies configurable and we will pass them properties in the initialstate so they have different behaviors: some may descend faster, some may move to the side as well, some even throwing weapon, they may be bigger, have more health and so on.

And, also of course but I feel obligated to say it again: I'm freewheeling here and I'm quite sure this is NOT the right approach to making any kind of real time game using react. I just think it is a fun project to play with redux and I enjoy doing it and writing about it.

As usual, you'll find the code on the github repository, on the episode_4 branch here:

github.com/ClementBenezech/the-react-game-e..

And also you can always test the lastest version of the game here:

clementbenezech.github.io/grid-experiment