Dans cet article, je vais vous guider dans la création d’un jeu de serpent en utilisant une application React. Il s’agit d’un simple jeu 2d construit à l’aide de TypeScript, et nous n’aurons pas besoin d’utiliser de bibliothèques graphiques tierces pour le construire.
Voici ce que nous allons réaliser dans ce tutoriel :
Snake est un jeu amusant auquel vous avez peut-être joué sur d’anciens téléphones fonctionnels comme les modèles Nokia 3310.
Le concept est simple : le serpent se déplace à l’intérieur d’une boîte, et lorsqu’il capture un fruit/objet, vos points augmentent et le serpent grandit. Si le serpent touche les limites de la boîte ou entre en collision avec lui-même, le jeu est terminé.
Cet article vous fournira toutes les compétences/étapes nécessaires pour créer votre propre jeu Snake à partir de zéro. Nous allons d’abord examiner les structures de code et leur logique. Puis j’expliquerai comment elles fonctionnent lorsqu’elles sont toutes connectées.
Sans plus attendre, commençons.
Table des matières
Conditions préalables
Avant de commencer à lire cet article, vous devez avoir une compréhension de base des sujets suivants :
Qu’est-ce qu’un jeu de serpent ? Qu’allons-nous utiliser dans ce jeu ?
Un jeu de serpent est un jeu d’arcade dans lequel un serpent se déplace dans une boîte. Votre score augmente en fonction du nombre d’objets/fruits que le serpent mange. Cela augmente également la taille du serpent. S’il entre en collision avec lui-même ou avec la limite de la boîte, le jeu est terminé.
Vous pouvez en savoir plus sur l’histoire ou les origines du jeu en consultant le lien Wiki.
Nous allons utiliser les outils suivants pour construire notre jeu :
- Redux : Pour créer et gérer l’état global de l’application.
- Redux-saga : Un middleware redux que nous utiliserons pour gérer les tâches asynchrones.
- Balise HTML Canvas : Nous l’utiliserons pour dessiner un objet comme un serpent et le fruit.
- React : Bibliothèque UI.
- Chakra-UI : Bibliothèque de composants.
Qu’est-ce que redux ? Pourquoi l’utilisons-nous ?
Redux est un conteneur d’état qui vous aide à créer et à gérer l’état global de votre application. Redux se compose de quelques parties de base comme :
- État global
- Magasin Redux
- Actions et créateurs d’actions
- Réducteurs
Vous pouvez apprendre tout ce qui concerne les sujets ci-dessus et comment Redux fonctionne en interne dans la section de démarrage de la doc Redux.
Nous utilisons la bibliothèque de gestion d’état Redux car elle nous aidera à gérer notre état global d’une manière plus simple. Elle nous permettra d’éviter le prop drilling. Elle nous permettra également d’effectuer des actions asynchrones complexes via un middleware.
Vous pouvez en apprendre plus sur les middleware ici.
Qu’est-ce que redux-saga ? Pourquoi l’utilisons-nous ?
Redux-saga est un middleware qui nous aide à nous intercaler entre l’action dispatchée et le reducer du redux store. Cela nous permet d’effectuer certains effets secondaires entre l’action distribuée et le réducteur, comme la récupération de données, l’écoute d’actions particulières ou la mise en place d’abonnements, la création d’actions, et plus encore.
Redux saga utilise des générateurs et des fonctions de générateur. Une saga typique ressemble à ceci :
function* performAction() {
yield put({
type : COPY_DATA,
payload : "Bonjour"
}) ;
}
performAction
est une fonction génératrice. Cette fonction générateur va exécuter la fonction put
. Elle crée un objet et le renvoie à la saga, en indiquant quel type d’action doit être exécuté avec quelle charge utile. Ensuite, l’appel put
renvoie un descripteur d’objet indiquant quelle saga peut le reprendre plus tard et exécuter l’action particulière.
NOTE : Vous pouvez en savoir plus sur les générateurs et les fonctions de générateur en vous référant à la section des prérequis.
Maintenant la question se pose : pourquoi utilisons-nous le middleware redux-saga ? La réponse est simple :
- Il fournit une meilleure façon d’écrire des cas de tests unitaires, ce qui nous aidera à tester les fonctions du générateur d’une manière plus simple.
- Il peut vous aider à effectuer beaucoup d’effets secondaires et à fournir un meilleur contrôle sur les changements. Par exemple, lorsque vous voulez voir si une action X particulière est exécutée, puis exécuter l’action Y. Des fonctions comme
takeEvery
,all
, etc. permettent d’effectuer ces opérations en toute simplicité. Nous en parlerons plus en détail dans une section ultérieure.
Si vous n’êtes pas familier avec redux-saga, alors je vous recommande fortement de parcourir la documentation ici.
Description du cas d’utilisation
NOTE : Les diagrammes de contexte, conteneur et classe dessinés dans ce billet de blog ne suivent pas exactement les conventions de ces diagrammes. Je les ai approximés ici pour que vous puissiez comprendre les concepts de base.
Avant de commencer, je vous suggère de vous documenter sur les c4models, les diagrammes de conteneurs et les diagrammes de contexte. Vous pouvez trouver des ressources à leur sujet dans la section des prérequis.
Dans cet article, nous allons considérer le cas d’utilisation suivant : Créer un jeu de serpent.
Le cas d’utilisation est assez explicite, et nous avons discuté de ce que le jeu de serpent implique ci-dessus. Vous trouverez ci-dessous le diagramme de contexte de notre cas d’utilisation :
Notre diagramme de contexte est assez simple. Le joueur interagit avec l’interface utilisateur. Plongeons plus profondément dans l’interface utilisateur du plateau de jeu et explorons les autres systèmes qui s’y trouvent.
Comme vous pouvez le voir sur le diagramme ci-dessus, notre interface utilisateur de plateau de jeu est divisée en deux couches :
- Couche d’interface utilisateur
- Couche de données
La couche UI est constituée des composants suivants :
- Calculatrice de score : Il s’agit d’un composant qui affiche le score chaque fois que le serpent mange le fruit.
- CanvasBoard : Il s’agit d’un composant qui gère la majeure partie de l’interface utilisateur de notre jeu. Sa fonctionnalité de base est de dessiner le serpent sur le canevas et de vider le canevas. Il assume également les responsabilités suivantes :
- Il détecte si le serpent est entré en collision avec lui-même ou avec les murs d’enceinte (détection de collision).
- Aide à déplacer le serpent le long du plateau avec des événements clavier.
- Réinitialise le jeu lorsque le jeu est terminé.
- Instructions : Il fournit les instructions pour jouer au jeu, ainsi que le bouton de réinitialisation.
- Utilitaires : Ce sont les fonctions utilitaires que nous utiliserons tout au long de l’application lorsque cela sera nécessaire.
Parlons maintenant de la couche de données. Elle se compose des éléments suivants :
- Redux-saga : Ensemble de fonctions génératrices qui effectueront certaines actions.
- Actions et créateurs d’actions : Il s’agit de l’ensemble des constantes et des fonctions qui aideront à distribuer les actions appropriées.
- Réducteurs : Cela nous aidera à répondre aux différentes actions distribuées par les créateurs d’actions et les sagas.
Nous allons plonger en profondeur dans tous ces composants et voir comment ils fonctionnent collectivement dans les sections ultérieures. Tout d’abord, initialisons notre projet et mettons en place notre couche de données – c’est-à-dire le magasin Redux.
Mise en place de l’application et de la couche de données
Avant de commencer à comprendre nos composants de jeu, mettons d’abord en place notre application React et la couche de données.
Le jeu est construit avec React. Je recommande vivement d’utiliser le modèle create-react-app pour installer tout ce qui est nécessaire au démarrage de votre application React.
Pour créer un projet CRA (create-react-app), nous devons d’abord l’installer. Tapez la commande ci-dessous dans votre terminal :
npm install -g create-react-app
Note : Avant d’exécuter cette commande, assurez-vous que vous avez installé Node.js dans votre système. Suivez ce lien pour l’installer.
Ensuite, nous allons commencer par créer notre projet. Appelons-le « snake-game ». Tapez la commande ci-dessous dans votre terminal pour créer le projet :
npx create-react-app snake-game
Cela peut prendre quelques minutes. Une fois que c’est terminé, allez dans votre projet nouvellement créé en utilisant la commande suivante :
cd snake-game
Une fois dans le projet, tapez la commande suivante pour démarrer le projet :
npm run start
Cette commande va ouvrir un nouvel onglet dans votre navigateur avec le logo React qui tourne sur la page comme ci-dessous :
Maintenant, la configuration initiale de notre projet est terminée. Configurons notre couche de données (le magasin Redux). Notre couche de données nécessite que nous installions les paquets suivants :
Tout d’abord, commençons par installer ces paquets. Avant de commencer, assurez-vous que vous êtes dans le répertoire du projet. Tapez la commande ci-dessous dans le terminal :
npm install redux react-redux redux-saga
Une fois que ces paquets sont installés, nous allons d’abord configurer notre magasin Redux. Pour commencer, créons d’abord un dossier nommé store
:
mkdir store
Ce dossier store
sera composé de tous les fichiers liés à Redux. Nous allons organiser notre dossier store de la manière suivante :
store/
├─── actions
│ └─── index.ts
├─── reducers
│ └─── index.ts
└── sagas
└─── index.ts
├─── index.ts
Voyons ce que fait chacun de ces fichiers :
action/index.tsx
: Ce fichier est constitué de constantes qui représentent les actions que notre application peut effectuer et dispatcher vers le magasin Redux. Un exemple d’une telle constante d’action ressemble à ceci :
export const MOVE_RIGHT = "MOVE_RIGHT"
Nous allons utiliser la même constante d’action pour créer une fonction qui renverra un objet avec les propriétés suivantes :
type
: Type d’action, c’est-à-dire constante d’actionpayload
: données supplémentaires qui agissent comme une charge utile.
Ces fonctions qui renvoient un objet avec la propriété type
sont appelées des créateurs d’actions. Nous utilisons ces fonctions pour envoyer des actions à notre magasin Redux.
L’attribut payload
signifie qu’avec l’action, nous pouvons également passer des données supplémentaires qui peuvent être utilisées pour stocker ou mettre à jour la valeur dans l’état global.
NOTE: Il est obligatoire d’avoir la propriété type
retournée par le créateur de l’action. La propriété payload
est facultative. En outre, le nom de la propriété payload
peut être n’importe quoi.
Voyons un exemple de créateur d’action :
//Sans charge utile
export const moveRight = () => ({
type : MOVE_RIGHT
}) ;
//Avec une charge utile
export const moveRight = (data : string) => ({
type : MOVE_RIGHT,
charge utile : données
}) ;
Maintenant que nous savons ce que sont les actions et les créateurs d’actions, nous pouvons passer à la configuration de notre prochain artefact qui est un reducer.
Les réducteurs sont des fonctions qui renvoient un nouvel état global chaque fois qu’une action est envoyée. Ils prennent l’état global actuel et renvoient le nouvel état basé sur l’action qui est envoyée/appelée. Ce nouvel état est calculé sur la base de l’état précédent.
Nous devons veiller à ne pas effectuer d’effets secondaires dans cette fonction. Nous ne devons pas modifier l’état global – nous devons plutôt retourner l’état mis à jour en tant que nouvel objet. Par conséquent, la fonction reducer doit être une fonction pure.
Maintenant, assez parlé des réducteurs. Jetons un coup d’oeil à nos exemples de réducteurs :
const GlobalState = {
données : ""
} ;
const gameReducer = (state = GlobalState, action) => {
switch (action.type) {
cas "MOVE_RIGHT" :
/**
* Effectuer un certain nombre d'opérations
*/
return {
...état, données : action.payload
} ;
par défaut :
return state ;
}
}
Dans cet exemple, nous avons créé une fonction réducteur qui s’appelle gameReducer
. Elle prend l’état (paramètre par défaut comme état global) et une action. Chaque fois que le type d'action
correspond au cas de commutation, il exécute une action particulière, comme le retour d’un nouvel état basé sur l’action.
Le fichier sagas/index.ts
comprendra toutes les sagas que nous utiliserons dans notre application. Nous avons quelques notions de base sur les sagas que nous avons brièvement expliquées dans les sections précédentes. Nous approfondirons cette section lorsque nous commencerons à implémenter le jeu du serpent.
Maintenant, nous avons une compréhension de base des artefacts impliqués dans la création de notre magasin Redux. Allons-y et créons store/index.ts
comme ci-dessous :
import {
createStore,
applyMiddleware
} de "redux" ;
import createSagaMiddleware de "redux-saga" ;
import gameReducer de "./reducers" ;
import watcherSagas de "./sagas" ;
const sagaMiddleware = createSagaMiddleware() ;
const store = createStore(gameReducer, applyMiddleware(sagaMiddleware)) ;
sagaMiddleware.run(watcherSagas) ;
exporter le magasin par défaut ;
Nous allons d’abord importer notre reducer et la saga. Ensuite, nous allons utiliser la fonction createSagaMiddleware()
pour créer un middleware saga.
Ensuite, nous allons le connecter à notre magasin en le passant comme argument à la fonction applyMiddleware
à l’intérieur de createStore
que vous utilisez pour créer un magasin. Nous passerons également gameReducer
à cette fonction pour qu’un reducer soit mappé à notre magasin.
Enfin, nous allons exécuter notre sagaMiddleware en utilisant ce code :
sagaMiddleware.run(watcherSagas) ;
Notre dernière étape consiste à injecter ce magasin
au niveau supérieur de notre application React en utilisant le composant Provider
fourni par react-redux
. Vous pouvez le faire comme suit :
import { Provider } de "react-redux" ;
import store de "./store" ;
const App = () => {
return (
// Composants enfants...
) ;
} ;
export default App ;
J’ai également installé chakra-UI comme bibliothèque de composants UI pour notre projet. Pour installer chakra-UI, tapez la commande suivante :
npm install @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^5
Nous devons également configurer le ChakraProvider
qui ira dans notre fichier App.tsx
. Notre fichier App.tsx
mis à jour ressemblera à ceci :
import { ChakraProvider, Container, Heading } from "@chakra-ui/react" ;
import { Provider } de "react-redux" ;
import store de "./store" ;
const App = () => {
return (
JEU DE SNAKE
//Composants pour enfants
) ;
} ;
export default App ;
Comprendre la couche UI
Comprenons d’abord la dynamique de notre jeu de serpent du point de vue de l’interface utilisateur. Avant de commencer, notre jeu de serpent final ressemblera à ce qui suit :
La couche UI se compose de 3 couches : Le calculateur de score, le tableau et les instructions. Le diagramme ci-dessous présente ces sections :
Plongeons plus profondément dans chacune de ces sections pour comprendre le fonctionnement de notre jeu de serpent.
Tableau de bord
Nous allons commencer par comprendre le Canvas Board :
- Notre tableau de toile va être de dimensions
hauteur : 600, largeur : 1000
- L’ensemble de ce tableau est divisé en blocs de
20x20
. Autrement dit, chaque objet dessiné sur ce tableau a unehauteur de 20
et unelargeur de 20
. - Nous utilisons l’élément
Pour dessiner les formes dans le composant tableau de toile.
Dans notre projet, nous écrivons le composant CanvasBoard dans le fichier components/CanvasBoard.tsx
. Maintenant que notre compréhension de base est claire au sujet du composant CanvasBoard, commençons à construire ce composant.
Créez un composant simple qui renvoie un élément de toile comme ci-dessous :
export interface ICanvasBoard {
height : nombre ;
width : nombre ;
}
const CanvasBoard = ({ height, width } : ICanvasBoard) => {
return (
Appelez ce composant dans notre fichier App.tsx
avec une largeur et une hauteur de 1000 et 600 comme accessoire comme ci-dessous :
import { ChakraProvider, Container, Heading } from "@chakra-ui/react" ;
import { Provider } from "react-redux" ;
import CanvasBoard de "./components/CanvasBoard" ;
import ScoreCard de "./components/ScoreCard" ;
import store de "./store" ;
const App = () => {
return (
JEU DU SERPENT
//Composant Canvasboard ajouté
) ;
} ;
export default App ;
Cela créera une boîte simple de hauteur=600 et de largeur=1000 avec une bordure noire comme ci-dessous :
Maintenant, dessinons un serpent au centre de ce canevas. Mais avant de commencer à dessiner, nous devons obtenir le contexte de cet élément de toile
.
Le contexte d’un élément de toile vous fournit toutes les informations dont vous avez besoin concernant l’élément de toile. Il vous donne les dimensions du canevas et vous aide également à dessiner sur le canevas.
Pour obtenir le contexte de notre élément de toile, nous devons appeler la fonction getCanvas('2d')
qui renvoie le contexte 2d de la toile. Le type de retour de cette fonction est l’interface CanvasRenderingContext2D
.
Pour faire cela en JS pur, il faudrait faire quelque chose comme ci-dessous :
const canvas = document.querySelector('canvas') ;
const canvasCtx = canvas.getContext('2d') ;
Mais pour le faire dans React, nous devons créer un ref
et le passer à l’élément canvas afin que nous puissions l’adresser plus tard dans différents hooks. Pour ce faire dans notre application, créez un ref
en utilisant le hook useRef
:
const canvasRef = useRef(null) ;
Passez le ref dans notre élément canvas
:
Une fois que le canvasRef
est passé dans l’élément canvas
, nous pouvons l’utiliser dans un hook useEffect
et stocker le contexte dans une variable d’état.
Dessin des objets
Après avoir obtenu le contexte, nous devons effectuer les tâches suivantes à chaque fois qu’un composant se met à jour :
- Effacer le canevas
- Dessine le serpent avec la position actuelle
- Dessine un fruit à une position aléatoire à l’intérieur de la boîte
Nous allons effacer le canevas plusieurs fois, nous allons donc en faire une fonction utilitaire. Donc, pour cela, créons un dossier appelé utilitaires
:
mkdir utilitaires
cd utilitaires
touch index.tsx
La commande ci-dessus va également créer un fichier index.tsx
dans le dossier utilities. Ajoutez le code ci-dessous dans le fichier utilities/index.tsx
:
La fonction clearBoard
est assez simple. Elle effectue les actions suivantes :
- Il accepte les objets 2d canvas context en tant qu’argument.
- Elle vérifie que le contexte n’est pas nul ou indéfini.
- La fonction
clearRect
va effacer tous les pixels ou objets présents à l’intérieur du rectangle. Cette fonction prend la largeur et la hauteur en argument pour le rectangle à effacer.
Nous utiliserons cette fonction clearBoard
dans notre useEffect CanvasBoard
pour effacer le canevas à chaque fois que le composant est mis à jour. Pour différencier les différents useEffects
, nous appellerons le useEffect ci-dessus useEffect1.
Commençons maintenant par dessiner le serpent et le fruit à une position aléatoire. Comme nous allons dessiner les objets plusieurs fois, nous allons créer une fonction utilitaire appelée drawObject
. Ajoutez le code ci-dessous dans le fichier utilities/index.tsx
:
La fonction drawObject
accepte les arguments suivants :
context
– Un objet de contexte de canevas 2D pour dessiner l’objet sur le canevas.objectBody
– Il s’agit d’un tableau d’objets, chaque objet ayant des propriétésx
ety
, comme l’interfaceIObjectBody
.fillColor
– Couleur à remplir à l’intérieur de l’objet.strokeStyle
– Couleur à remplir dans le contour de l’objet. La valeur par défaut est#146356
.
Cette fonction vérifie si le contexte est indéfini ou nul. Ensuite, elle itère sur le corps de l'objet
via forEach. Pour chaque objet, elle effectue les opérations suivantes :
- Il attribuera le
fillStyle
et lestrokeStyle
dans le contexte. - Il utilisera
fillReact
pour créer un rectangle rempli avec les coordonnéesobject.x
etobject.y
de taille20x20
- Enfin, il utilisera
strokeRect
pour créer un rectangle délimité avec les coordonnéesobject.x
etobject.y
de taille20x20
Pour dessiner le serpent, nous devons maintenir la position du serpent. Pour cela, nous pouvons utiliser notre outil de gestion d’état global redux
.
Nous devons mettre à jour notre fichier reducers/index.ts
. Puisque nous voulons suivre la position du serpent, nous allons l’ajouter dans notre état global comme suit :
Appelons cet état dans notre composant CanvasBoard
. Nous utiliserons le hook useSelector
de react-redux pour obtenir l’état requis depuis le magasin. Ce qui suit nous donnera l’état global du serpent
:
const snake1 = useSelector((state : IGlobalState) => state.snake) ;
Intégrons-le dans notre composant CanvasBoard
et passons-le à notre fonction drawObject
pour voir le résultat :
Voyons à quoi ressemblera la sortie lorsque le serpent sera dessiné :
Déplacer le serpent sur le plateau
Maintenant que nous avons dessiné notre serpent sur la toile, apprenons à le déplacer sur le plateau.
Le mouvement du serpent est simple. Il doit toujours suivre les points ci-dessous :
- Si le serpent se déplace horizontalement, il ne peut se déplacer que vers le haut, le bas et dans la direction dans laquelle il se déplace. Par exemple, si le serpent se déplace vers la droite, il peut se déplacer vers le haut ou le bas ou continuer à se déplacer vers la droite.
- Si le serpent se déplace verticalement, il ne peut se déplacer que vers la droite, la gauche ou continuer dans la direction dans laquelle il se déplace actuellement. Par exemple, si le serpent se déplace vers le haut, il peut se déplacer vers la droite ou la gauche (ou continuer vers le haut).
- Le serpent ne peut pas se déplacer dans la direction opposée à celle de son déplacement actuel. Autrement dit, si le serpent se déplace vers la gauche, il ne peut pas se déplacer directement vers la droite. De même, s’il se déplace vers le haut, il ne peut pas se déplacer vers le bas.
Pour que le mouvement de notre serpent soit fluide, le serpent doit toujours se déplacer de façon rectangulaire. Et il doit respecter les points ci-dessus pour avoir ce mouvement.
Le diagramme ci-dessous résume le fonctionnement du mouvement du serpent dans l’ensemble de l’application :
REMARQUE : dans le diagramme ci-dessus, le mouvement complet du serpent commence avec le composant CanvasBoard
.
ASTUCE : Ne vous inquiétez pas si vous ne pouvez pas suivre le schéma ci-dessus. Lisez simplement les sections suivantes pour y voir plus clair.
Pour maintenir le mouvement du serpent, nous allons introduire une autre variable d’état dans notre état global, appelée disallowedDirection
. Le but de cette variable est de garder la trace de la direction opposée au mouvement du serpent.
Par exemple, si le serpent se déplace vers la gauche, la variable disallowedDirection
sera définie sur la droite. Donc, pour résumer, nous suivons cette direction afin d’éviter que le serpent ne se déplace dans la direction opposée.
Créons cette variable dans notre état global :
Créons maintenant des actions et des créateurs d’actions qui nous aideront à déplacer le serpent.
Nous aurons deux types d’actions pour ce cas :
- Actions pour les sagas
- Voici les actions qui seront envoyées par le composant
CanvasBoard
. Ces actions seront :- MOVE_RIGHT
- MOVE_LEFT
- MOVE_UP
- MOVE_DOWN
- Voici les actions qui seront envoyées par le composant
- Actions pour les réducteurs
- Ce sont les actions qui seront produites par la saga pour propager les appels aux reducers. Ces actions seront :
Nous allons examiner de plus près ces actions dans les sections suivantes.
Nous allons créer une action supplémentaire appelée SET_DIS_DIRECTION
pour définir l’état disallowedDirection
.
Créons quelques créateurs d’action pour le mouvement du serpent :
setDisDirection
– Ce créateur d’action sera utilisé pour définir ladisallowedDirection
via l’actionSET_DIS_DIRECTION
. Voici le code de ce créateur d’action :
export const setDisDirection = (direction : string) => ({
type : SET_DIS_DIRECTION,
charge utile : direction
}) ;
makeMove
– Cette action sera utilisée pour définir/mettre à jour les nouvelles coordonnées du serpent en mettant à jour la variable d’état duserpent
. Voici le code de ce créateur d’action :
export const makeMove = (dx : nombre, dy : nombre, move : chaîne) => ({
type : move,
payload : [dx, dy]
}) ;
Les paramètres dx
et dy
sont les deltas. Ils indiquent au magasin Redux de combien nous devons augmenter/diminuer les coordonnées de chaque bloc du serpent pour déplacer le serpent dans la direction donnée.
Le paramètre move
est utilisé pour spécifier dans quelle direction le serpent va se déplacer. Nous verrons bientôt ces créateurs d’actions dans les prochaines sections.
Enfin, notre fichier actions/index.
ts mis à jour ressemblera à quelque chose comme ceci :
export const MOVE_RIGHT = "MOVE_RIGHT" ;
export const MOVE_LEFT = "MOVE_LEFT" ;
export const MOVE_UP = "MOVE_UP" ;
export const MOVE_DOWN = "MOVE_DOWN" ;
export const RIGHT = "RIGHT" ;
export const LEFT = "LEFT" ;
export const UP = "UP" ;
export const DOWN = "DOWN" ;
export const SET_DIS_DIRECTION = "SET_DIS_DIRECTION" ;
export interface ISnakeCoord {
x : nombre ;
y : nombre ;
}
export const makeMove = (dx : nombre, dy : nombre, move : chaîne) => ({
type : move,
charge utile : [dx, dy]
}) ;
export const setDisDirection = (direction : string) => ({
type : SET_DIS_DIRECTION,
charge utile : direction
}) ;
Maintenant, regardons la logique que nous utilisons pour déplacer le serpent en fonction des actions ci-dessus. Tous les mouvements du serpent seront suivis par les actions suivantes :
Toutes ces actions sont les éléments constitutifs du mouvement du serpent. Ces actions, lorsqu’elles sont lancées, mettent toujours à jour l’état global du serpent
en fonction de la logique que nous décrivons ci-dessous. Et elles calculeront les nouvelles coordonnées du serpent à chaque mouvement.
Pour calculer les nouvelles coordonnées du serpent après chaque mouvement, nous utiliserons la logique suivante :
- Copiez les coordonnées dans une nouvelle variable appelée
newSnake
- Ajoutez au début de la variable
newSnake
les nouvelles coordonnées x et y. Les attributs x et y de ces coordonnées sont mis à jour en ajoutant les valeurs x et y de la charge utile de l’action. - Enfin, supprimez la dernière entrée du tableau
newSnake
.
Maintenant que nous avons une certaine compréhension de la façon dont le serpent se déplace, ajoutons les cas suivants dans notre gameReducer
:
Pour chaque mouvement du serpent, nous mettons à jour les nouvelles coordonnées x et y qui sont augmentées par les payloads action.payload[0]
et action.payload[1]
. Nous avons terminé avec succès la mise en place des actions, des créateurs d’actions et de la logique du réducteur.
Nous sommes prêts et pouvons maintenant utiliser tout cela dans notre composant CanvasBoard
.
Tout d’abord, ajoutons un hook useEffect dans notre composant CanvasBoard
. Nous utiliserons ce crochet pour attacher/ajouter un gestionnaire d’événement. Ce gestionnaire d’événements sera lié à l’événement keypress
. Nous utilisons cet événement parce qu’à chaque fois que nous appuyons sur les touches w
a
s
d
, nous devons être en mesure de contrôler le mouvement du serpent.
Notre useEffect ressemblera à quelque chose comme ci-dessous :
Il fonctionne de la manière suivante :
- Lors du montage du composant, l’écouteur d’événements avec la fonction de rappel
handleKeyEvents
est attaché à l’objet fenêtre. - Lors du démontage du composant, l’écouteur d’événements est retiré de l’objet fenêtre.
- S’il y a un changement dans la direction ou la fonction
handleKeyEvents
, nous réexécuterons cet useEffect. Par conséquent, nous avons ajoutédisallowedDirection
ethandleKeyEvents
dans le tableau de dépendances.
Regardons comment le callback handleKeyEvents
est créé. Vous trouverez ci-dessous le code correspondant :
const handleKeyEvents = useCallback(
(event : KeyboardEvent) => {
if (disallowedDirection) {
switch (event.key) {
cas "w" :
moveSnake(0, -20, disallowedDirection) ;
pause ;
cas "s" :
moveSnake(0, 20, disallowedDirection) ;
pause ;
cas "a" :
moveSnake(-20, 0, disallowedDirection) ;
pause ;
cas "d" :
event.preventDefault() ;
moveSnake(20, 0, disallowedDirection) ;
pause ;
}
} else {
si (
disallowedDirection !== "LEFT" &&
disallowedDirection !== "UP" &&
disallowedDirection !== "DOWN" &&
event.key === "d"
)
moveSnake(20, 0, disallowedDirection) ; //Déplacement vers la DROITE au départ
}
},
[disallowedDirection, moveSnake]
) ;
Nous avons enveloppé cette fonction avec un crochet useCallback
. C’est parce que nous voulons la version mémorisée de cette fonction qui est appelée à chaque changement d’état (c’est-à-dire, sur le changement de disallowedDirection
et moveSnake
). Cette fonction est appelée à chaque fois que l’on appuie sur une touche du clavier.
Cette fonction de rappel du gestionnaire d’événements a l’objectif suivant :
- Si la
direction disallowedDirection
est vide, nous nous assurons que le jeu ne démarrera que lorsque l’utilisateur appuiera sur la touched
. Cela signifie que le jeu ne démarrera que lorsque le serpent sera en mouvement. Cela signifie que le jeu ne démarre que lorsque le serpent se déplace vers la droite.
REMARQUE: au départ, la valeur de la variable d’état globale disallowedDirection
est une chaîne vide. De cette façon, nous savons que si sa valeur est vide alors c’est le début du jeu.
Une fois que le jeu commence, la variable disallowedDirection
ne sera pas vide et elle écoutera alors toutes les pressions du clavier telles que w
s
et a
.
Enfin, à chaque pression sur le clavier, nous appelons la fonction appelée moveSnake
. Nous allons l’examiner de plus près dans la prochaine section.
La fonction moveSnake
est une fonction qui distribue une action passée au créateur d’action makeMove
. Cette fonction accepte trois arguments :
- dx – Delta pour l’axe des x. Ceci indique de combien le serpent doit se déplacer le long de l’axe des x. Si
dx
est positif, il se déplace vers la droite, s’il est négatif, il se déplace vers la gauche. - dy – Delta pour l’axe des ordonnées. Il indique de combien le serpent doit se déplacer le long de l’axe des ordonnées. Si
dy
est positif, il se déplace vers le bas, si c’est négatif, il se déplace vers le haut. - disallowedDirection – Cette valeur indique que le serpent ne doit pas se déplacer dans la direction opposée. C’est une action qui est capturée par notre middleware saga.
Le code de la fonction moveSnake
ressemblera à ceci :
La fonction moveSnake
est une fonction simple qui vérifie les conditions :
- Si dx > 0, et que la
disallowedDirection
n’est pasRIGHT
, alors il peut se déplacer dans la direction RIGHT. - Si dx < 0, et que la
disallowedDirection
n’est pasLEFT
, alors il peut se déplacer dans la direction LEFT. - Si dy > 0, et que la
disallowedDirection
n’est pasDOWN
, il peut se déplacer dans la direction DOWN. - Si dy < 0, et que la
disallowedDirection
n’est pasUP
, alors il peut se déplacer dans la direction UP.
Cette valeur disallowedDirection
est définie dans nos sagas dont nous parlerons davantage dans les sections ultérieures de cet article. Si nous revoyons la fonction handleKeyEvents
maintenant, elle a beaucoup plus de sens. Prenons un exemple ici :
- Supposons que vous voulez déplacer le serpent vers la DROITE. Alors cette fonction détectera que la touche
d
est pressée. - Une fois cette touche enfoncée, elle appelle la fonction
makeMove
(condition de démarrage du jeu) avecdx
égal à 20 (+ve),dy
égal à 0, et ladisallowedDirection
précédemment définie est appelée ici.
De cette façon, nous faisons le mouvement du serpent dans une direction particulière. Maintenant, regardons les sagas
que nous avons utilisées et comment elles gèrent le mouvement du serpent.
Créons un fichier appelé saga/index.ts
. Ce fichier contiendra toutes nos sagas. Ce n’est pas une règle, mais en général, nous créons deux sagas.
La première est la saga qui distribue les actions réelles au magasin – appelons-la saga worker. La seconde est la saga watcher qui surveille toute action en cours de distribution – appelons-la saga watcher.
Maintenant, nous devons créer une saga watcher qui surveillera les actions suivantes : MOVE_RIGHT
, MOVE_LEFT
, MOVE_UP
, MOVE_DOWN
.
Cette saga watcher va surveiller les actions ci-dessus et exécuter la fonction moveSaga
qui est une saga worker.
Vous remarquerez que nous avons utilisé une nouvelle fonction appelée takeLatest
. Cette fonction appellera la saga worker et annulera tous les appels précédents de la saga si l’une des actions mentionnées dans le premier argument est distribuée.
D’après les mots de la docs de redux-saga :
takeLatest(pattern, saga, ...args)
Crée une
saga
pour chaque action distribuée au magasin qui correspond aumotif
. Et annule automatiquement toute tâchesaga
lancée précédemment si elle est toujours en cours.
- Chaque fois qu’une action est envoyée au magasin. Et si cette action correspond au
modèle
,takeLatest
lance une nouvelle tâchesaga
en arrière-plan. Si une tâchesaga
a été lancée précédemment (sur la dernière action distribuée avant l’action actuelle), et si cette tâche est toujours en cours, la tâche sera annulée.
motif : String | Array | Function
– pour plus d’informations, voir la documentation de[take(pattern)](https://redux-saga.js.org/docs/api/#takepattern)
saga :
Function
– une fonction Generator
args :
Tableau
– arguments à passer à la tâche lancée.takeLatest
ajoutera l’action entrante à la liste des arguments (c’est-à-dire que l’action sera le dernier argument fourni àsaga
)
Maintenant, créons une saga ouvrière appelée moveSaga
qui va effectivement distribuer les actions au magasin Redux :
La saga worker moveSaga
exécute les fonctions suivantes :
- Il s’exécute à l’intérieur d’une boucle infinie.
- Ainsi, une fois qu’une direction est donnée – c’est-à-dire si la touche
d
est enfoncée et que l’actionMOVE_RIGHT
est envoyée – elle commence à envoyer la même action jusqu’à ce qu’une nouvelle action (c’est-à-dire une direction) soit donnée. Ceci est géré par le snippet ci-dessous :
3. Une fois que l’action ci-dessus est envoyée, nous définissons la direction interdite dans la direction opposée, ce qui est pris en charge par le créateur de l’action setDisDirection
.
Maintenant, assemblons ces sagas dans notre fichier sagas/index.ts
:
Mettons maintenant à jour notre composant CanvasBoard
pour intégrer ces changements.
Une fois que vous avez effectué ces changements, vous pouvez essayer de déplacer le serpent. Et voilà ! Vous verrez le résultat suivant :
Dessiner le fruit à une position aléatoire
Pour dessiner un fruit à une position aléatoire sur le plateau, nous allons utiliser la fonction utilitaire generateRandomPosition
. Jetons un coup d’œil à cette fonction :
Il s’agit d’une fonction qui va générer des coordonnées x et y aléatoires en multiples de 20. Ces coordonnées seront toujours inférieures à la largeur et à la hauteur du tableau. Elle accepte la largeur
et la hauteur
comme arguments.
Une fois que nous avons cette fonction, nous pouvons l’utiliser pour dessiner le fruit à une position aléatoire dans le tableau.
Tout d’abord, créons une variable d’état pos
qui sera initialement constituée d’une position aléatoire.
Ensuite, nous allons dessiner le fruit via notre fonction drawObject
. Après cela, nous mettrons légèrement à jour notre hook useEffect
:
Une fois que nous aurons effectué ces changements, notre tableau ressemblera à ce qui suit :
Calculateur de score
Le score du jeu est calculé en fonction du nombre de fruits que le serpent a consommés sans se heurter à lui-même ou à la limite de la boîte. Si le serpent consomme le fruit, la taille du serpent augmente. S’il entre en collision avec le bord de la boîte, alors le jeu est terminé.
Maintenant que nous savons quels sont nos critères pour calculer le score, voyons comment calculer la récompense.
Calcul de la récompense
La récompense après que le serpent ait consommé le fruit est la suivante :
- Augmente la taille du serpent.
- Augmente le score.
- Place le nouveau fruit à un autre endroit aléatoire.
Si le serpent consomme le fruit, alors nous devons augmenter la taille du serpent. C’est une tâche très simple, nous pouvons simplement ajouter les nouvelles coordonnées x et y qui sont inférieures à 20 du dernier élément du tableau d’état global du serpent
. Par exemple, si le serpent a les coordonnées suivantes :
{
serpent : [
{ x : 580, y : 300 },
{ x : 560, y : 300 },
{ x : 540, y : 300 },
{ x : 520, y : 300 },
{ x : 500, y : 300 },
],
}
Nous devrions simplement ajouter l’objet suivant dans le tableau du serpent : { x : 480, y : 280 }
De cette façon, nous augmentons la taille du serpent et nous ajoutons la nouvelle partie/bloc à la fin de celui-ci. Pour que cela soit mis en œuvre via Redux et redux-saga, nous aurons besoin de l’action et du créateur d’action suivants :
export const INCREMENT_SCORE = "INCREMENT_SCORE" ; //action
export const increaseSnake = () => ({ //action creator
type : INCREASE_SNAKE
}) ;
Nous allons également mettre à jour notre gameReducer
pour tenir compte de ces changements. Nous allons ajouter le cas suivant :
cas INCREASE_SNAKE :
const snakeLen = state.snake.length ;
return {
...état,
serpent : [
...état.serpent,
{
x : state.snake[snakeLen - 1].x - 20,
y : state.snake[snakeLen - 1].y - 20,
},
],
} ;
Dans notre composant CanvasBoard
, nous allons d’abord introduire une variable d’état appelée isConsumed
. Cette variable va vérifier si le fruit est consommé ou non.
const [isConsumed, setIsConsumed] = useState(false) ;
Dans notre hook useEffect
où nous dessinons notre serpent
et le fruit
juste en dessous, nous allons ajouter la condition suivante :
//Quand l'objet est consommé
if (snake1[0].x === pos ?.x && snake1[0].y === pos ?.y) {
setIsConsumed(true) ;
}
La condition ci-dessus vérifiera si la tête du serpent snake [0]
est égale à la pos
, ou la position du fruit. Si c’est le cas, alors la variable d’état isConsumed
sera mise à true.
Une fois le fruit consommé, nous devons augmenter la taille du serpent. Nous pouvons le faire facilement via un autre useEffect
. Créons un autre useEffect
et appelons le créateur d’action increaseSnake
:
//utilisationEffect2
useEffect(() => {
if (isConsumed) {
//Augmente la taille du serpent lorsque l'objet est consommé avec succès
dispatch(increaseSnake()) ;
}
}, [isConsumed]) ;
Maintenant que nous avons augmenté la taille du serpent, voyons comment nous pouvons mettre à jour le score et générer un nouveau fruit à une autre position aléatoire.
Pour générer un nouveau fruit à une autre position aléatoire, nous mettons à jour la variable d’état pos
qui va réexécuter le useEffect1 et dessiner l’objet à pos
. Nous devons mettre à jour notre useEffect1 avec une nouvelle dépendance de pos
et mettre à jour useEffect2 comme suit :
useEffect(() => {
//Génère un nouvel objet
if (isConsumed) {
const posi = generateRandomPosition(width - 20, height - 20) ;
setPos(posi) ;
setIsConsumed(false) ;
//Augmente la taille du serpent lorsque l'objet est consommé avec succès
dispatch(increaseSnake()) ;
}
}, [isConsumed, pos, height, width, dispatch]) ;
Une dernière chose à faire dans ce système de récompense est de mettre à jour le score chaque fois que le serpent mange le fruit. Pour ce faire, suivez les étapes ci-dessous :
- Introduire une nouvelle variable d’état globale appelée
score
. Mettez à jour notre état global comme ci-dessous dans le fichierreducers/index.ts
:
export interface IGlobalState {
serpent : ISnakeCoord[] | [] ;
disallowedDirection : string ;
score : nombre ;
}
const globalState : IGlobalState = {
serpent : [
{ x : 580, y : 300 },
{ x : 560, y : 300 },
{ x : 540, y : 300 },
{ x : 520, y : 300 },
{ x : 500, y : 300 },
],
disallowedDirection : "",
score : 0,
} ;
2. Créez l’action et le créateur d’action suivants dans notre fichier actions/index.ts
:
export const INCREMENT_SCORE = "INCREMENT_SCORE" ; //action
//action créateur :
export const scoreUpdates = (type : string) => ({
type
}) ;
3. Ensuite, nous mettons à jour notre réducteur pour gérer l’action INCREMENT_SCORE
. Cela va simplement incrémenter le score
global de
l’état de un.
cas INCREMENT_SCORE :
return {
...état,
score : state.score + 1,
} ;
4. Ensuite, nous mettons à jour notre état de score, en envoyant l’action INCREMENT_SCORE
chaque fois que le serpent attrape le fruit. Pour cela, nous pouvons mettre à jour notre useEffect2 comme suit :
useEffect(() => {
//Génère un nouvel objet
if (isConsumed) {
const posi = generateRandomPosition(width - 20, height - 20) ;
setPos(posi) ;
setIsConsumed(false) ;
//Augmente la taille du serpent lorsque l'objet est consommé avec succès
dispatch(increaseSnake()) ;
//Incrémente le score
dispatch(scoreUpdates(INCREMENT_SCORE)) ;
}
}, [isConsumed, pos, height, width, dispatch]) ;
5. Enfin, nous créons un composant appelé ScoreCard
. Il affichera le score actuel du joueur. Nous allons le stocker dans le fichier components/ScoreCard.tsx
.
import { Heading } de "@chakra-ui/react" ;
import { useSelector } de "react-redux" ;
import { IGlobalState } de "../store/reducers" ;
const ScoreCard = () => {
const score = useSelector((state : IGlobalState) => state.score) ;
return (
Score actuel : {score}
) ;
}
export default ScoreCard ;
Ensuite, nous devrons également ajouter le composant ScoreCard
dans le fichier App.tsx
pour l’afficher sur notre page.
import { ChakraProvider, Container, Heading } from "@chakra-ui/react" ;
import { Provider } de "react-redux" ;
import CanvasBoard de "./components/CanvasBoard" ;
import ScoreCard de "./components/ScoreCard" ;
import store de "./store" ;
const App = () => {
return (
JEU DU SERPENT
) ;
} ;
export default App ;
Une fois que tout est en place, notre serpent aura un système de récompense complet qui augmente la taille du serpent pour mettre à jour le score.
Détection des collisions
Dans cette section, nous allons voir comment implémenter la détection de collision pour notre jeu de serpent.
Dans notre jeu Snake, si une collision est détectée, le jeu est terminé, c’est-à-dire qu’il s’arrête. Il y a deux conditions pour que les collisions se produisent :
- Le serpent entre en collision avec les limites de la boîte, ou
- Le serpent entre en collision avec lui-même.
Examinons la première condition. Supposons que la tête du serpent touche les limites de la boîte. Dans ce cas, nous arrêtons immédiatement le jeu.
Pour que cette condition soit intégrée à notre jeu, nous devons procéder comme suit :
- Créez une action et un créateur d’action comme ci-dessous :
export const STOP_GAME = "STOP_GAME" ; //action
//créateur d'action
export const stopGame = () => ({
type : STOP_GAME
}) ;
2. Nous devons également mettre à jour notre fichier sagas/index.ts
. Nous allons nous assurer que saga arrête de distribuer des actions lorsque l’action STOP_GAME
est rencontrée.
export function* moveSaga(params : {
type : string ;
payload : ISnakeCoord ;
}) : Générateur<
| PutEffect<{ type : string ; payload : ISnakeCoord }>
| PutEffect<{ type : string ; payload : string }>
| CallEffect
> {
while (params.type !== STOP_GAME) {
yield put({
type : params.type.split("_")[1],
payload : params.payload,
}) ;
switch (params.type.split("_")[1]) {
cas DROIT :
yield put(setDisDirection(LEFT)) ;
break ;
cas GAUCHE :
yield put(setDisDirection(RIGHT)) ;
pause ;
cas UP :
yield put(setDisDirection(DOWN)) ;
pause ;
cas DOWN :
yield put(setDisDirection(UP)) ;
pause ;
}
yield delay(100) ;
}
}
function* watcherSagas() {
yield takeLatest(
[MOVE_RIGHT, MOVE_LEFT, MOVE_UP, MOVE_DOWN, STOP_GAME],
moveSaga
) ;
}
3. Enfin, nous devons mettre à jour notre useEffect1 en ajoutant la condition suivante :
if ( //Check si la tête du serpent est hors des limites de l'obox
snake1[0].x >= largeur ||
snake1[0].x <= 0 ||
snake1[0].y <= 0 ||
snake1[0].y >= hauteur
) {
setGameEnded(true) ;
dispatch(stopGame()) ;
window.removeEventListener("keypress", handleKeyEvents) ;
}
Nous supprimons également l’écouteur d’événements handleKeyEvents
. Cela permettra de s’assurer qu’une fois le jeu terminé, le joueur ne pourra plus déplacer le serpent.
Enfin, voyons comment nous pouvons détecter l’auto-collision du serpent. Nous allons utiliser une fonction utilitaire appelée hasSnakeCollided
. Elle accepte deux paramètres : le premier est le tableau de serpents, et le second est la tête du serpent. Si la tête du serpent touche une partie de lui-même, elle renvoie true ou false.
La fonction hasSnakeCollided
ressemblera à ce qui suit :
export const hasSnakeCollided = (
serpent : IObjectBody[],
currentHeadPos : IObjectBody
) => {
let flag = false ;
snake.forEach((pos : IObjectBody, index : nombre) => {
si (
pos.x === currentHeadPos.x &&
pos.y === currentHeadPos.y &&
index !== 0
) {
flag = true ;
}
}) ;
retourne le drapeau ;
} ;
Nous devrons peut-être légèrement mettre à jour notre useEffect1 en mettant à jour la condition de détection de collision comme ceci :
si (
//Chèque si le serpent est entré en collision avec lui-même
hasSnakeCollided(snake1, snake1[0]) ||
//Contrôle si la tête du serpent est en dehors des limites de l'obox
snake1[0].x >= largeur ||
snake1[0].x <= 0 ||
snake1[0].y <= 0 ||
snake1[0].y >= hauteur
) {
setGameEnded(true) ;
dispatch(stopGame()) ;
window.removeEventListener("keypress", handleKeyEvents) ;
}
Notre useEffect1 ressemblera finalement à ce qui suit :
//utiliserEffet1
useEffect(() => {
//Dessinez sur le canevas à chaque fois
setContext(canvasRef.current && canvasRef.current.getContext("2d")) ;
clearBoard(context) ;
drawObject(context, snake1, "#91C483") ;
drawObject(context, [pos], "#676FA3") ; //Drawn object randomly
//Quand l'objet est consommé
if (snake1[0].x === pos ?.x && snake1[0].y === pos ?.y) {
setIsConsumed(true) ;
}
if (
hasSnakeCollided(snake1, snake1[0]) ||
snake1[0].x >= largeur ||
snake1[0].x <= 0 ||
snake1[0].y <= 0 ||
snake1[0].y >= hauteur
) {
setGameEnded(true) ;
dispatch(stopGame()) ;
window.removeEventListener("keypress", handleKeyEvents) ;
} else setGameEnded(false) ;
}, [context, pos, snake1, height, width, dispatch, handleKeyEvents]) ;
Notre jeu ressemblera à ce qui suit une fois que nous aurons ajouté le système de détection des collisions :
Composant d’instruction
Nous sommes maintenant dans la phase finale du jeu ! Notre dernier composant sera le composant Instruction
. Il comprendra des instructions sur le jeu, comme la condition initiale du jeu, les clés à utiliser et un bouton de réinitialisation.
Commençons par créer un fichier appelé components/Instructions.tsx
. Placez le code ci-dessous dans ce fichier :
import { Box, Button, Flex, Heading, Kbd } de "@chakra-ui/react" ;
export interface IInstructionProps {
resetBoard : () => void ;
}
const Instruction = ({ resetBoard } : IInstructionProps) => (
Comment jouer
NOTE : Commencez le jeu en appuyant sur d
w Déplacement vers le haut
a Déplacement vers la gauche
s Déplacement vers le bas
d Déplacement vers la droite
) ;
export default Instruction
Le composant Instruction
acceptera resetBoard
comme prop qui est une fonction qui aidera l’utilisateur lorsque le jeu sera terminé ou lorsqu’il voudra réinitialiser le jeu.
Avant de nous plonger dans la fonction resetBoard
, nous devons effectuer les mises à jour suivantes dans notre magasin Redux et notre saga :
- Ajoutez l’action et le créateur d’action suivants dans le fichier
actions/index.ts
:
export const RESET_SCORE = "RESET_SCORE" ; //action
export const RESET = "RESET" ; //action
//Action creator :
export const resetGame = () => ({
type : RESET
}) ;
2. Ajoutez ensuite la condition suivante dans notre fichier sagas/index.ts
. Nous allons nous assurer que saga cesse de distribuer des actions lorsque les actions RESET
et STOP_GAME
sont rencontrées.
export function* moveSaga(params : {
type : string ;
payload : ISnakeCoord ;
}) : Générateur<
| PutEffect<{ type : string ; payload : ISnakeCoord }>
| PutEffect<{ type : string ; payload : string }>
| CallEffect
> {
while (params.type !== RESET && params.type !== STOP_GAME) {
yield put({
type : params.type.split("_")[1],
payload : params.payload,
}) ;
switch (params.type.split("_")[1]) {
cas DROIT :
yield put(setDisDirection(LEFT)) ;
break ;
cas GAUCHE :
yield put(setDisDirection(RIGHT)) ;
pause ;
cas UP :
yield put(setDisDirection(DOWN)) ;
pause ;
cas DOWN :
yield put(setDisDirection(UP)) ;
pause ;
}
yield delay(100) ;
}
}
function* watcherSagas() {
yield takeLatest(
[MOVE_RIGHT, MOVE_LEFT, MOVE_UP, MOVE_DOWN, RESET, STOP_GAME],
moveSaga
) ;
}
3. Enfin, nous mettons à jour notre fichier reducers/index.ts
pour le cas RESET_SCORE
comme suit :
cas RESET_SCORE :
return { ...state, score : 0 } ;
Une fois que nos sagas et nos reducers sont mis à jour, nous pouvons jeter un coup d’œil aux opérations que le callback resetBoard
effectuera.
La fonction resetBoard
effectue les opérations suivantes :
- Supprime l’écouteur d’événements
handleKeyEvents
- exécute les actions nécessaires à la réinitialisation du jeu.
- Distribue l’action pour réinitialiser le score.
- Nettoie le canevas.
- Dessine à nouveau le serpent à sa position initiale
- Dessine le fruit à une nouvelle position aléatoire.
- Enfin, ajoute l’écouteur d’événements
handleKeyEvents
pour l’événement d’appui sur la touche
.
Voici à quoi ressemblera notre fonction resetBoard
:
const resetBoard = useCallback(() => {
window.removeEventListener("keypress", handleKeyEvents) ;
dispatch(resetGame()) ;
dispatch(scoreUpdates(RESET_SCORE)) ;
clearBoard(context) ;
drawObject(context, snake1, "#91C483") ;
drawObject(
contexte,
[generateRandomPosition(width - 20, height - 20)],
"#676FA3"
) ; //Dessine l'objet de façon aléatoire
window.addEventListener("keypress", handleKeyEvents) ;
}, [context, dispatch, handleKeyEvents, height, snake1, width]) ;
Vous devez placer cette fonction à l’intérieur du composant CanvasBoard
et passer la fonction resetBoard
en tant que prop à la fonction Instruction
comme ci-dessous :
<>
Une fois cette fonction placée, le composant Instruction sera configuré comme ci-dessous :
Jeu final
Si vous avez suivi jusqu’à ce point, alors félicitations ! Vous avez réussi à créer un jeu de serpent amusant avec React, Redux et redux-sagas. Une fois que tous ces éléments sont connectés, votre jeu ressemblera à ce qui suit :
Résumé
Voilà donc comment vous pouvez construire un jeu Snake à partir de rien. Vous pouvez trouver le code source complet du jeu dans le dépôt ci-dessous :
https://github.com/keyurparalkar/snake-game
Si vous avez aimé l’idée de construire votre propre jeu Snake à partir de rien, vous pouvez passer à la vitesse supérieure en apportant ces améliorations :
- Construisez le jeu de serpent avec three.js
- Ajouter un tableau des scores en ligne
Merci de votre lecture !