Envie de coder un vrai jeu vidéo qui tourne dans le navigateur ? Dans ce tutoriel, tu crées un jeu de combat React en 2D : Cyber Arena Fighter, un jeu cyberpunk avec 3 combattants, une campagne de 5 niveaux, un boss final en 3 phases, un mode survie, une boutique, un classement et des contrôles tactiles. Tout est expliqué étape par étape, avec le pourquoi de chaque choix, et le projet complet est disponible sur GitHub.

⚛️ React 18⚡ Vite🎨 Canvas 2D🔊 Web Audio API📱 PWA⏱️ 45 min de lecture

Créer un jeu de combat React est l’un des meilleurs projets pour progresser en JavaScript : il faut une boucle de jeu, de la physique, des collisions, une intelligence artificielle, des menus, une sauvegarde et un rendu fluide sur ordinateur comme sur mobile. Ici, on ne s’arrête pas à un petit carré qui saute : on décortique un jeu complet et soigné, prêt à être mis en ligne et installé comme une application.

Jeu de combat React avec Canvas : tutoriel Cyber Arena Fighter
Cyber Arena Fighter, le jeu de combat 2D en React et Canvas que tu vas comprendre et reconstruire.

Particularité de ce projet : aucun serveur, aucune base de données et aucun fichier audio. Les personnages et les décors sont dessinés par le code sur un <canvas>, la musique et les bruitages sont synthétisés avec la Web Audio API, et la progression est sauvegardée dans le navigateur. Résultat : un site 100 % statique, léger et hébergeable n’importe où.

Tu découvres React ? Jette d’abord un œil à notre jeu d’échecs en React TypeScript, plus simple côté rendu, puis reviens ici pour passer au temps réel.

🎯
À qui s’adresse ce tutoriel ?

Chaque section indique son niveau :

  • 🟢 Débutant : installer, lancer, tester et mettre en ligne le jeu, sans toucher au code.
  • 🟡 Intermédiaire : l’organisation du projet, React, l’état global et la sauvegarde.
  • 🔴 Confirmé : la boucle à pas fixe, la machine à états des ennemis, le boss et le rendu Canvas.
💡

Débutant ? Lis les sections 🟢 en entier et survole le reste : tu auras quand même le jeu complet qui tourne sur ton PC… et même en ligne.

Ce que fait ce jeu de combat React

🟢 Débutant

Cyber Arena Fighter est un beat’em up en 2D : tu choisis un combattant, tu nettoies les bas-fonds néon d’une mégapole vague après vague, puis tu affrontes TITAN-X, le mécha géant qui contrôle la ville. Voici tout ce que contient le projet :

FonctionnalitéCe que ça apporte
🗺️ Mode Campagne5 niveaux de difficulté croissante, chacun avec son décor et ses ennemis, et le boss TITAN-X à la fin
♾️ Mode SurvieDes vagues infinies de plus en plus fortes, un boss toutes les 5 vagues, +15 % de PV entre les vagues
🎯 Mode EntraînementImpossible de mourir, ennemis affaiblis : parfait pour apprendre les commandes
🥷 3 combattantsGuerrier équilibré, Assassin rapide avec double saut, Paladin lent mais très résistant
⚔️ Système de combatCoup rapide, coup lourd, onde plasma, garde, esquive avec invulnérabilité, combos et soins
🛒 Boutique4 améliorations permanentes (5 niveaux chacune) et des soins, payés avec les crédits gagnés en combat
🏆 Classement et statsTop 10 par mode, statistiques détaillées (dégâts, ennemis, meilleur combo, temps de jeu…)
🔊 Audio généréMusique et bruitages synthétisés en JavaScript, sans aucun fichier MP3
📱 MobileBoutons tactiles automatiques en paysage, invitation à tourner l’écran en portrait

Visite guidée du jeu, écran par écran

🟢 Débutant

Avant d’ouvrir le code, faisons le tour du jeu. Les captures ont été réalisées avec un profil de démonstration (pseudo, crédits, scores et statistiques fictifs) afin de montrer chaque écran rempli.

La page d’accueil

La page / présente le jeu et ses quatre piliers (campagne, boss, survie, progression). Le bouton Lancer le jeu mène à la page /game.

Page d'accueil du jeu de combat React Cyber Arena Fighter
La page d’accueil : un bouton, quatre arguments, et le jeu est lancé.

Le menu principal

Le hub du jeu : on y lance une partie et on accède à la boutique, au classement, aux statistiques, aux options et à l’aide. Le solde de crédits reste affiché en permanence.

Menu principal de Cyber Arena Fighter avec le solde de crédits
Le menu principal, avec le solde de crédits en haut.

Le choix du mode

  • Campagne : 5 niveaux de difficulté croissante, le dernier contre le boss TITAN-X.
  • Survie : des vagues infinies, un boss toutes les 5 vagues.
  • Entraînement : on ne peut pas mourir, idéal pour apprendre les commandes.
Sélection du mode de jeu : Campagne, Survie ou Entraînement
Trois modes de jeu pour trois façons de jouer.

Le choix du combattant et de la difficulté

Les trois personnages ont des statistiques différentes (vie, vitesse, dégâts, défense, critique). Les trois difficultés modifient les dégâts reçus, la vie des ennemis et le multiplicateur de score (×0,75, ×1 ou ×1,5).

Choix du combattant (Guerrier, Assassin, Paladin) et de la difficulté
Guerrier, Assassin ou Paladin : chaque combattant change ta façon de jouer.

Le combat

En haut à gauche : la vie, la stamina et les soins disponibles. Au centre : le score, le niveau et la vague. À droite : les crédits, le bouton pause et le compteur de combo. Le panneau des commandes se ferme d’un clic et n’apparaît pas sur les écrans tactiles.

Combat en cours dans le jeu de combat 2D en React : HUD, combo et ennemi
Niveau 1, « Bas-Fonds Néon » : combo ×2 en cours.
ActionClavier
Se déplacer← →
Sauter (double saut pour l’Assassin)↑ ou Espace
Coup rapideA ou J
Coup lourdS ou K
Onde plasma (30 stamina)D ou L
Garde (maintenir)E ou I
Esquive (20 stamina)Maj (Shift)
Utiliser un soinR ou H
PauseP ou Échap

