Comment créer un jeu de serpent avec React, Redux et Redux Saga ?
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 besoi
📺 Calculateur taille écran idéal
Distance recommandée selon la taille et résolution. Pour 4K, on peut s'asseoir 2× plus près qu'en HD.
Questions courantes
Qu’est-ce qu’un jeu de serpent ?
Un jeu de serpent est un jeu classique où un serpent se déplace à l’intérieur d’une boîte et grandit en mangeant des fruits ou des objets.
Comment créer un jeu de serpent avec React ?
Pour créer un jeu de serpent avec React, vous devez utiliser des bibliothèques telles que Redux et Redux Saga pour gérer les états et les actions du jeu.
Quels sont les prérequis pour créer un jeu de serpent ?
Les prérequis pour créer un jeu de serpent incluent des connaissances en React, Redux et Redux Saga, ainsi que des compétences en programmation JavaScript.
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 :

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é.
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
D’après les mots de la docs de redux-saga :
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.
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.
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 :


- 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é.
- 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.
Si vous n’êtes pas familier avec redux-saga, alors je vous recommande fortement de parcourir la documentation ici.
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 :

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’appellegameReducer. 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 fonctioncreateSagaMiddleware()` 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 :


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 20et 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 fichierApp.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 :

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.
const CanvasBoard = ({hauteur, largeur } : ICanvasBoard) => { const canvasRef = (useRef < HTMLCanvasElement) | (null > null) ; const [context, setContext] = (useState < CanvasRenderingContext2D) | (null > null) ;
useEffect(() => { //Dessinez sur le canevas à chaque fois setContext(canvasRef.current && canvasRef.current.getContext(“2d”)) ; //stocker dans la variable d’état }, [context]) ;
return (
) ; } ;`Stockage du contexte du canevas 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:
- 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
clearRectva 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:
export const drawObject = (
contexte : CanvasRenderingContext2D | null,
objectBody : IObjectBody[],
fillColor : string,
strokeStyle = “#146356”)
) => {
if (context) {
objectBody.forEach((object : IObjectBody) => {
context.fillStyle = fillColor ;
context.strokeStyle = strokeStyle ;
context ?.fillRect(object.x, object.y, 20, 20) ;
context ?.strokeRect(objet.x, objet.y, 20, 20) ;
}) ;
}
} ;Fonction permettant de dessiner un objet sur le canevas 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ésxety, 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
fillStyleet lestrokeStyledans le contexte. - Il utilisera
fillReactpour créer un rectangle rempli avec les coordonnéesobject.xetobject.yde taille20x20 - Enfin, il utilisera
strokeRectpour créer un rectangle délimité avec les coordonnéesobject.xetobject.yde 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 :
export interface IGlobalState { serpent : ISnakeCoord[] | [] ; }
const globalState : IGlobalState = {
//Position de l’ensemble du serpent
serpent : [
{ x : 580, y : 300 },
{ x : 560, y : 300 },
{ x : 540, y : 300 },
{ x : 520, y : 300 },
{ x : 500, y : 300 },
],
} ;Mise à jour de l’état global Appelons cet état dans notre composant CanvasBoard. Nous utiliserons le hook useSelectorde react-redux pour obtenir l’état requis depuis le magasin. Ce qui suit nous donnera l’état global duserpent`:
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 :
export interface ICanvasBoard { height : nombre ; width : nombre ; }
const CanvasBoard = ({ height, width } : ICanvasBoard) => { const canvasRef = useRef(null) ; const [context, setContext] = useState(null) ; const snake1 = useSelector((state : IGlobalState) => state.snake) ; const [pos, setPos] = useState( generateRandomPosition(width - 20, height - 20) ) ;
useEffect(() => {
//Dessinez sur le canevas à chaque fois
setContext(canvasRef.current && canvasRef.current.getContext("2d")) ; //Stockage dans une variable d'état
drawObject(context, snake1, "#91C483") ; //Dessine le serpent à la position requise
drawObject(context, [pos], "#676FA3") ; //Dessine le fruit au hasard
}, [context])
retour (
) ; } ;`Code pour dessiner le serpent et le fruit Voyons à quoi ressemblera la sortie lorsque le serpent sera dessiné :

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 :

