A game with React? Episode 3: Long Distance weapons!
Let's try killing stuff from afar
Hey there! Welcome to this new episode of this fabulous series about maybe making a some kind of game using react functional components and hooks., because why not.
My objective today is to create some kind of game mechanics with our existing elements, because obviously just having fixed objects around the screen that will kill you if you touch them isn't quite the exact definition of a fun experience.
So I went and played around a little bit and I decided I wanted to try something mocking the style of the good old classic Space Invaders.
So I want our Borat Saucer to be positioned a the bottom of the screen, and to be able to move right and left. This, we have already implemented.
I also want the fixed objects to be able to move torwards the bottom of the screen, slowly approaching the players position.
And of course, I want the Borat saucer to be able to send some kind of projectiles towards the top of the screen.
Finally, I want a way for the projectiles and the enemies to detect collisions, so projectiles can kill enemies.
So to be honest, as I'm writing this, I'm not quite sure how I will do it, however, I got some ideas :)
Let's improve previous code a Bit: better collisions and death animation.
Ok so, the first thing I wanna do here is handle the Hitbox issue. Because for now, items are considered "collided" only when their exact coordinates match, which does not feel natural at all because elements on screen overlap way before the collision is detected. And also it is hard to collect stuff or hit something. It is a pain in the ass really.
To do that, we just need to change the condition inside fixedObject. Instead of checking if X and Y coordinates match, we just calculate the difference between the two X coordinates and the two Y coordinates. The we check the absolute value of that, and we want it to be equal to a number which will actually define the size of the Hitbox. The higher, the bigger. This is what we do:
if ((Math.abs(storeMovingObjectPosition.x - storePositionX) <=5)
&& (Math.abs(storeMovingObjectPosition.y - storePositionY)<=5)) {
/*do stuff related to the collision*/
}
Math.abs() provides us with the absolute value of the difference. Now our objects will collide whenever both their coordinates are inside a 5 unit range of another object.
We will also add a hook in there, named haveBeenKilled, with useState. This hook is going to be defined with a "false" value at first when initialized. Whenever the fixed object collides with something, we will set this hook to true.
Then inside useEffect, we check the value of this hook, and if it equals true, then we apply a new className to make the object explode then disappear, and we stop checking for further collisions. So once it explodes, the object is still here, but "inactive" since it will not test its position against the player's. And also invisible.
Check the new FixedItem component:
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) => {
//Setting Hook on global state position for this object
//const currentPosition = state => state.GWBushPosition;
//const storePosition = useSelector(currentPosition)
const dispatch = useDispatch();
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)
//Setting up Hook for havBeenKilledFlag
const [haveBeenKilled, setHaveBeenKilled] = useState(false);
//Creating the reference we'll assign to the square.
const inputRef = useRef();
useEffect(() => {
if (haveBeenKilled) {
inputRef.current.className = "gwbush gwbush--dead"
} else {
//Checking if the coordinates match to define the right styling
if ((Math.abs(storeMovingObjectPosition.x - storePositionX) <=5) && (Math.abs(storeMovingObjectPosition.y - storePositionY)<=5)) {
dispatch({ type: 'lifeCount/decrease', payload: null})
setHaveBeenKilled(true)
} else {
inputRef.current.className = "gwbush"
}
}
})
return (
<div className = "gwbush" style = {{top:storePositionY + "vh", left: storePositionX + "vh"}} ref={inputRef}></div>
)
}
export default FixedItem
Pretty neat hey?
So now, whenever there is a collision, the enemy disappears. However, we would like to make is slowly fade away just after it violently exploded. So how do we do that?
CSS animation is the key(frames)
This is how we do it, it is very very simple animation work, let's do some changes in the fixedItem.scss file:
@keyframes fade-out {
0% {
opacity:1;
}
100% {
opacity:0;
}
}
.gwbush {
height: 10%;
width: 10%;
background-color:black;
position: absolute;
background: url("/images/gwbush.png");
background-size:contain;
background-repeat:no-repeat;
background-position:center;
&:focus {
outline:none;
}
&--dead {
background: url("/images/fire-explosion.png");
background-size:contain;
animation: fade-out 2s linear forwards;
}
}
So we define a new background for the dead bush so now it is a raging explosion, and we make it fade out using CSS animation. We just need to define keyframes making opacity of the element go from 1 to zero, then using animation, we play it and give it a duration (2s here) and a behavior (Linear, the fade will be progressive, and forwards so the end of the animation becomes the new element styling, so after it fades out, it stays invisible.
There we go. Now we just have to create a pack of GWBushes on the screen like this:
/**IN APP.JS**/
return(
<div className = "grid" >
<MovingItem/>
<FixedItem xPosition = {5} yPosition = {10}/>
<FixedItem xPosition = {25} yPosition = {10}/>
<FixedItem xPosition = {45} yPosition = {10}/>
<FixedItem xPosition = {65} yPosition = {10}/>
<FixedItem xPosition = {85} yPosition = {10}/>
<FixedItem xPosition = {5} yPosition = {20}/>
<FixedItem xPosition = {25} yPosition = {20}/>
<FixedItem xPosition = {45} yPosition = {20}/>
<FixedItem xPosition = {65} yPosition = {20}/>
<FixedItem xPosition = {85} yPosition = {20}/>
<LifeCounter/>
</div>
)
And our game kind of looks like this now:
How do you like that? :D
From "Lame" to "Meh", Moving and shooting
Because you know, for now, this still sucks. I mean, Ok, we can go fight the invaders, but they don't move, and also we'll kill ourselves because they hurt us when we kill them. Also they don't move, so we need to adress that, and of course, we need to be able to shoot them
To the moving part!
Ok so first, let's make our FixedItems enemies. I will rename the file and component Enemies.jsx from now on because it does make more sense.
This part is actually pretty easy. We just need to create hooks on positionX and positionY (Position X as well because later we might want to have the enemies move to the side as well) and then, inside use effect, we increase the value of positionY every 500ms with setTimeOut.
The new useEffect will look like this:
/**hooks**/
const [PositionX, setPositionX] = useState(props.xPosition);
const [PositionY, setPositionY] = useState(props.yPosition);
/**hooks*//
useEffect(() => {
if (haveBeenKilled) {
inputRef.current.className = "gwbush gwbush--dead"
} else {
//Checking if the coordinates match to define the right styling
if ((Math.abs(storeMovingObjectPosition.x - PositionX) <=5) && (Math.abs(storeMovingObjectPosition.y - PositionY)<=5)) {
dispatch({ type: 'lifeCount/decrease', payload: null})
setHaveBeenKilled(true)
} else {
inputRef.current.className = "gwbush"
}
}
if (PositionY < 100) {
setTimeout(() => {
setPositionY(PositionY+1)}
, 100)
}
Which gives us this result:
The shooting part
So now we wanna be able to shoot the GWBushes from afar. Which mean, our projectile need to be able to access the enemies position so it knows when it hits something. And at the moment, we are not able to do that as the enemies make use of local state to store their position.
So first thing, we need to remedy to that and store all enemy positions in the store.
Ten hours later...
Wow, I did not think it was going such a PAIN to just use Redux to update the enemies coordinates. The method I used to move the BoratSaucer would not work and I have been running in circles for literally hours. I mean. Really. Hours.
I wanted my objects to be in an array in the store, so I could easily generate them using array.map in the app. But this made updating the state really tedious, because I could not find a clean way to update only the current enemy when calling the reducer, and everything I tried led either to weird behaviors, or just app crashes. So I was a little pissed of course, and went browsing the mighty internet.
I found that I was not the only one having issues, and that solutions existed to actually be able to write immutable calls in the form of a normal variable value assignment. But now that I found a way to keep going, let explain what I modified so far.
So first I needed to modify the the enemy.jsx component, so I could use the new array I just added in initialState:
const initialState = {
boratPosition: {'x':'50', 'y':'90'},
GWBushPosition: {'x':'50', 'y':'50'},
enemies : [ {'id': 0, 'x': '5', 'y':'10'},
{'id': 1,'x': '25', 'y':'10'},
{'id': 2, 'x': '45', 'y':'10'},
{'id': 3,'x': '65', 'y':'10'},
{'id': 4,'x': '85', 'y':'10'}],
lifeCount: 10
}
So let's add redux support and setup hooks in enemy.jsx component. It is pretty simple, nothing we've not done before, set up hooks on coordinates, and add a call to action using useDispatch.
const Enemy = (props) => {
//Redux Hook
const storePositionX = state => state.enemies[props.id].x;
const positionX = useSelector(storePositionX)
//Redux Hook
const storePositionY = state => state.enemies[props.id].y;
const positionY = useSelector(storePositionY)
//some previous code
useEffect(() => {
if (haveBeenKilled) {
inputRef.current.className = "gwbush gwbush--dead"
} else {
//Checking if the coordinates match to define the right styling
if ((Math.abs(storeMovingObjectPosition.x - positionX) <=5) && (Math.abs(storeMovingObjectPosition.y - positionY)<=5)) {
dispatch({ type: 'lifeCount/decrease', payload: null})
setHaveBeenKilled(true)
} else {
inputRef.current.className = "gwbush"
}
}
if (positionY < 100) {
setTimeout(() => {
dispatch({ type: 'enemy/descend', payload: {'position': (parseInt(positionY) + 2).toString(), 'id': props.id }})
}
, 200)
}
})
return (
<div className = "gwbush" style = {{top:positionY + "vh", left: positionX + "vh"}} ref={inputRef}></div>
)
}
export default Enemy
So the enemy will now call the reducer to action enemy/descend every 200ms. It updates its position, making it re-render.
Then, in app.js, we just generate the enemies on screen:
const reactElementArray = enemies.map(enemy => {
return <Enemy id = {enemy.id}/>
})
return(
<div className = "grid" >
<MovingItem/>
{reactElementArray}
<LifeCounter/>
</div>
)
}
export default App;
And finally, we need to write the reducer itself. Now, I went in there loaded with confidence, but then I stumbled upon some very annoying behavior. You see, as long as I was trying to update only a single object from the state, everything worked fine. But when I stated putting my enemy position in an enemies[] array in the state, the behavior would act crazy.
The things here is, when we update the state, we are supposed to update it fully. Which mean each reducer actually has to return the full state, so we have to duplicate it and then with the spread operator, we modify the specific value we want to update, then we return it.
We cannot, say, just update an enemy Y position just by reaffecting it a new value directly, like this. I mean, we can. But it should not be done and will probably create weird behaviors:
state.enemies[someIndex].y = someNewValue;
But I just couldn't find a way to make it work. Best shot I gave it, I would be able to update the state, but my Array in the state would then get prototype object instead of array, I mean really, let's be honest and acknowledge that I probably could have pulled it with a deeper understanding of the language's specifics.
Anyways, i looked around and found a solution: Immer can be installed in the form of a npm package, and gives you ONE export to use: produce.
Now, what it does for you is really GREAT because it kind of abstracts the whole "immutability constraints layer" for you and you can just write some good old data changes as you would with normal variables.
To install Immer in your project: immerjs.github.io/immer/installation
Which mean, now that i installed immer and just imported produce in my reducer, I can do that:
import produce from 'immer'
/*then , define an action updating a specific property inside state. */
case 'enemy/descend' : {
return produce(state, draft => {
// Modify the draft however you want
draft.enemies[action.payload.id].y = action.payload.position;
})
}
As of now, we just have to wrap the data mutation inside a produce statement, and write a "normal" data update. It takes the state, and returns draft, which is a new state generated from the current one and taking your changes into account. Simple as that :)
Annnnnnd now we've got a working version of our objects, able to descend slowly towards the bottom of the screen. So our next steps are:
Duplicate our enemy component to create a projectile component. We will give it roughly the same behavior. It checks its position against enemies. Whenever it collides with an enemy, it disappears.
Make the enemy component detect collision with projectiles instead of the player, so they explode on impact when shot at.
Find a way for the BoratSaucer to shoot those projectiles on demand with a keypress.
So for the modifications above, nothing really new to implement:
Create a projectiles[] array in the state. This will store Id and position of current projectiles.
In enemy.jsx, we set up a hook on the projectiles array to be able to reference its values, and modify the collision code so it is based on these new coordinates instead of the player position.
We create a projectile.js component, then copy paste the enemy code in there. Now we just have to alter it a little bit. It is basically the same component, but it will use another action to move: "projectile/elevate". You can look up its code on the Github repository, there is no point describing it extensively as it is exactly the same logic.
Now we have missiles shooting down invaders :)
So finally, let's make it fire. The behavior I'm going for is this one:
Add a scenario in the "keypress manager" I made in the movingObject. When the user presses the "space" key, a new projectile is fired.
To "fire" the projectile, I will use the redux state: whenever the space key is pressed, an action from the reducer is performed, and this action will be to push a new projectile inside the array.
Projectiles are already rendered in APP based on the state so I won't touch this this might as well work just out of the box :)
Let's try this! New keypress Manager:
const moveAround = (key) => {
if (/*some key*/) {
/*do some stuff*/
}
/*** other stuff***/
} else if ( key === " ") {
dispatch({ type: 'projectile/spawn', payload: parseInt(storePosition.x)})
}
}
New action to add projectile to global state (With the two other projectile/enemy related actions):
case 'enemy/descend' : {
return produce(state, draft => {
// Modify the draft however you want
draft.enemies[action.payload.id].y = action.payload.position;
})
}
case 'projectile/elevate' : {
return produce(state, draft => {
// Modify the draft however you want
draft.projectiles[action.payload.id].y = action.payload.position;
})
}
case 'projectile/spawn' : {
return produce(state, draft => {
// Modify the draft however you want
console.log(draft.projectiles.length)
draft.projectiles.push({'id': draft.projectiles.length, 'x' : action.payload, 'y' : "90" })
})
}
And threre we go. Just define state.projectiles[] as a blank array in the initial state, and start popping out some heavy artillery :p
After that, I dis some code improvements to handle collisions better, make sure dead enemies would not trigger missile explosions any more and so on by adding a "dead" flag for enemies in the state and moving around little bits of code, I also added a Killcounter next to the life counter. So feel free to browse the github branch from this episode to see the last version of full code.
Hope you enjoyed :)
Episode Branch is in the Repository HERE : github.com/ClementBenezech/the-react-game-e..
And also you can test the "game" here:
clementbenezech.github.io/grid-experiment
PS: I updated the repo with an update due to performance issues. I mean, dont' get me wrong I think this approach is bound to deliver terrible performances, however I thought I might change one thing: Instead of having the projectile listen to the enemies to see if it collides, we just have the enemy dispatch a "dead" flag set to true to the projectile he just collided with. This way we can avoid having the projectiles listen to the enemies and still keep the same behavior.
I also changed the behavior a little bit, dead enemies will disappear while projectile will explose.