La pause

Le moteur se fige complètement : plus aucune mise à jour, plus aucun ennemi qui bouge. On peut reprendre, ouvrir les options, l’aide, ou abandonner la partie.

Menu pause du jeu avec options, aide et abandon
Le menu pause, par-dessus le combat flouté.

La fin de niveau

Un résumé (score, crédits gagnés, ennemis éliminés, temps) et un accès direct à la boutique avant le niveau suivant. Chaque niveau a ensuite son propre décor et ses propres ennemis.

Écran Niveau terminé avec score, crédits et temps
Fin du niveau 1 : on peut passer à la boutique avant d’enchaîner.
Niveau 2 Marché Noir avec un nouveau décor cyberpunk
Niveau 2, « Marché Noir » : nouvelle palette de couleurs, nouveaux ennemis.

Le Game Over

Score final, meilleur combo, niveau atteint, détection d’un nouveau record, puis le choix entre réessayer, passer par la boutique ou changer de personnage.

Écran Game Over avec score final et meilleur combo
Le Game Over récapitule la partie et propose de rejouer immédiatement.

La boutique

Les crédits gagnés en combat servent à acheter 4 améliorations permanentes (Nano-Lames, Armure Titane, Noyau d’Énergie, Bouclier Cinétique) et des soins d’urgence. Le prix augmente à chaque niveau.

Boutique Cyber Marché avec améliorations permanentes
Le Cyber Marché : dégâts, vie, stamina, défense et soins.

Le classement et les statistiques

Les 10 meilleures parties de chaque mode sont sauvegardées dans le navigateur, avec le combattant, la difficulté et la date. Un écran de statistiques cumule toute ta carrière.

Classement du mode Campagne : top 10 des meilleures parties
Le classement Campagne.
Classement du mode Survie avec la vague atteinte
Le classement Survie, trié par score avec la vague atteinte.
Écran des statistiques du joueur : parties, ennemis, dégâts, combos
Dix statistiques cumulées, de la première partie à la dernière.

Les options et l’aide

Pseudo, son, volumes séparés de la musique et des effets, affichage des aides en jeu et remise à zéro de la progression. L’écran d’aide reprend les commandes, les conseils et le comportement de chaque ennemi.

Écran des options : pseudo, son, volumes, aide aux commandes
Les options, sauvegardées automatiquement.
Écran d'aide avec les commandes clavier, mobile et la liste des ennemis
L’aide intégrée : commandes, conseils et bestiaire.

Sur mobile

En mode paysage, des boutons tactiles apparaissent automatiquement : déplacements, saut et garde à gauche, attaques, esquive et soin à droite. En portrait, le jeu demande de tourner l’appareil.

Contrôles tactiles du jeu de combat React sur smartphone
Le mode Entraînement sur smartphone, avec les boutons tactiles.

Les technologies et pourquoi elles

🟢 Débutant

Voici la pile technique du jeu, avec pour chaque brique la raison de ce choix :

TechnologieRôlePourquoi ce choix
React 18Menus, écrans, HUDIdéal pour les interfaces à états : chaque écran est un composant
ViteServeur de dev et buildDémarrage instantané, rechargement à chaud, build optimisé
Canvas 2DRendu du combatDes milliers de dessins par seconde sans toucher au DOM
Tailwind CSSStylesClasses utilitaires (bg-black, text-cyan-300…) : on stylise sans quitter le JSX
Framer MotionAnimations des menusTransitions fluides en une ligne
React RouterDeux pages : / et /gameDes URL propres, une page d’accueil séparée du jeu
Web Audio APIMusique et bruitagesSons générés par le code : zéro fichier audio à charger
lucide-reactIcônesDes icônes SVG légères et cohérentes
💡
Bon à savoir

React n’est pas un moteur de jeu. L’astuce de ce projet est de lui confier uniquement ce qu’il fait bien (les menus et l’état), et de laisser le combat à un moteur en JavaScript pur qui dessine sur un canvas. C’est ce découpage qui rend un jeu de combat React fluide.

Prérequis

🟢 Débutant
Il te fautDétail
Node.js 20 LTS (ou plus récent)Exécute les outils de développement (Vite, npm) : à télécharger sur nodejs.org
Un éditeur de codeVS Code recommandé (voir nos meilleures extensions VS Code)
Un navigateur récentChrome, Edge, Firefox ou Safari
Git (facultatif)Pour cloner le dépôt et suivre tes modifications (voir Git et GitHub pour débutant)
Les bases de JavaScriptVariables, fonctions, classes, modules import / export. Les bases de React aident pour les sections 🟡

Vérifie ton installation dans un terminal (sous Windows : PowerShell ou le terminal intégré de VS Code) :

node -v    # doit afficher v20.x.x ou plus
npm -v     # doit afficher 10.x.x ou plus
Terminal
⚠️
Débutant sous Windows ?

Pendant l’installation de Node.js, garde toutes les options par défaut : npm (le gestionnaire de paquets) est installé en même temps. Ferme puis rouvre ton terminal après l’installation, sinon la commande npm ne sera pas reconnue.

Installer et lancer le jeu en 5 minutes

🟢 Débutant

Tu veux d’abord voir le jeu tourner avant de comprendre le code ? Récupère le projet depuis GitHub, au choix sans ou avec Git.

Option A : télécharger le ZIP (sans Git)

  1. Ouvre le dépôt GitHub de Cyber Arena.
  2. Clique sur le bouton vert Code, puis sur Download ZIP.
  3. Décompresse l’archive où tu veux (par exemple Documents/projets/cyber-arena).
  4. Ouvre ce dossier dans VS Code : Fichier → Ouvrir le dossier…

Option B : cloner avec Git

git clone https://github.com/LM-Code-Be/cyber-arena.git
cd cyber-arena
Terminal

Installer les dépendances

Dans le terminal, à la racine du projet (le dossier qui contient package.json) :

npm install
Terminal

