A game with react? Episode 2: collision management

A game with react? Episode 2: collision management

Okay, so last time, we built a grid and added a component able to move on said grid following keyboard input from the user. If you did not read the first blogpost, go here first, might be worth it:

clementbenezech.hashnode.dev/hooksa-game-wi..

So today, we are going to create another very simple component which will be a fixed element on our screen, the GWBush. The GWBush is a very toxic bush for our hero from Kazakhstan , and whenever he touches one, he loses a life. This is lone of the most basic game mechanics: touch something, get hurt :)

So we need a few things here:

  • The fixed component it self, which will be very easy because it does pretty much nothing.

  • A way to check if two components are at the same coordinates.

  • A life counter that we can display on screen and that will decrement when Borat touches the bush.

The component looks like that:

import '../styles/gwBush.scss'
import { useState } from 'react'

const FixedItem = (props) => {

    //Creating hooks for xPosition and yPosition: this will allow the component to re-render whenever coordinates change (not really useful for now)
    const [xPosition, setXPosition] = useState(props.xPosition);
    const [yPosition, setYPosition] = useState(props.yPosition);

    //Returning the square element   
    return <div className = "gwbush" style = {{top:yPosition + "vh", left: xPosition + "vh"}}></div>
}
export default FixedItem

It works the same way as the MovingItem, but it does not move. Anyways, now we have a Borat and we can run around the GWBush.

Screenshot 2021-11-05 at 17-32-52 React Redux App.png

Communication is the key

So we've got 2 components with each the knowledge of their own positioning, however they have no clue about where each other is. And this is problematic for us as we want Borat to be able to know if he just collided with a GWBush. And this is where Redux will start coming handy, because it will allow us to maintain a global state every component can read and update.

First we need to create a store for our app, where we can save the state. Call it Store.js, put it with the other components.

import { createStore } from 'redux'
import AppReducer from "./AppReducer"

//Creating the store
let store = createStore(AppReducer);
//checking initial value
console.log('Initial state: ', store.getState());

export default store

Now we create the AppReducer. This is the file where we define the different types of actions a component can have regarding the Redux State. Let's just create it like that for the moment:

const initialState = {
    boratPosition: {'x':'50', 'y':'50'},
    GWBushPosition: {'x':'50', 'y':'50'}
}
   // 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) {  
         case 'api/doSomething' : {
           return {
             ...state,
             something : action.something
           }
         }


       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 
   }}

We'll define the actions later on, but first, thanks to the reducer, we now have default values for our state and we are one step away from making it available to the components, To do so, we need to use a to wrap our app so it can have access to the store. It goes like this, in index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App';
import store from './components/Store';
import { Provider } from 'react-redux';

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);

Now, any of our components can subscribe to the store and by doing this, it instantly knows if a property has changed in the state and even better, can update itself!

Making Moving component "store based"

We will modify our MovingObject component, the mobile one, to use the coordinates we have in the global state instead of its own. To do that, we will substitute our useState hook by another one, the useSelector hook. It will allow us to read the state value we are interested in - the GWBush coordinates - and refresh whenever this value changes.

The question is, how will we make it move around? How can we update the global state?

This is the moment where we need to define some actions for our state. First we'll define two actions:

  • One that sets a new value for MovingObject.x
  • One that sets a new value for MovingObject.y

So let's go back to the reducer and define those two actions:

const initialState = {
    boratPosition: {'x':'50', 'y':'50'},
    GWBushPosition: {'x':'50', 'y':'50'}
}
   // 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) {  
         case 'borat/putXPosition' : {
           return {
                ...state,
                    boratPosition: {'x':action.payload, 'y': state.boratPosition.y}
            }
        }
        case 'borat/putYPosition' : {
          return {
               ...state,
                   boratPosition: {'x':state.boratPosition.x, 'y': action.payload}
           }
       }
       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 
   }}

Each time, we update the state with a new value for boratPosition, with the new x or y value.

We just need to update MovingItem.jsx and use useDispatch to "call" the reducer's actions and update the item position.

import '../styles/fancySquare.scss'
import { useSelector, useDispatch } from 'react-redux';
import { useRef, useEffect } from 'react';

const MovingItem = (props) => {

    const dispatch = useDispatch()

    //Creating the reference we'll assign to the square.
    const inputRef = useRef();

    //Adding behavior on render: give focus to the square.
    useEffect(() => {
        inputRef.current.focus();
      })

    //Setting Hook on global state position for this object
    const currentPosition = state => state.boratPosition;
    const storePosition = useSelector(currentPosition)

    //handling position changing scenarios in a function

    const moveAround = (key) => {
        if (key === "ArrowDown") {
            if (storePosition.y <= 89) {
                dispatch({ type: 'borat/putYPosition', payload: parseInt(storePosition.y)+1})
            }
        } else if ( key === "ArrowUp") {
            if (storePosition.y >= 1) {
                dispatch({ type: 'borat/putYPosition', payload: parseInt(storePosition.y)-1})
                }
        } else if ( key === "ArrowLeft") {
            if (storePosition.x >= 1) {
                dispatch({ type: 'borat/putXPosition', payload: parseInt(storePosition.x)-1})
            }
        } else if ( key === "ArrowRight") {
            if (storePosition.x <= 89) {
                dispatch({ type: 'borat/putXPosition', payload: parseInt(storePosition.x)+1})
            }
        }
    }

    //Returning the square element   
    return <div tabIndex = '0' className = "fancy-square" style = {{top:storePosition.y + "vh", left: storePosition.x + "vh"}} onKeyDown = { e => {
            e.preventDefault();  
            moveAround(e.key)
    }} ref={inputRef}></div>
}
export default MovingItem

