A game with react? Episode 5 : Creating enemy Prototypes and behaviors

A game with react? Episode 5 : Creating enemy Prototypes and behaviors

Hello! Welcome to episode 5 of "let's make a game with react because why not?", a fun hook based space adventure!

Let's take it back where we were. we had created an enemy wave management system and implemented multiple waves of enemies, but they were all the same apart form the coloring, and this is what we are going to fix today!

Let's create our evil opponents from space. I want 4 of them for this episode, which should be more than enough work for one episode, and then if we want we can always add some more properties and prototypes. Now these first 4 prototypes are going to be defined as a literal object with 5 properties:

  • A speed multiplier, defining the speed at which it moves.
  • A health count, to know how many lives it has.
  • A className so it can render itself according to its type looks
  • A boolean Flag shootsFromAfar, to know if it can access the long distance weapon part of the enemy routine.
  • The size, which we will use to determine the size of the hitbox later maybe

We'll only use the first four for now, but might use the size later :)

So, this is how it looks when ready (EnemyPrototype.js

//This exports a single function taking enemyType (string) as a parameter and returning an enemyTypeObject
//Thils will be used to define the prorperties of each enemy in constants.js

export const enemyPrototypes = (enemyType) => {
    switch (enemyType) {
        /*Standard enemy. Just descends slowly*/
        case 'regular':
            return (
                {   'size': 5,
                    'speed': 1,
                    'health': 1, 
                    'className': "regular",
                    'attacksFromADistance': false
                }
            )
            case 'fast':
            /*As strong, but twice as fast */
            return (
                {   'size': 5,
                    'speed': 2,
                    'health': 1, 
                    'className': "fast",
                    'attacksFromADistance': false
                }
            )
            case 'heavy':
            //Normal speed, triple health//
            return (
                {   'size': 5,
                    'speed': 1,
                    'health': 3, 
                    'className': "heavy",
                    'attacksFromADistance': false
                }
            )
            case 'shooter':
            //Can shoot projectiles!
            return (
                {   'size': 5,
                    'speed': 1,
                    'health': 1, 
                    'className': "shooter",
                    'attacksFromADistance': true
                }
            )
        }
    }

and then in constants.js, we import enemyPrototypes and use it to define each enemy properties in our initialState like this:

import { enemyPrototypes } from './EnemyPrototypes';

export const initialState = {
    boratPosition: {'x':'50', 'y':'90'},
    GWBushPosition: {'x':'50', 'y':'50'},
    enemies : [[{'id': 0, 'x': '5', 'y':'10', "dead": false, "enemyType" : enemyPrototypes("regular")},
    {'id': 1,'x': '25', 'y':'10', "dead": false, "enemyType" : enemyPrototypes("shooter")},
    {'id': 2, 'x': '45', 'y':'10', "dead": false, "enemyType" : enemyPrototypes("heavy")},
   /*blah blah blah other enemies*/
    {'id': 9,'x': '85', 'y':'20', "dead": false}]],
    projectiles :[],
    lifeCount: 3,
    killCount: 0,
    waveCount: 0,
    gameStarted: false
}

Then of course, the structure of our element has changes but we have merely added a new property so it does not impact previous code too much.

Now let's update the enemy's component code so we actually make use of those. First, we change the way we assign the className both in initial render and useEffect.

/*In useEffect*/
inputRef.current.className = "enemy enemy--"+currentEnemy.enemyType.className

/*in Initial render*/
return (
<div className = {"enemy enemy--"+currentEnemy.enemyType.className} 
style = {{top:currentEnemy.y + "vh", left: currentEnemy.x + "vh"}} 
ref={inputRef}></div> 
      )

Now, we need to add the enemyType speed multiplier when the enemy moves:

dispatch({ type: 'enemy/descend', payload: 
            {'waveNumber': waveNumber, 
               'position':  parseInt(currentEnemy.y)+1*currentEnemy.enemyType.speed).toString(),
               'id': props.id }})
            , 100)

Then, to take the lifecount into account, we need to change the way things work a little bit. Now, when a projectile hits an enemy, the enemy won't automatically die, it will just decrement it's enemy.enemyType.heath property and when it reaches 0, then it will die.

I guess by now you have an idea of what we will have to do:

  • Add and action to decrement enemies.enemyType.health when an enemy is hit.
  • Modify the enemy component so it sets itself to state.dead = true dead whenever its health amount (on which we will set a hook too) equals to 0.

Easy enough.

So we've already made use of health, speed, and className, now we've got to take care of the last prototype: the shooter.