Cette commande télécharge toutes les bibliothèques dans un dossier node_modules/. Compte de 30 secondes à 2 minutes. Les éventuels messages npm warn ne sont pas bloquants.

📦
Pourquoi node_modules n’est pas dans le ZIP ?

Ce dossier pèse souvent des centaines de Mo et se recrée en une commande : il n’est donc jamais versionné sur GitHub. npm install le reconstruit à partir de package.json et package-lock.json.

Lancer le jeu

npm run dev
Terminal

Ouvre ensuite http://localhost:3000 dans ton navigateur. C’est tout : tu joues déjà à ton jeu de combat React.

Tester le jeu de toutes les manières

🟢 Débutant

Le projet propose plusieurs scripts, définis dans package.json :

CommandeRôle
npm run devServeur de développement avec rechargement instantané
npm run buildGénère la version de production optimisée dans dist/
npm run previewSert localement le contenu de dist/
npm run lintAnalyse le code pour détecter les erreurs

Le mode développement

Avec npm run dev, chaque fichier enregistré met la page à jour tout seul, sans perdre l’état de l’application : c’est le Hot Module Replacement. Pour arrêter le serveur : Ctrl + C dans le terminal.

💡
Essaie tout de suite

Ouvre src/game/constants.js, remplace name: 'Guerrier' par name: 'Gladiateur', enregistre, et regarde l’écran de sélection changer instantanément.

La version de production

C’est exactement ce qui sera mis en ligne :

npm run build
npm run preview
Terminal

Le build produit un dossier dist/ contenant du HTML, du CSS et du JavaScript minifiés et découpés en plusieurs fichiers. npm run preview le sert sur http://localhost:3000 pour vérifier que tout fonctionne avant la mise en ligne.

⚠️
N’ouvre pas dist/index.html en double-cliquant

