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.
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.

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.
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ébutantCyber 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 Campagne | 5 niveaux de difficulté croissante, chacun avec son décor et ses ennemis, et le boss TITAN-X à la fin |
| ♾️ Mode Survie | Des vagues infinies de plus en plus fortes, un boss toutes les 5 vagues, +15 % de PV entre les vagues |
| 🎯 Mode Entraînement | Impossible de mourir, ennemis affaiblis : parfait pour apprendre les commandes |
| 🥷 3 combattants | Guerrier équilibré, Assassin rapide avec double saut, Paladin lent mais très résistant |
| ⚔️ Système de combat | Coup rapide, coup lourd, onde plasma, garde, esquive avec invulnérabilité, combos et soins |
| 🛒 Boutique | 4 améliorations permanentes (5 niveaux chacune) et des soins, payés avec les crédits gagnés en combat |
| 🏆 Classement et stats | Top 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 |
| 📱 Mobile | Boutons tactiles automatiques en paysage, invitation à tourner l’écran en portrait |
Visite guidée du jeu, écran par écran
🟢 DébutantAvant 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.

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.

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.

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).

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.

| Action | Clavier |
|---|---|
| Se déplacer | ← → |
| Sauter (double saut pour l’Assassin) | ↑ ou Espace |
| Coup rapide | A ou J |
| Coup lourd | S ou K |
| Onde plasma (30 stamina) | D ou L |
| Garde (maintenir) | E ou I |
| Esquive (20 stamina) | Maj (Shift) |
| Utiliser un soin | R ou H |
| Pause | P 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.

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.


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.

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.

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.



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.


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.