What we want to do here is add a new condition in the useEffect() statement, checking is the enemy is of shooter type, and if so, we make it shoot a projectile every once in a while.

It will look like this:

//If the enemy is of the shooting type and the last time it fired was more than 4 seconds ago, we fire. 
            if (currentEnemy.enemyType.attacksFromADistance === true && Date.now() - timeOfLastFire > 4000)  {
                dispatch({ type: 'projectile/spawn', payload: {'positionX' : parseInt(currentEnemy.x), 'positionY' : parseInt(currentEnemy.y), 'type' : "enemy"}})
                setTimeOfLastFire(Date.now())
            }

Every 4 seconds (note that this could also be a property of the enemy type, some might fire rapidly, other slowly, the projectiles could be deal a different amount of damage and so on) we spawn a projectile at the enemy's coordinates if he is of the shooting type. The projectiles are now typed "player or "enemy" and this will modify their behavior accordingly. The projectile will have a different style, and move towards the bottom on the screen so it can maybe hit the player.

Then, we have to make the player listen to the projectiles array in the state so it can tell if it collides. Then it kills the projectile, and decreases the player's Lifecount.

Then we update the projectile component so we can give it two different moving speed and behavior depending on its type ("player" or "enemy"). The enemy projectile moves slower, towards the bottom of the screen, and looks like a can of nuclear waster. The player type projectile is a rocket, pretty fast, going towards the top of the screen. It only deals damage to enemies, whereas the enemy projectile only deals damage to the player.

Since we have enemies with more than one heath point, I wasn't satisfied with the way the projectiles behave. What I would do was, I would check if the projectile was dead in the game grid, and chose to ignore the dead ones for render. So there would jut be a projectile disappearing and no clue about its explosion.

This was fine when enemies died on first impact you know, because it exploded and then we'd just see a massive explosion, no more projectile, and would be pretty sure it was in there anyways, it felt pretty right.

But now we need to be able to display the dead projectile for a short amount of time on screen, so we can play a simple animation of an explosion whenever the projectile goes from "live" to "dead". So look what I did here:

/*inside useEffect in projectile*/
/*some code*/
else if (currentProjectile.dead === true && currentProjectile.timeOfDeath === null) {
            /*We give it the dead class*/
            inputRef.current.className = "projectile--"+currentProjectile.type+" projectile--"+currentProjectile.type+'--dead' 
            /* We set time of death. Whenever the projectile has been dead more than 1 second,
            we'll stop rendering it on the grid (see GameGrid.jsx).*/
            dispatch( {'type': 'projectile/setTimeOfDeath', payload: {'id': props.id , 'timeOfDeath' : Date.now()}})
        }

Whenever the projectile is set to dead but has not been defined a time of death yet, we define this time of death as Date.now(). and in Game Grid, instead of checking it the projectile is dead to decide whether it should be rendered, or not, we actually check if the projectile died more then 1 second ago, like this:

/*Choosing what projectiles to render based on Time of death*/

    let reactElementArrayProjectiles = [];

    if (projectiles.length > 0) {
        reactElementArrayProjectiles = projectiles.map(projectile => {
            if (Date.now() - projectile.timeOfDeath < 1000 || projectile.timeOfDeath === null) {
                return <Projectile id = {projectile.id}/>
            }
          })
    }

Now the projectile explodes on impact, giving us some feedback.

finalepisode5bis.gif

That is going to be all for now, and I'm probably going to leave this on the side for a moment and do some other things, even if there is still a lot we could do here, I think our work is done for now. This has been a good week, and I'm actually very pleasantly surprised I was able to do all this in such a short amount of time and using React Functional components with Redux and minimal external library usage (just immer to handle state updates).

I really hope you enjoyed this, and that I helped you understand what kind of interactions you can set up in there, I mean really all that we used here can be applied to more serious projects, it always basically is the same concept of objects interacting with each other and rendering themselves whenever needed based on local state or Global State value updates.

I've spent some time cleaning and commenting the code of all major components at stake here (Enemy, MovingObject, Projectile, GameGrid, App) and clean the src folder by setting aside the javascript definitions in a src/js/ folder.

Please feel free to browse the repository (branch: episode_5) and look at the codebase.

Also feel free to give me any feedback about what you liked in this series, what you didn't and how I could improve this in the future!

REPOSITORY HERE: github.com/ClementBenezech/the-react-game-e..

PLAY THE GAME HERE: clementbenezech.github.io/grid-experiment

Until we meet again!

Clement.