Le jeu utilise des routes (/game) et des modules JavaScript qui exigent un vrai serveur web. Ouvert directement depuis le disque (file://), tu obtiendrais un écran blanc. Passe toujours par npm run preview.

Sur ton téléphone, via le Wi-Fi

Le serveur est configuré avec host: true dans vite.config.js : il est donc accessible depuis les autres appareils de ton réseau. Au lancement de npm run dev, Vite affiche deux adresses :

  ➜  Local:   http://localhost:3000/
  ➜  Network: http://192.168.1.42:3000/
Sortie de Vite

Tape l’adresse Network dans le navigateur du téléphone, puis tourne-le en paysage : les contrôles tactiles apparaissent automatiquement. Si la page ne charge pas, autorise Node.js dans le pare-feu Windows (une fenêtre le propose au premier lancement) et vérifie que le téléphone est bien sur le même Wi-Fi.

Simuler un mobile depuis le PC

Dans Chrome ou Edge : F12 → icône Toggle device toolbar (Ctrl + Shift + M) → choisis un modèle de téléphone et passe en paysage. Recharge la page (F5) pour que le jeu détecte l’écran tactile.

Vérifier la qualité du code

npm run lint
Terminal

Aucune sortie signifie aucune erreur détectée par ESLint.

Remettre sa progression à zéro

La sauvegarde est stockée dans le navigateur (localStorage, clé cyberArena_save_v2). Deux options :

  • dans le jeu : Options → Réinitialiser la progression ;
  • ou dans les outils de développement : F12 → Application → Local Storage → supprime la clé.

L’architecture du jeu de combat React

🟡 Intermédiaire

Plutôt qu’un seul gros fichier, le projet est découpé en dossiers qui ont chacun un rôle précis :

cyber-arena/
├── index.html                 Page HTML de base (titre, polices, favicon)
├── package.json               Dépendances et scripts npm
├── vite.config.js             Configuration de Vite (port, alias @, découpage du build)
├── tailwind.config.js         Couleurs et polices du thème
├── public/                    Fichiers copiés tels quels (logo, favicons, .htaccess)
└── src/
    ├── main.jsx               Point d'entrée : monte <App /> dans la page
    ├── App.jsx                Routes / et /game
    ├── index.css              Variables CSS et classes utilitaires (néons, grille…)
    ├── pages/
    │   ├── HomePage.jsx       Page d'accueil
    │   └── GamePage.jsx       Page du jeu
    ├── contexts/
    │   └── GameContext.jsx    État global : phase, profil, sauvegarde
    ├── game/                  Le moteur — JavaScript pur, sans React
    │   ├── constants.js       Toutes les données : personnages, ennemis, niveaux…
    │   ├── engine.js          Boucle de jeu, physique, IA, collisions, boss
    │   ├── render.js          Dessin des décors et des combattants
    │   └── audio.js           Synthétiseur de musique et d'effets sonores
    └── components/
        ├── brand/LmCodeCredit.jsx
        ├── ui/                Composants génériques (bouton, slider, toast)
        └── game/
            ├── GameContainer.jsx   Choisit l'écran à afficher selon la phase
            ├── GameStage.jsx       Crée le <canvas> et le moteur
            ├── GameHUD.jsx         Vie, stamina, score par-dessus le canvas
            ├── TouchControls.jsx   Boutons tactiles
            └── ui/                 Tous les écrans : menu, boutique, pause…
Arborescence

Le principe clé : deux mondes qui se parlent peu

Le jeu est séparé en deux parties indépendantes, qui ne communiquent que par une configuration au départ et des événements à l’arrivée :

┌────────────────────────────┐  config (perso, niveau, upgrades)  ┌──────────────────────────┐
│ REACT (components,         │ ─────────────────────────────────▶ │ MOTEUR (src/game)        │
│        contexts)           │                                    │ engine.js · render.js    │
│ Menus, boutique, HUD,      │ ◀───────────────────────────────── │ 60 mises à jour / s      │
│ sauvegarde                 │  événements : pause, levelCleared, │ dessin sur <canvas>      │
└────────────────────────────┘  defeat, victory (+ statistiques)  └──────────────────────────┘
Schéma

React ne se re-rend pas 60 fois par seconde : il gère les menus et la sauvegarde, et le moteur gère le combat. Si chaque mouvement de personnage passait par un setState, React recalculerait l’interface en permanence et le jeu saccaderait, surtout sur mobile.

💡
Pourquoi c’est une bonne architecture

Le moteur ne connaît pas React : il reçoit un <canvas>, une configuration et un callback. Tu pourrais le réutiliser tel quel avec Vue, Svelte ou du JavaScript sans framework, et le tester sans navigateur pour la logique.

Les données du jeu : constants.js

🟢 Débutant

Tout ce qui concerne l’équilibrage se trouve dans un seul fichier, src/game/constants.js. Les chiffres sont séparés de la logique : pour rendre un ennemi plus dangereux, tu modifies une valeur, pas un algorithme. Exemple avec les ennemis :

export const ENEMY_TYPES = {
  Grunt:  { name: 'Grunt', hp: 45, speed: 2.2, damage: 9, reach: 46, score: 100, coins: [6, 10], color: '#ff4d4d', ... },
  Archer: { name: 'Archer', hp: 35, damage: 10, reach: 520, ranged: 'arrow', ... },
  // Assassin, Berserker, Mage…
};
src/game/constants.js

On y trouve aussi :

ConstanteContenu
CHARACTERSLes 3 combattants : couleurs, vie, vitesse, dégâts, défense, critique, double saut
ENEMY_TYPESGrunt, Archer, Assassin, Berserker, Mage : vie, portée, dégâts, score, crédits
DIFFICULTIESFacile, Normal, Difficile : multiplicateurs de dégâts, de vie et de score
UPGRADESLes 4 améliorations de la boutique, leur effet et leur prix par niveau
CAMPAIGN_LEVELSLes 5 niveaux : nom, palette de couleurs du décor et composition des vagues
CONTROLSLa liste des touches, réutilisée par l’aide et le panneau de commandes

Chaque ennemi a son rôle tactique : le Grunt est lent mais tenace, l’Archer tire à distance (saute ou bloque), l’Assassin se jette sur toi, le Berserker est insensible aux coups rapides (utilise les coups lourds et l’onde plasma) et le Mage lance des orbes qui visent ta position.

💡
Exercice

Passe hp: 45 à hp: 200 pour le Grunt, enregistre, et lance une partie : tu sentiras immédiatement la difficulté changer.

Le moteur de jeu : engine.js

🔴 Confirmé

Le cœur du jeu de combat React est… une classe JavaScript classique, totalement indépendante de React. Elle s’utilise en trois méthodes :

const engine = new Engine(canvas, config, onEvent);
engine.start();          // lance la boucle
engine.setPaused(true);  // fige le jeu
engine.destroy();        // nettoie tout (écouteurs clavier, boucle)
Utilisation du moteur

La boucle de jeu à pas fixe

requestAnimationFrame appelle ta fonction à chaque rafraîchissement de l’écran. Problème : un écran 144 Hz l’appelle plus de deux fois plus souvent qu’un écran 60 Hz. Si on déplaçait le joueur de 5 pixels par appel, il irait deux fois plus vite sur un écran gaming !

La solution : la logique avance par pas fixes de 1/60 s, quel que soit l’écran. On accumule le temps réellement écoulé, puis on exécute autant de pas que nécessaire :

loop(now) {
  const dt = Math.min(now - this.last, 250);
  this.last = now;
  if (!this.paused) {
    this.acc += dt;                         // temps accumulé
    let steps = 0;
    while (this.acc >= STEP && steps < 5) { // STEP = 1000 / 60
      this.update();                        // physique, IA, collisions
      this.acc -= STEP;
      steps++;
    }
    if (steps === 5) this.acc = 0;          // évite la "spirale de la mort" après un lag
  }
  this.render();                            // dessin
  this.raf = requestAnimationFrame(this.loop);
}
src/game/engine.js

Trois détails importants :

  • Math.min(..., 250) : si l’onglet est resté en arrière-plan plusieurs secondes, on ne veut pas simuler toutes ces secondes d’un coup.
  • steps < 5 : on limite le nombre de pas par image. Sans cette limite, un appareil lent prendrait de plus en plus de retard, ferait de plus en plus de pas, et finirait figé : c’est la spirale de la mort.
  • render() est appelé à chaque image, même en pause : le décor reste affiché, mais plus rien ne bouge.
🎓
Le même principe que les grands moteurs

Cette technique (fixed timestep) est celle d’Unity (FixedUpdate) ou de Godot (_physics_process). Elle rend la physique déterministe : un saut a toujours la même hauteur, sur tous les appareils.

Ce que fait update() à chaque pas

  • Le joueur (updatePlayer) : déplacements, gravité, saut et double saut, garde, esquive avec invulnérabilité, régénération de la stamina et les 3 attaques.
  • Les ennemis (updateEnemies) : une machine à états, détaillée juste en dessous.
  • Le boss TITAN-X (updateBoss) : 3 phases selon sa vie restante.
  • Les vagues (startNextWave, onWaveCleared) : apparition des ennemis et passage à la vague suivante.
  • Le game feel : hitstop, tremblement d’écran, combos, textes de dégâts flottants et particules.

La physique et les collisions

Chaque combattant possède une position (x, y), une vitesse (vx, vy) et un booléen onGround. À chaque pas, la gravité augmente vy, puis la position avance de la vitesse. Quand le personnage touche le sol, on le replace dessus et on remet vy à zéro. Un saut, c’est simplement donner une vitesse verticale négative (vers le haut).

Pour les coups, on utilise des hitbox : pendant quelques images d’une attaque, un rectangle invisible apparaît devant le personnage. Si ce rectangle chevauche celui d’un ennemi, le coup touche. Le test de chevauchement de deux rectangles (AABB) tient en une ligne :

// Deux rectangles se chevauchent s'ils se recouvrent sur les deux axes
const overlap = (a, b) =>
  a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y;
Exemple simplifié

L’IA des ennemis : une machine à états

Un ennemi qui frappe dès qu’il est à portée serait injuste : impossible de réagir. Chaque ennemi suit donc un cycle de 4 états :

APPROCHE ──▶ PRÉPARATION ──▶ ATTAQUE ──▶ RÉCUPÉRATION ──┐
   ▲         (« ! » affiché)                            │
   └────────────────────────────────────────────────────┘
Machine à états
  • Approche : l’ennemi marche vers le joueur jusqu’à être à portée (reach).
  • Préparation : il s’arrête et un « ! » apparaît au-dessus de lui. C’est le télégraphe : ton signal pour bloquer ou esquiver.
  • Attaque : la hitbox est active pendant quelques images.
  • Récupération : l’ennemi est vulnérable un court instant. C’est la fenêtre idéale pour contre-attaquer.

Ce schéma est la base de presque tous les jeux d’action : il rend le combat lisible, et la difficulté se règle simplement en raccourcissant la phase de préparation.

Le boss TITAN-X en 3 phases

Le boss change de comportement selon sa vie restante, ce qui oblige le joueur à adapter sa stratégie :

AttaqueComment l’éviter
Charge à travers l’arèneTraverser le boss avec l’esquive (invulnérabilité)
Onde de choc au solSauter au bon moment
Barrage de tirs laserBloquer avec la garde
Invocation de sbiresLes éliminer vite avant qu’ils ne t’encerclent

Le « game feel » : ce qui rend les coups satisfaisants

Un jeu peut être techniquement correct et pourtant sembler mou. Ce qui fait la différence, ce sont de petits effets presque invisibles :

  • Hitstop : le jeu se fige pendant quelques millisecondes à l’impact. Le cerveau perçoit alors un coup « lourd ».
  • Tremblement d’écran : un décalage aléatoire du dessin, qui s’atténue rapidement.
  • Particules et textes flottants : les dégâts s’affichent et s’envolent au-dessus de l’ennemi.
  • Combos : enchaîner les coups augmente un multiplicateur de score (jusqu’à ×2) ; être touché le remet à zéro.

La communication vers React

Le moteur ne modifie jamais l’état React directement. Il appelle un simple callback, que GameStage.jsx traduit en actions du reducer :

(type, stats) => {
  if (type === 'pause') dispatch({ type: 'PAUSE' });
  else if (type === 'levelCleared') dispatch({ type: 'LEVEL_CLEARED', payload: stats });
  else if (type === 'defeat' || type === 'victory') dispatch({ type: 'RUN_ENDED', payload: { result: type, stats } });
}
src/components/game/GameStage.jsx

Le rendu Canvas : render.js

🔴 Confirmé

Une résolution interne fixe

Le canvas fait toujours 1280 × 720 pixels en interne, puis il est étiré en CSS pour remplir l’écran en gardant le format 16:9. Toutes les coordonnées du jeu restent donc identiques sur un smartphone comme sur un écran 4K : une arène de 1280 de large, point.

canvas.width = 1280;   // résolution interne : ne change jamais
canvas.height = 720;
// En CSS : width: 100%; aspect-ratio: 16 / 9; → le navigateur s'occupe de l'agrandissement
Exemple simplifié

Un décor pré-dessiné et la parallaxe

Redessiner des centaines d’immeubles et de fenêtres 60 fois par seconde serait coûteux. Les immeubles sont donc pré-dessinés une seule fois dans un canvas hors écran (createBackground), puis simplement recopiés à chaque image avec drawImage. En les décalant moins vite que le premier plan, on obtient un effet de parallaxe qui donne de la profondeur. La pluie, les néons et la lune sont ajoutés par-dessus.

L’effet néon

Le style cyberpunk repose sur une propriété du Canvas : l’ombre portée colorée. Avec un flou et une couleur vive, une simple ligne devient un tube de néon :

ctx.shadowBlur = 18;
ctx.shadowColor = '#22d3ee'; // halo cyan
ctx.fillStyle = '#22d3ee';
ctx.fillRect(x, y, w, h);    // le rectangle « brille »
ctx.shadowBlur = 0;          // à remettre à zéro : l'ombre coûte cher
Exemple simplifié
⚠️
Attention aux performances

shadowBlur est l’une des opérations les plus lentes du Canvas. Utilise-la sur les éléments importants (personnages, néons) et jamais sur des centaines de particules.

Le son généré par le code : audio.js

🟡 Intermédiaire

Pas un seul fichier MP3 : les sons sont synthétisés avec la Web Audio API, à partir d’oscillateurs, de bruit filtré et d’enveloppes de volume. Le jeu reste ainsi très léger et se charge instantanément.

audio.unlock();          // obligatoire après un premier clic/touche (règle des navigateurs)
audio.playMusic('boss'); // 'menu' | 'battle' | 'boss'
audio.sfx('hit');        // bruitage ponctuel
src/game/audio.js

Le principe d’un bruitage tient en quelques lignes : un oscillateur produit une onde, un gain contrôle son volume, et on fait chuter ce volume très vite pour obtenir un son court et percutant :

const ctx = new AudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'square';                                       // son « 8 bits »
osc.frequency.setValueAtTime(440, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(110, ctx.currentTime + 0.15); // la note descend
gain.gain.setValueAtTime(0.3, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.15);   // enveloppe courte
osc.connect(gain).connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 0.15);
Exemple simplifié
🔇
Pourquoi unlock() ?

Les navigateurs interdisent de jouer du son avant la première interaction de l’utilisateur (clic, touche, tap). L’AudioContext est donc créé ou repris au premier clic : sans ça, le jeu resterait muet.

L’état global et la sauvegarde : GameContext.jsx

🟡 Intermédiaire

Un useReducer, partagé par un contexte React, centralise l’état. La phase détermine l’écran affiché :

MENU → MODE_SELECT → CHARACTER_SELECT → PLAYING ⇄ PAUSED
                                          │
                     LEVEL_COMPLETE ◀─────┤──────▶ GAME_OVER / VICTORY
                     (boutique, niveau suivant)
Phases du jeu

GameContainer.jsx n’a plus qu’à faire un switch sur cette phase pour afficher le bon composant. Chaque transition passe par une action du reducer (PAUSE, LEVEL_CLEARED, RUN_ENDED…) : on sait toujours d’où vient un changement d’écran, ce qui facilite énormément le débogage.

Le profil (crédits, améliorations, scores, statistiques, options) est enregistré automatiquement dans le localStorage à chaque modification :

useEffect(() => {
  try {
    localStorage.setItem(SAVE_KEY, JSON.stringify(state.profile));
  } catch {
    // Stockage indisponible (navigation privée) : le jeu reste jouable sans sauvegarde.
  }
}, [state.profile]);
src/contexts/GameContext.jsx

Le try / catch n’est pas un détail : en navigation privée sur certains navigateurs, ou quand le stockage est plein, setItem lève une exception. Sans protection, le jeu planterait au premier achat en boutique.

💡
Le suffixe _v2 de la clé

La clé cyberArena_save_v2 contient un numéro de version. Si tu changes un jour la structure du profil, passe à _v3 : les anciennes sauvegardes incompatibles seront ignorées au lieu de casser le jeu.

Le HUD et les contrôles tactiles

🟡 Intermédiaire
  • **GameHUD.jsx** lit l’état du moteur (engine.getHud()) toutes les 80 ms au lieu de 60 fois par seconde. C’est largement suffisant pour l’œil, et beaucoup plus léger pour React.
  • **TouchControls.jsx* utilise les *Pointer Events et setPointerCapture : un bouton reste « appuyé » même si le doigt glisse légèrement à côté. Chaque bouton appelle simplement engine.setInput('left', true/false), exactement comme le clavier.
  • Les écrans tactiles sont détectés avec matchMedia('(pointer: coarse)'), et la hauteur utilise 100dvh au lieu de 100vh pour tenir compte des barres d’adresse mobiles qui apparaissent et disparaissent.

Le clavier et le tactile partagent donc la même entrée du moteur : ajouter un nouveau périphérique (une manette, par exemple) revient à appeler setInput depuis une nouvelle source.

Transformer le jeu en PWA installable

🟡 Intermédiaire

Une PWA (Progressive Web App) s’installe sur un PC ou un téléphone comme une vraie application : icône sur le bureau ou l’écran d’accueil, lancement en plein écran et fonctionnement hors ligne. Comme Cyber Arena n’a besoin d’aucun serveur, c’est le candidat idéal. Pour les bases du sujet, vois aussi notre guide pour créer une PWA installable en HTML, CSS et JavaScript.

1. Installer le plugin

npm install -D [email protected]
Terminal

La version 0.20.x est compatible avec Vite 4, utilisé par ce projet.

2. Préparer les icônes

Crée deux images PNG carrées dans public/ : pwa-192.png (192 × 192 px) et pwa-512.png (512 × 512 px). Tu peux les générer à partir de public/favicon.png avec n’importe quel éditeur d’image, ou avec un outil en ligne comme RealFaviconGenerator.

3. Configurer Vite

Dans vite.config.js, importe le plugin et ajoute-le à la liste plugins :

import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      registerType: 'autoUpdate',
      includeAssets: ['favicon.png', 'lm-code-logo.jpg'],
      manifest: {
        name: 'Cyber Arena Fighter',
        short_name: 'Cyber Arena',
        description: 'Jeu de combat 2D cyberpunk — par LM-Code',
        lang: 'fr',
        start_url: '/game',
        scope: '/',
        display: 'fullscreen',
        orientation: 'landscape',
        background_color: '#0a0014',
        theme_color: '#0a0014',
        icons: [
          { src: 'pwa-192.png', sizes: '192x192', type: 'image/png' },
          { src: 'pwa-512.png', sizes: '512x512', type: 'image/png' },
          { src: 'pwa-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
        ],
      },
      workbox: {
        globPatterns: ['**/*.{js,css,html,png,jpg,svg,woff2}'],
        navigateFallback: '/index.html',
        runtimeCaching: [
          {
            // Polices Google mises en cache pour le mode hors ligne
            urlPattern: /^https:\/\/fonts\.(googleapis|gstatic)\.com\/.*/i,
            handler: 'CacheFirst',
            options: {
              cacheName: 'google-fonts',
              expiration: { maxEntries: 20, maxAgeSeconds: 60 * 60 * 24 * 365 },
            },
          },
        ],
      },
    }),
  ],
  // ... le reste de la configuration ne change pas
});
vite.config.js

