Creando la lógica del juego
14. Lógica del Juego
En este punto comenzamos a incluir la lógica de nuestro juego, primero, vamos a agregar los eventos de click sobre el componente ActionKey a cada uno de los ovnis vacíos de la parte inferior de la pantalla, estos ovnis, dispararán una onda que chocará contra el enemigo, eliminandolo de la pantalla y sumando un punto, si el enemigo alcanza la parte inferior de la pantalla, un punto de vida será reducido de nuestro medidor.
Vamos al archivo ActionKey.js, eliminamos el código anterior y pegamos el siguiente:
import React, {useRef, useState, useEffect, useContext, Fragment} from 'react';
import anime from 'animejs/lib/anime.es.js';
import { AliensContext } from '../context/context';
import Laser from './Laser';
function ActionKey({id, laserId, image, enemyId, enemyImage}) {
const elementRef = useRef(null);
const enemyRef = useRef(null);
const {
shoot,
setShoot,
enemyActive,
activePlayers,
setActivePlayers,
currentSpeed,
setCurrentSpeed,
setCurrentLife,
currentPoints,
setCurrentPoints,
setEnemyActive
} = useContext(AliensContext);
const [animation, setAnimation] = useState(null);
const shootEnemy = (e) => {
if (animation) {
if (animation.direction === 'normal') {
animation.reverse();
setCurrentPoints(currentPoints+1);
setShoot({id: laserId, hit: true});
// Increase difficulty each 5 points
if (currentPoints % 5 === 0) {
setCurrentSpeed(currentSpeed-500);
}
}
}
}
/**
* Sets array with active players only
* @param {object} player
*/
const setShootInContext = (player) => {
activePlayers.forEach((item) => {
if (item.id === player) {
item.state = false;
}
});
setActivePlayers(activePlayers);
}
const updateLife = (lifes) => {
setCurrentLife(lifes);
}
const animatePlayer = () => {
anime({
targets: elementRef.current,
translateY: [0, 100], // from 100 to 250
delay: 100,
direction: 'forward',
});
}
const setEnemyAnimation = () => {
let anim = anime({
targets: enemyRef.current,
translateY: window.innerHeight - 280,
duration: currentSpeed,
update: function(anim) {
const enemyPosition = enemyRef.current.getBoundingClientRect().top +
enemyRef.current.getBoundingClientRect().height;
const playerPosition = elementRef.current.getBoundingClientRect().top;
if (enemyPosition > playerPosition) {
setShootInContext(elementRef.current.id);
anim.pause();
animatePlayer();
}
}
});
setAnimation(anim);
}
useEffect(() => {
const totalActive = activePlayers.filter((item) => item.state);
updateLife(totalActive.length);
});
useEffect(() => {
if (enemyActive === enemyId) {
setEnemyAnimation();
setEnemyActive(null);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [enemyActive]);
return (
<Fragment>
<div id={enemyId} ref={enemyRef} className="enemy">
<img src={enemyImage} alt='enemy'/>
</div>
<div id={id} ref={elementRef} className="action-key"
onTouchEnd={shootEnemy} onClick={shootEnemy}>
<img src={image} alt='key'/>
<Laser id={laserId} />
</div>
</Fragment>
);
}
export default ActionKey;
En este caso, para las animaciones, estamos usando las funcionalidades de anime las cuales te recomiendo revisar en la documentación oficial
15. Actualizando nuestro archivo Game
En nuestro archivo game.js vamos a actualizarlo con el siguiente código:
import React, { useContext, useEffect } from 'react';
import { AliensContext } from '../context/context';
import Life from './Life';
import Points from './Points';
import ActionKey from './ActionKey';
function Game() {
const {
assets,
currentScene,
setCurrentScene,
currentLife,
setIsRunning,
setEnemyActive,
activePlayers
} = useContext(AliensContext);
const isActive = currentScene === 'game' ? 'active': '';
useEffect(() => {
if (isActive === 'active') {
const interval = setInterval(() => {
randomEnemy();
}, 800);
return () => clearInterval(interval);
}
});
useEffect(() => {
if (currentLife === 0) {
setCurrentScene('gameover');
setIsRunning(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentLife]);
const randomEnemy = () => {
const activeElements = activePlayers.filter(item => item.state);
let randomId = 'enem1';
console.log(activeElements, activeElements.length);
if (activeElements.length > 0) {
const randomEnemy = activeElements[Math.floor(Math.random() * activeElements.length)];
randomId = randomEnemy.id.replace('s', 'enem');
}
setEnemyActive(randomId);
}
return (
<div className={isActive + ' scene game'}>
<div className="game-indicators">
<Points />
<Life />
</div>
<div className="game-container">
<div className="side side1">
<ActionKey
id="s1"
laserId="l1"
image={assets.s1}
enemyImage={assets.enem1}
enemyId="enem1"
/>
</div>
<div className="side side2">
<ActionKey
id="s2"
laserId="l2"
image={assets.s2}
enemyImage={assets.enem2}
enemyId="enem2"
/>
</div>
<div className="side side3">
<ActionKey
id="s3"
laserId="l3"
image={assets.s3}
enemyImage={assets.enem3}
enemyId="enem3"
/>
</div>
<div className="side side4">
<ActionKey
id="s4"
laserId="l4"
image={assets.s4}
enemyImage={assets.enem4}
enemyId="enem4"
/>
</div>
</div>
</div>
);
}
export default Game;
Como puedes ver, se actualizó el llamado a la escena de game over, así mismo, se usa el contexto para la generación randomica de los enemigos, esto lo hacemos generando un ID random, que se compara con los existentes para generar un nuevo enemigo cada cierto intervalo de tiempo.
16. Actualizando nuestro Contexto
Ahora, creamos nuevas variables de estado en nuestro archivo de contexto, para ello vamos a copiar este código en el archivo context.js:
import React, {createContext, useState} from 'react';
import s1 from '../img/s1.png';
import s2 from '../img/s2.png';
import s3 from '../img/s3.png';
import s4 from '../img/s4.png';
import enem1 from '../img/enem1.png';
import enem2 from '../img/enem2.png';
import enem3 from '../img/enem3.png';
import enem4 from '../img/enem4.png';
import logo from '../img/logo.png';
export const AliensContext = createContext();
const AliensProvider = (props) => {
const [assets, setAssets] = useState({
s1,
s2,
s3,
s4,
enem1,
enem2,
enem3,
enem4,
logo
});
const [isRunning, setIsRunning] = useState(false);
const [currentScene, setCurrentScene] = useState('intro');
const [currentLife, setCurrentLife] = useState(4);
const [currentPoints, setCurrentPoints] = useState(0);
const [shoot, setShoot] = useState({});
const [enemyActive, setEnemyActive] = useState(null);
const [currentSpeed, setCurrentSpeed] = useState(8000);
const [activePlayers, setActivePlayers] = useState([
{id: 's1', state: true},
{id: 's2', state: true},
{id: 's3', state: true},
{id: 's4', state: true}]);
return (
<AliensContext.Provider
value={{
assets,
setAssets,
isRunning,
setIsRunning,
currentScene,
setCurrentScene,
currentLife,
setCurrentLife,
currentPoints,
setCurrentPoints,
shoot,
setShoot,
enemyActive,
setEnemyActive,
currentSpeed,
setCurrentSpeed,
activePlayers,
setActivePlayers
}}
>
{props.children}
</AliensContext.Provider>
)
}
export default AliensProvider;
17. Actualizando nuestro archivo App.js
Recuerda que nuestro archivo App.js es la entrada principal de la aplicación, lo vamos a actualizar con el siguiente código:
import React from 'react';
import AliensProvider from './context/context';
import Game from './components/Game';
import Intro from './components/Intro';
import GameOver from './components/GameOver';
function App() {
return (
<AliensProvider>
<div className="container">
<Intro />
<Game />
<GameOver />
</div>
</AliensProvider>
);
}
export default App;
Al final nuestro juego luce así:
Este es un juego simple que sirve como demostración de como con React podemos implementar lógica compleja, combinando animaciones, estados y todo el set de herramientas que nos proporcionan los Hooks.
Para una versión mejorada, podemos implementar niveles, cambios de velocidad, etc, todo esto usando Hooks como useReducer que nos proporciona la misma funcionalidad que Redux, pero ya directamente con el core de React.
Hacer juegos con el DOM requiere manejar un equilibrio con las animaciones, ya que es mucho mas complejo y pesado que hacerlo con canvas, pero tenemos la gran ventaja de ir probando el poder de CSS y toda la funcionalidad y potencia que nos proporciona React.