**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 :
export interface IGlobalState { serpent : ISnakeCoord[] | [] ; disallowedDirection : string ; }
const globalState : IGlobalState = { //Position de l’ensemble du serpent serpent : [ { x : 580, y : 300 }, { x : 560, y : 300 }, { x : 540, y : 300 }, { x : 520, y : 300 }, { x : 500, y : 300 }, ], disallowedDirection : "" } ;`Ajout d’un nouvel é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
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 ladisallowedDirectionvia 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
newSnakeles 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:
return {
...état,
serpent : newSnake,
} ;
}`Cas du mouvement du serpent
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 :
return () => {
window.removeEventListener("keypress", handleKeyEvents) ;
} ;
}, [disallowedDirection, handleKeyEvents])`Capture des événements clavier via le hook useEffect Il fonctionne de la manière suivante :
- Lors du montage du composant, l’écouteur d’événements avec la fonction de rappel
handleKeyEventsest 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édisallowedDirectionethandleKeyEventsdans 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 disallowedDirectionest 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
dxest 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
dyest 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 :
if (dx < 0 && dy === 0 && ds !== "LEFT") {
dispatch(makeMove(dx, dy, MOVE_LEFT)) ;
}
si (dx === 0 && dy < 0 && ds !== "UP") {
dispatch(makeMove(dx, dy, MOVE_UP)) ;
}
if (dx === 0 && dy > 0 && ds !== "DOWN") {
dispatch(makeMove(dx, dy, MOVE_DOWN)) ;
}
},
[dispatch]
) ;Envoi de l’action pour chaque mouvement du serpent La fonction moveSnake` est une fonction simple qui vérifie les conditions :
- Si dx > 0, et que la
disallowedDirectionn’est pasRIGHT, alors il peut se déplacer dans la direction RIGHT. - Si dx < 0, et que la
disallowedDirectionn’est pasLEFT, alors il peut se déplacer dans la direction LEFT. - Si dy > 0, et que la
disallowedDirectionn’est pasDOWN, il peut se déplacer dans la direction DOWN. - Si dy < 0, et que la
disallowedDirectionn’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
dest 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 ladisallowedDirectionpré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.
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.
Vous pouvez en apprendre plus sur les middleware ici.
)
Maintenant, créons une saga ouvrière appelée moveSaga qui va effectivement distribuer les actions au magasin Redux :
//Dispatche l'action SET_DIS_DIRECTION
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) ;
}
}La saga du travailleur 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
dest enfoncée et que l’actionMOVE_RIGHTest 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 :
Maintenant, assemblons ces sagas dans notre fichier sagas/index.ts:
export function* moveSaga(params : { type : string ; charge utile : ISnakeCoord ; }) : Générateur< | PutEffect<{ type : string ; payload : ISnakeCoord }> | PutEffect<{ type : string ; payload : string }> | CallEffect
{ while (true) { 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], moveSaga ) ; }
export default watcherSagas ;Notre fichier saga Mettons maintenant à jour notre composant CanvasBoard` pour intégrer ces changements.
export interface ICanvasBoard { height : nombre ; width : nombre ; }
const CanvasBoard = ({ height, width } : ICanvasBoard) => { const canvasRef = useRef < HTMLCanvasElement | null > (null) ; const [context, setContext] = useState < CanvasRenderingContext2D | null > (null) ; const snake1 = useSelector((state : IGlobalState) => state.snake) ; const [pos, setPos] = useState < IObjectBody > ( generateRandomPosition(width - 20, height - 20) ) ;
const moveSnake = useCallback(
(dx = 0, dy = 0, ds : string) => {
si (dx > 0 && dy === 0 && ds !== "RIGHT") {
dispatch(makeMove(dx, dy, MOVE_RIGHT)) ;
}
if (dx < 0 && dy === 0 && ds !== "LEFT") {
dispatch(makeMove(dx, dy, MOVE_LEFT)) ;
}
si (dx === 0 && dy < 0 && ds !== "UP") {
dispatch(makeMove(dx, dy, MOVE_UP)) ;
}
if (dx === 0 && dy > 0 && ds !== "DOWN") {
dispatch(makeMove(dx, dy, MOVE_DOWN)) ;
}
},
[dispatch]
) ;
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]
) ;
useEffect(() => {
//Dessinez sur le canevas à chaque fois
setContext(canvasRef.current && canvasRef.current.getContext("2d")) ; //stocke dans la variable d'état
clearBoard(context) ;
drawObject(context, snake1, "#91C483") ; //Dessine le serpent à la position requise
}, [context]) ;
useEffect(() => {
window.addEventListener("keypress", handleKeyEvents) ;
return () => {
window.removeEventListener("keypress", handleKeyEvents) ;
} ;
}, [disallowedDirection, handleKeyEvents]) ;
retour (
) ;
} ;`Mise à jour du composant CanvasBoard avec le déplacement du serpent Une fois que vous avez effectué ces changements, vous pouvez essayer de déplacer le serpent. Et voilà ! Vous verrez le résultat suivant :

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 :
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.
clearBoard(context) ;
drawObject(context, snake1, "#91C483") ; //Dessine le serpent à la position requise
drawObject(context, [pos], "#676FA3") ; //Dessine l'objet au hasard
}, [context]) ;`Dessine des fruits au hasard avec le serpent
Une fois que nous aurons effectué ces changements, notre tableau ressemblera à ce qui suit :

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 notregameReducer` 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 fichieractions/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’actionINCREMENT_SCORE. Cela va simplement incrémenter le scoreglobalde` 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 composantScoreCarddans le fichierApp.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.

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 fichiersagas/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 :

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
resetBoard()}>Réinitialiser le jeu
) ;
export default InstructionLe composantInstructionaccepteraresetBoard` 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 notrefichier sagas/index.ts. Nous allons nous assurer que saga cesse de distribuer des actions lorsque les actions RESETetSTOP_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 fichierreducers/index.tspour le casRESET_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
handleKeyEventspour 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 :

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 :

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 :
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.
- Construisez le jeu de serpent avec three.js
- Ajouter un tableau des scores en ligne
Merci de votre lecture !
Vous pouvez en savoir plus sur l’histoire ou les origines du jeu en consultant le lien Wiki.
FAQ
Qu’est-ce que Redux ?
Redux est une bibliothèque JavaScript qui permet de gérer les états d’une application de manière prévisible et efficace.
Comment utiliser Redux Saga ?
Redux Saga est une bibliothèque qui permet de gérer les actions asynchrones dans une application Redux. Elle permet de définir des workflows complexes et de gérer les erreurs de manière efficace.
Quels sont les avantages de créer un jeu de serpent avec React ?
Les avantages de créer un jeu de serpent avec React incluent la possibilité de créer des applications web interactives et réactives, ainsi que la possibilité d’utiliser des bibliothèques telles que Redux et Redux Saga pour gérer les états et les actions du jeu.
Comment déboguer un jeu de serpent créé avec React ?
Pour déboguer un jeu de serpent créé avec React, vous pouvez utiliser des outils tels que le débogueur de Chrome ou des bibliothèques de débogage telles que Redux DevTools.
Quels sont les prérequis pour créer un jeu de serpent avec React ?
Les prérequis pour créer un jeu de serpent avec React incluent des connaissances en React, Redux et Redux Saga, ainsi que des compétences en programmation JavaScript.
À lire aussi sur le site
Questions fréquentes
Comment réussir créer un jeu de serpent avec react, redux et redux saga ?
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 besoi
Quel est le matériel nécessaire pour créer un jeu de serpent avec react, redux et redux saga ?
Le matériel dépend du contexte précis. Reportez-vous à la section dédiée dans cet article pour la liste détaillée et nos recommandations.
Combien de temps faut-il prévoir pour créer un jeu de serpent avec react, redux et redux saga ?
Selon votre niveau et le contexte, comptez généralement entre 30 minutes et plusieurs heures. Les détails de durée sont précisés dans le guide.
Quelles sont les erreurs à éviter ?
Les erreurs les plus fréquentes sont détaillées dans cet article, avec les bonnes pratiques pour les éviter et obtenir un résultat satisfaisant.
À lire aussi
Comment créer un tableau de bord personnalisé avec les API de WordPress et React
Lorsque vous gérez des sites Web, tout tourne autour des données : vues, temps de réponse, utilisateurs, taux de rebond, etc. Et, si vous gérez des sites Web, vous avez probablement eu à faire face à
Comment créer un nouveau contexte d empilement avec la propriété Isolation en CSS
Qu est-ce que la propriété d isolation CSS ? En CSS, vous pouvez utiliser la propriété d isolation pour créer un nouveau contexte d empilement. Voici à quoi cela ressemble : .container-for-new-stackin
Comment créer des formulaires dynamiques en React
Dans ce tutoriel, nous allons apprendre à construire des formulaires dynamiques dans React. En utilisant des formulaires dynamiques, nous pouvons ajouter des champs ou les supprimer en fonction de nos
Défilement de la page vers le haut Comment défiler vers une section particulière avec Reac…
Lorsque vous développez des sites Web et des applications à l aide de React et de tout autre framework React, vous souhaitez ajouter certaines fonctionnalités pour aider vos utilisateurs à naviguer su