Ce que fait cette configuration :

  • manifest génère le fichier manifest.webmanifest : nom, icônes, couleurs et mode d’affichage de l’application installée ;
  • workbox génère un service worker qui met en cache tous les fichiers du jeu pour qu’il démarre sans connexion ;
  • registerType: 'autoUpdate' installe automatiquement les nouvelles versions lors de tes mises à jour ;
  • orientation: 'landscape' et display: 'fullscreen' lancent le jeu en plein écran, en paysage.

Le plugin injecte tout seul le lien vers le manifest et l’enregistrement du service worker dans index.html au moment du build. La balise <meta name="theme-color">, qui colore la barre du navigateur sur mobile, est déjà présente.

4. Tester la PWA

Le service worker n’est actif qu’en production :

npm run build
npm run preview
Terminal
  1. Ouvre http://localhost:3000 dans Chrome ou Edge.
  2. F12 → Application → Manifest : vérifie qu’il n’y a pas d’erreur et que les icônes s’affichent.
  3. Onglet Service workers : le statut doit être activated and is running.
  4. Coche Offline dans ce même onglet et recharge la page : le jeu fonctionne toujours.
  5. Une icône Installer apparaît dans la barre d’adresse : clique dessus, le jeu s’ouvre dans sa propre fenêtre.