Les technologies et pourquoi elles
🟢 DébutantVoici la pile technique du jeu, avec pour chaque brique la raison de ce choix :
| Technologie | Rôle | Pourquoi ce choix |
|---|---|---|
| React 18 | Menus, écrans, HUD | Idéal pour les interfaces à états : chaque écran est un composant |
| Vite | Serveur de dev et build | Démarrage instantané, rechargement à chaud, build optimisé |
| Canvas 2D | Rendu du combat | Des milliers de dessins par seconde sans toucher au DOM |
| Tailwind CSS | Styles | Classes utilitaires (bg-black, text-cyan-300…) : on stylise sans quitter le JSX |
| Framer Motion | Animations des menus | Transitions fluides en une ligne |
| React Router | Deux pages : / et /game | Des URL propres, une page d’accueil séparée du jeu |
| Web Audio API | Musique et bruitages | Sons générés par le code : zéro fichier audio à charger |
| lucide-react | Icônes | Des icônes SVG légères et cohérentes |
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 faut | Dé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 code | VS Code recommandé (voir nos meilleures extensions VS Code) |
| Un navigateur récent | Chrome, 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 JavaScript | Variables, 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 plusTerminalPendant 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ébutantTu 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)
- Ouvre le dépôt GitHub de Cyber Arena.
- Clique sur le bouton vert Code, puis sur Download ZIP.
- Décompresse l’archive où tu veux (par exemple
Documents/projets/cyber-arena). - 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-arenaTerminalInstaller les dépendances
Dans le terminal, à la racine du projet (le dossier qui contient package.json) :
npm installTerminalCette 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.
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 devTerminalOuvre 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ébutantLe projet propose plusieurs scripts, définis dans package.json :
| Commande | Rôle |
|---|---|
npm run dev | Serveur de développement avec rechargement instantané |
npm run build | Génère la version de production optimisée dans dist/ |
npm run preview | Sert localement le contenu de dist/ |
npm run lint | Analyse 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.
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 previewTerminalLe 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.
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 ViteTape 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 lintTerminalAucune 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édiairePlutô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…ArborescenceLe 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émaReact 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.
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ébutantTout 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.jsOn y trouve aussi :
| Constante | Contenu |
|---|---|
CHARACTERS | Les 3 combattants : couleurs, vie, vitesse, dégâts, défense, critique, double saut |
ENEMY_TYPES | Grunt, Archer, Assassin, Berserker, Mage : vie, portée, dégâts, score, crédits |
DIFFICULTIES | Facile, Normal, Difficile : multiplicateurs de dégâts, de vie et de score |
UPGRADES | Les 4 améliorations de la boutique, leur effet et leur prix par niveau |
CAMPAIGN_LEVELS | Les 5 niveaux : nom, palette de couleurs du décor et composition des vagues |
CONTROLS | La 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.
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 moteurLa 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.jsTrois 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.
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 :
| Attaque | Comment l’éviter |
|---|---|
| Charge à travers l’arène | Traverser le boss avec l’esquive (invulnérabilité) |
| Onde de choc au sol | Sauter au bon moment |
| Barrage de tirs laser | Bloquer avec la garde |
| Invocation de sbires | Les é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.jsxLe 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'agrandissementExemple 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 cherExemple simplifié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édiairePas 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 ponctuelsrc/game/audio.jsLe 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é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édiaireUn 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 jeuGameContainer.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.jsxLe 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.
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 etsetPointerCapture: un bouton reste « appuyé » même si le doigt glisse légèrement à côté. Chaque bouton appelle simplementengine.setInput('left', true/false), exactement comme le clavier. - Les écrans tactiles sont détectés avec
matchMedia('(pointer: coarse)'), et la hauteur utilise100dvhau lieu de100vhpour 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édiaireUne 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]TerminalLa 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.jsCe que fait cette configuration :
manifestgénère le fichiermanifest.webmanifest: nom, icônes, couleurs et mode d’affichage de l’application installée ;workboxgé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'etdisplay: '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 previewTerminal- Ouvre http://localhost:3000 dans Chrome ou Edge.
F12→ Application → Manifest : vérifie qu’il n’y a pas d’erreur et que les icônes s’affichent.- Onglet Service workers : le statut doit être activated and is running.
- Coche Offline dans ce même onglet et recharge la page : le jeu fonctionne toujours.
- 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
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ébutantLe 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.
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…)
- Lance
npm run build. - Envoie le contenu du dossier
dist/(pas le dossier lui-même) danspublic_html/via FTP ou le gestionnaire de fichiers. - C’est tout : le fichier
.htaccessfourni danspublic/est copié automatiquement dansdist/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églage | Valeur |
|---|---|
| Commande de build | npm run build |
| Dossier publié | dist |
| Version de Node | 20 |
Pour les routes : sur Netlify et Cloudflare Pages, crée un fichier public/_redirects contenant :
/* /index.html 200public/_redirectsVercel 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/TerminalLe 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 installTerminalÉ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 -pTerminalÉ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.jsDans src/index.css, remplace tout par les trois directives Tailwind :
@tailwind base;
@tailwind components;
@tailwind utilities;src/index.cssDans 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.jsEnfin, 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 :
- une classe
Engineavec la boucle à pas fixe et un rectangle qui se déplace avec les flèches ; - la gravité et le saut : vitesse verticale, sol,
onGround; - une attaque : une hitbox devant le joueur pendant quelques images, testée contre les ennemis ;
- un ennemi qui marche vers le joueur, puis la machine à états approche → préparation → attaque → récupération ;
- la vie, les dégâts, la mort, puis les vagues et les niveaux ;
- le game feel : hitstop, tremblement d’écran, particules, textes flottants. C’est ce qui rend les coups satisfaisants, et c’est souvent négligé.
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
GameContext.jsx: unuseReduceravec une phase (menu, sélection, jeu, pause, fin…) et un profil sauvegardé danslocalStorage.GameStage.jsx: crée le moteur dans unuseEffectet le détruit au démontage. Lakey={runId-level}force une instance neuve à chaque niveau.GameContainer.jsx: un simpleswitchsur la phase pour afficher le bon écran.GameHUD.jsx: litengine.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é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 lintpour nettoyer le code ;npm run buildpuisnpm run previewpour tester la production ;- la PWA ;
- la mise en ligne.
Dépannage
| Problème | Solution |
|---|---|
'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 use | Un autre programme utilise ce port. Lance npm run dev -- --port 5173, ou ferme l’autre serveur. |
| Écran blanc après la mise en ligne | Tu 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 /game | La redirection des routes n’est pas configurée sur l’hébergeur. |
| Pas de son | Clique une fois dans la page (règle des navigateurs), vérifie Options → Son, puis le volume de l’onglet. |
| Pas de boutons tactiles sur mobile | Tourne 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_TYPESpuis place-le dans les vagues deCAMPAIGN_LEVELS. - 🗺️ Un 6ᵉ niveau : une nouvelle entrée dans
CAMPAIGN_LEVELSavec sa palette de couleurs et ses vagues. - 🎮 Le support des manettes avec la Gamepad API (
navigator.getGamepads()), à lire dansupdate(). - 🌐 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 ! ⚔️