Now our component moves around, and its position is stored in the global state.

Maybe we could make every fixed object listen to the moving object's position through the store, and then compare it with its own position. I like this approach, so we are going to do that. For now. Remember I'm not really sure where I'm going here.

For now, we'll just add a red border around the GWBush whenever Borat is at its coordinates.

Making Fixed component able to tell there is a collision, and take action

So, let's make some changes in the FixedComponent

First, we need to add a hook to be able to access the value of the Moving Object coordinates. Then, we need to assign a reference to GWBush so we can target it inside useEffect and alter its className when we want the red border to appear.

In useEffect, we test if coordinates match, and we define the corresponding className depending on the result.

import '../styles/gwBush.scss'

//Importing useSelector hook to be able to "listen" to the Redux store
import { useSelector, useDispatch } from "react-redux"
import { useEffect } from 'react'
import {useState} from 'react'
import {useRef} from 'react'


const FixedItem = (props) => {

    const storePositionX = props.xPosition;
    const storePositionY = props.yPosition;

    //Setting Hook on global state position for the MovingObject
    const currentMovingObjectPosition = state => state.boratPosition;
    const storeMovingObjectPosition = useSelector(currentMovingObjectPosition)

    //Creating the reference we'll assign to the square.
    const inputRef = useRef();

    useEffect(() => {
        //Checking if the coordinates match to define the right styling
        if ((storeMovingObjectPosition.x == storePositionX) && (storeMovingObjectPosition.y == storePositionY)) {
            inputRef.current.className = "gwbush gwbush--contact"
        } else {
            inputRef.current.className = "gwbush"
        }
      })
      return (
            <div className = "gwbush" style = {{top:storePositionY + "vh", left: storePositionX + "vh"}} ref={inputRef}></div> 
      )
    }


export default FixedItem

"Et voilà!"

Animation.gif

Our GWBush is now aware whenever he share his position on the grid with Borat, and tells us so by adding a red border on himself. We can even add multiple Bushes! For example this:

<MovingItem xPosition = {50} yPosition = {50}/>
      <FixedItem xPosition = {30} yPosition = {30}/>
      <FixedItem xPosition = {40} yPosition = {70}/>
      <FixedItem xPosition = {20} yPosition = {45}/>

will render this:

AnimationMultipleBorat.gif

Adding the life counter component

Now, the red border is just there to illustrate, but what we wanted to achieve was to have a life counter that would decrease its value whenever Borat hits a GWBush. To do so, we must:

  • Add a "lifeCount" value in the Global State.
  • Create a simple component displaying that value using a useSelector hook.
  • In the GWBush (fixed component), update the life count in store whenever the Borat is on the bush.

So we add this in the initial state definition in the reducer:

lifeCount: 10

Then we create the life counter and set up the hook on state.lifeCount:

import '../styles/lifeCounter.scss'
import { useSelector } from "react-redux"

const LifeCounter = () => {

        //Setting Hook on life counter
        const lifeCount = state => state.lifeCount;
        const storeLifeCount = useSelector(lifeCount)


    if (storeLifeCount > 0) {
        return <div className = "life-counter">
            <i class="fas fa-heart life-counter__icon"></i>
            <div className = "life-counter__value">{storeLifeCount}</div>
        </div>
    } else {
        return <div className = "life-counter">Game Over Man!</div>
    }
}
export default LifeCounter

If life Counter is > 0, it will render the life count and the life icon. When it reaches 0, it will render a game over message.

You can initialize the component inside the grid for now, it does not matter, well work on the interface another time.

Then, we need to add a new action in the reducer, one that decreases the value of state.lifeCount by one:

case 'lifeCount/decrease' : {
        return {
             ...state,
                 lifeCount: state.lifeCount -1
         }
     }

And then we "cast" this action from the Fixed object whenever the moving object collides, using useDispatch. The new useEffect definition for this component now looks like this:

useEffect(() => {
        //Checking if the coordinates match to define the right styling
        if ((storeMovingObjectPosition.x == storePositionX) && (storeMovingObjectPosition.y == storePositionY)) {
            inputRef.current.className = "gwbush gwbush--contact"
            dispatch({ type: 'lifeCount/decrease', payload: null})
        } else {
            inputRef.current.className = "gwbush"
        }
      })

Let's have a look at the outcome:

game over man.gif

Great Success! Now we have:

  • A user controlled object that can move around and whose position is globally available through redux.
  • A fixed object with the ability to decrease the life count whenever he is encountered by the moving object.
  • A life count component to display remaining lives, and a game over message when they are depleted!

All sources are available on gitHub if you want to take a look / fork the repo to play around with it:

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

I'm thinking maybe next episode, I will evolve the fixed item to get it moving again, but on it's own. Scaaaaaaaary :) Maybe with some kind of basic AI so it moves in the direction of the player (can I even use that word yet? :p ). Just writing that, I feel I might be a bit ambitious... Let's just get it moving, and see what we can do from there :) Maybe also work on some kind of "hitbox" management so the size of each item is taken into account by the "collision detection" mechanism.

See you next time, hope you enjoyed the read!