Pour un audit complet : F12 → Lighthouse → Analyze page load.

5. Installer sur téléphone

⚠️
HTTPS obligatoire

Une PWA exige HTTPS (sauf sur localhost). Pour l’installer sur un téléphone, il faut donc d’abord mettre le jeu en ligne (section suivante).

  • Android (Chrome) : menu ⋮ → Installer l’application.
  • iPhone (Safari) : bouton Partager → Sur l’écran d’accueil.

Mettre le jeu en ligne

🟢 Débutant

Le résultat de npm run build (le dossier dist/) est un site 100 % statique : n’importe quel hébergeur fait l’affaire, même le moins cher.

🧭
Point important : les routes

Le jeu utilise des URL propres (/game). Si quelqu’un recharge la page directement sur /game, le serveur doit renvoyer index.html au lieu d’une erreur 404. Chaque option ci-dessous indique comment faire.

Hébergement classique (Apache, cPanel, Hostinger…)

  1. Lance npm run build.
  2. Envoie le contenu du dossier dist/ (pas le dossier lui-même) dans public_html/ via FTP ou le gestionnaire de fichiers.
  3. C’est tout : le fichier .htaccess fourni dans public/ est copié automatiquement dans dist/ et gère déjà la redirection des routes et le cache.

Netlify, Vercel ou Cloudflare Pages

Connecte ton dépôt GitHub, puis indique :

RéglageValeur
Commande de buildnpm run build
Dossier publiédist
Version de Node20

Pour les routes : sur Netlify et Cloudflare Pages, crée un fichier public/_redirects contenant :

/*  /index.html  200
public/_redirects

Vercel détecte Vite et gère cela automatiquement. À chaque git push, le jeu est reconstruit et redéployé.

Dans un sous-dossier

Pour publier le jeu à une adresse comme monsite.be/jeux/cyber-arena/, construis-le avec le bon chemin de base :

npx vite build --base=/jeux/cyber-arena/
Terminal

Le routeur utilise déjà import.meta.env.BASE_URL et s’adapte automatiquement. Pense à ajuster RewriteBase dans .htaccess et, si tu as fait la PWA, start_url et scope dans le manifest.

Recréer un jeu de combat React à partir de zéro

🔴 Confirmé

Cette partie explique la démarche pour reconstruire un jeu de ce type toi-même. Le code complet de chaque fichier est sur GitHub : l’idée ici est de comprendre dans quel ordre construire les choses, et pourquoi.

Étape 1 : créer le projet React avec Vite

npm create vite@latest cyber-arena -- --template react
cd cyber-arena
npm install
Terminal

Étape 2 : installer les dépendances

npm install framer-motion lucide-react react-router-dom clsx tailwind-merge class-variance-authority @radix-ui/react-slot @radix-ui/react-slider @radix-ui/react-toast tailwindcss-animate
npm install -D tailwindcss@3 postcss autoprefixer
npx tailwindcss init -p
Terminal

Étape 3 : configurer Tailwind et l’alias @

Dans tailwind.config.js, indique où se trouvent tes fichiers et ajoute tes polices :

import animate from 'tailwindcss-animate';

export default {
  content: ['./index.html', './src/**/*.{js,jsx}'],
  theme: {
    extend: {
      fontFamily: {
        display: ['Orbitron', 'sans-serif'], // titres
        sans: ['Rajdhani', 'sans-serif'],    // texte
      },
    },
  },
  plugins: [animate],
};
tailwind.config.js

Dans src/index.css, remplace tout par les trois directives Tailwind :

@tailwind base;
@tailwind components;
@tailwind utilities;
src/index.css

Dans vite.config.js, ajoute l’alias @ pour écrire import x from '@/game/engine' au lieu de ../../../game/engine :

resolve: { alias: { '@': path.resolve(__dirname, './src') } },
vite.config.js

Enfin, ajoute les polices Google Orbitron et Rajdhani dans le <head> de index.html.

Étape 4 : écrire les données (constants.js)

Commence par les données, avant le code : personnages, ennemis, niveaux, touches. Sortir tous les chiffres dans un seul fichier permet ensuite d’équilibrer le jeu sans toucher à la logique.

Étape 5 : construire le moteur pas à pas

Construis le moteur progressivement, en vérifiant le résultat à chaque étape :

  1. une classe Engine avec la boucle à pas fixe et un rectangle qui se déplace avec les flèches ;
  2. la gravité et le saut : vitesse verticale, sol, onGround ;
  3. une attaque : une hitbox devant le joueur pendant quelques images, testée contre les ennemis ;
  4. un ennemi qui marche vers le joueur, puis la machine à états approche → préparation → attaque → récupération ;
  5. la vie, les dégâts, la mort, puis les vagues et les niveaux ;
  6. le game feel : hitstop, tremblement d’écran, particules, textes flottants. C’est ce qui rend les coups satisfaisants, et c’est souvent négligé.
💡
Astuce de pro

Garde le moteur indépendant de React. Il reçoit un <canvas> et une configuration, et il prévient l’extérieur via un callback onEvent. Tu pourras le tester, le réutiliser ou le porter sur un autre framework.

Étape 6 : le rendu

Dessine d’abord des rectangles de couleur, et seulement ensuite les vrais personnages, le décor en parallaxe et les effets de néon (ctx.shadowBlur + ctx.shadowColor). Fixe une résolution interne (1280 × 720) et étire le canvas en CSS. Tant que le jeu n’est pas amusant avec des rectangles, de beaux graphismes ne le sauveront pas.

Étape 7 : brancher React

  1. GameContext.jsx : un useReducer avec une phase (menu, sélection, jeu, pause, fin…) et un profil sauvegardé dans localStorage.
  2. GameStage.jsx : crée le moteur dans un useEffect et le détruit au démontage. La key={runId-level} force une instance neuve à chaque niveau.
  3. GameContainer.jsx : un simple switch sur la phase pour afficher le bon écran.
  4. GameHUD.jsx : lit engine.getHud() à intervalle régulier.

Voici le squelette de GameStage.jsx, le pont entre les deux mondes :

function GameStage({ config, onEvent }) {
  const canvasRef = useRef(null);

  useEffect(() => {
    const engine = new Engine(canvasRef.current, config, onEvent);
    engine.start();
    return () => engine.destroy(); // nettoyage au démontage : boucle et écouteurs
  }, []);

  return <canvas ref={canvasRef} width={1280} height={720} className="w-full aspect-video" />;
}
Exemple simplifié
⚠️
Le piège du double montage

En développement, le mode strict de React monte, démonte puis remonte chaque composant. Sans le return () => engine.destroy(), tu aurais deux boucles de jeu en parallèle : le jeu tournerait deux fois trop vite et chaque touche compterait double.

Étape 8 : les écrans et la progression

Menu, choix du mode et du personnage, pause, fin de niveau, game over, victoire, boutique, classement, statistiques, options. Framer Motion ajoute des transitions fluides avec <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>.

Étape 9 : le son

Crée un AudioContext au premier clic (les navigateurs bloquent le son avant toute interaction), puis des fonctions qui jouent des oscillateurs avec une enveloppe de volume courte pour les bruitages, et une séquence de notes en boucle pour la musique.

Étape 10 : le mobile

Ajoute les boutons tactiles (Pointer Events), détecte les écrans tactiles avec matchMedia('(pointer: coarse)'), demande le mode paysage, et utilise 100dvh plutôt que 100vh.

Étape 11 : les finitions

  • npm run lint pour nettoyer le code ;
  • npm run build puis npm run preview pour tester la production ;
  • la PWA ;
  • la mise en ligne.

Dépannage

ProblèmeSolution
'npm' n'est pas reconnu…Node.js n’est pas installé, ou le terminal a été ouvert avant l’installation. Réinstalle Node.js puis rouvre le terminal (ou VS Code).
Port 3000 is already in useUn autre programme utilise ce port. Lance npm run dev -- --port 5173, ou ferme l’autre serveur.
Écran blanc après la mise en ligneTu as envoyé le dossier dist/ au lieu de son contenu, ou le site est dans un sous-dossier (voir la mise en ligne).
Erreur 404 en rechargeant /gameLa redirection des routes n’est pas configurée sur l’hébergeur.
Pas de sonClique une fois dans la page (règle des navigateurs), vérifie Options → Son, puis le volume de l’onglet.
Pas de boutons tactiles sur mobileTourne le téléphone en paysage et recharge la page.
Erreurs npm install (ERESOLVE…)Supprime node_modules/ et package-lock.json, vérifie node -v (20 ou plus), puis relance npm install.
PowerShell : « l’exécution de scripts est désactivée »Exécute une fois Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, ou utilise l’invite de commandes cmd.

Une erreur de type 404 ou 500 une fois en ligne ? Notre guide des codes HTTP et de leurs solutions t’aidera à trouver la cause.

Améliorer Cyber Arena Fighter

Quelques idées pour t’approprier le projet, de la plus simple à la plus ambitieuse :

  • 🎨 Un nouveau personnage : ajoute une entrée dans CHARACTERS (couleurs, vitesse, dégâts) et il apparaît automatiquement dans l’écran de sélection.
  • 👾 Un nouvel ennemi : ajoute-le dans ENEMY_TYPES puis place-le dans les vagues de CAMPAIGN_LEVELS.
  • 🗺️ Un 6ᵉ niveau : une nouvelle entrée dans CAMPAIGN_LEVELS avec sa palette de couleurs et ses vagues.
  • 🎮 Le support des manettes avec la Gamepad API (navigator.getGamepads()), à lire dans update().
  • 🌐 Un classement en ligne avec une petite API à la place du localStorage (voir notre API REST avec Node.js, Express et MySQL).
  • 🌍 La traduction : regroupe les textes dans un fichier par langue.

FAQ

Peut-on vraiment créer un jeu de combat avec React ?

Oui, à condition de ne pas faire passer le combat par React. Dans ce projet, React gère les menus, la boutique et la sauvegarde, tandis qu’un moteur en JavaScript pur dessine le combat sur un canvas 60 fois par seconde. C’est ce découpage qui garde le jeu fluide, même sur mobile.

Faut-il connaître React pour suivre ce tutoriel ?

Non pour installer, tester et mettre le jeu en ligne : il suffit de savoir ouvrir un terminal. Les bases de React (composants, useState, useEffect) aident pour les sections intermédiaires, et les sections confirmées demandent surtout de bien connaître JavaScript.

Pourquoi utiliser Canvas plutôt que des éléments HTML ?

Parce qu’un jeu redessine des dizaines d’éléments à chaque image. Le Canvas 2D dessine des pixels directement, sans créer ni mettre à jour d’éléments dans le DOM, ce qui est beaucoup plus rapide pour du temps réel.

Le jeu fonctionne-t-il sur mobile ?

Oui. En mode paysage, des boutons tactiles apparaissent automatiquement, et le jeu demande de tourner l’appareil en mode portrait. Il peut aussi être installé comme une application grâce à la PWA.

Où sont sauvegardés les scores et les crédits ?

Dans le localStorage du navigateur, sous la clé cyberArena_save_v2. Aucun serveur n’est nécessaire. La sauvegarde est propre à chaque navigateur et à chaque appareil.

Peut-on jouer hors ligne ?

Oui, une fois le jeu transformé en PWA avec vite-plugin-pwa. Le service worker met tous les fichiers en cache, et le jeu démarre ensuite même sans connexion.

Où héberger ce jeu de combat React ?

N’importe où : le build est un site statique. Un hébergement Apache classique, Netlify, Vercel ou Cloudflare Pages conviennent. Il faut seulement rediriger les routes comme /game vers index.html.

Conclusion

Tu aimes les projets de jeux ? Découvre aussi notre jeu Snake en Python avec Pygame, notre jeu d’échecs en React TypeScript et notre jeu de dames en Python.

Tu as maintenant un jeu de combat React complet, et surtout tu sais pourquoi il est construit ainsi. En résumé, tu as vu :

  • ✅ comment séparer un moteur de jeu en JavaScript pur de l’interface React ;
  • ✅ comment écrire une boucle à pas fixe qui tourne à la même vitesse sur tous les écrans ;
  • ✅ comment programmer une IA lisible avec une machine à états et un boss en plusieurs phases ;
  • ✅ comment dessiner un univers néon sur Canvas et synthétiser sons et musique sans fichier ;
  • ✅ comment sauvegarder la progression dans le navigateur ;
  • ✅ comment transformer le jeu en PWA et le mettre en ligne.

À toi de jouer : crée ton propre combattant, ajoute un niveau, et partage ton meilleur score en commentaire ! ⚔️

Comments

No comments yet. Why don’t you start the discussion?

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *