Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

TP2e année9 livrables

Code ton premier jeu

Pause dans le fil AppliFrais : on s'amuse. En une séance, tu codes un vrai petit jeu en JavaScript et Canvas — et tu réinvestis variables, fonctions, boucles et événements sans t'en rendre compte.

Par Josselyn C., prof de BTS SIO. Publié le , mis à jour le .

Objectif
dessiner sur un Canvas, animer une boucle de jeu, gérer le clavier, détecter une collision, compter un score
On manipule
une page HTML, un fichier JS, le navigateur
Pré-requis
les bases JS (variables, fonctions, conditions)
Pour qui
SLAM, 2e année
Durée
environ 3 h

Compétences : C2.1 Concevoir et développer une solution applicative

Un jeu, c'est juste : dessiner, bouger, écouter le clavier, vérifier des collisions — en boucle. On y va.

Avant de commencer

Pourquoi cette séance

Coder un jeu est l'un des meilleurs moyens d'ancrer la logique : tu utilises des variables (position, score), des fonctions (dessiner, mettre à jour), une boucle (le moteur du jeu) et des événements (le clavier) — le tout avec un retour visuel immédiat. Et c'est fun. À la fin, tu auras un jeu jouable, à montrer et à améliorer.

Le jeu qu'on construit

Un « attrape-justificatifs » : un panier que tu déplaces en bas de l'écran, des reçus qui tombent du haut. Attrape-les pour marquer des points, rate-en trop et c'est perdu. Simple, mais addictif.

BriqueTu sauras…
Canvasdessiner des formes par programme
Boucle de jeuanimer 60 images par seconde
Clavierdéplacer le panier
Collision & scoredétecter une prise, compter les points

Mise en place

Tout se fait avec un éditeur de code (VS Code) et un navigateur (Chrome, Firefox). Aucune installation lourde, aucun serveur compliqué.

  1. Crée un dossier attrape-justificatifs.
  2. Dedans, deux fichiers côte à côte : index.html et jeu.js.
  3. Pour lancer le jeu : soit tu double-cliques sur index.html, soit (mieux) tu utilises l'extension Live Server de VS Code (clic droit → « Open with Live Server »).
  4. Pour voir tes changements : enregistre (Ctrl/Cmd + S) puis recharge l'onglet (F5). Avec Live Server, ça se recharge tout seul.

0. La toile (Canvas)

Le Canvas est une zone de dessin dans une page web : un rectangle où l'on dessine par programme (formes, images, texte). Crée deux fichiers côte à côte.

index.html

html
<!DOCTYPE html>
<html lang="fr">
<head><meta charset="utf-8"><title>Attrape-justificatifs</title></head>
<body>
  <canvas id="jeu" width="480" height="360" style="border:1px solid #333"></canvas>
  <script src="jeu.js"></script>
</body>
</html>

jeu.js — accrocher le pinceau

const canvas = document.getElementById("jeu");
const ctx = canvas.getContext("2d");
console.log("Canvas prêt :", canvas.width, "x", canvas.height);

Chiffres-repères Canvas

PropriétéValeur / exempleCe que ça change
canvas.width / .height (attribut HTML)480 × 360 px dans notre jeuDéfinit la résolution de la toile (combien de pixels réels existent)
CSS width/height sur le <canvas>indépendant de l'attributDéfinit la taille d'affichage. Si différent de l'attribut → image étirée ou pixelisée
window.devicePixelRatio1 (standard) · 2 (Retina) · 3 (mobile haut de gamme)Sur Retina, doubler la résolution : canvas.width = 960; canvas.style.width = "480px"; → net
Coordonnées(0,0) = coin haut-gauche ; y croît vers le basInverse du repère mathématique classique
fillStyle (couleur)n'importe quelle valeur CSS"#5b2d8e", "rgb(91,45,142)", "rgba(91,45,142,0.5)", "violet" — tous valides

Dans la console, tu dois lire exactement :

Ce que tu dois voir
Canvas prêt : 480 x 360

Flag B1

Production

Crée les deux fichiers et affiche le message dans la console. Rends : confirme que le canvas s'affiche et le message apparaît.

0 sur 9 dans cette étape

1. Dessiner

On dessine avec ctx. Le repère : (0,0) en haut à gauche, x vers la droite, y vers le bas.

ctx.fillStyle = "#5b2d8e";        // la couleur
ctx.fillRect(50, 40, 120, 30);    // x, y, largeur, hauteur

Les méthodes de dessin essentielles — vue d'ensemble

MéthodeCe qu'elle faitSignature / syntaxeNotes pro
ctx.fillRect(x,y,l,h)Rectangle pleincoin haut-gauche + largeur + hauteurOpération la plus rapide : pas de chemin (path) intermédiaire
ctx.strokeRect(x,y,l,h)Rectangle vide (contour)Même signature ; épaisseur = ctx.lineWidth (défaut 1 px)Astuce debug : dessine la hitbox autour de tes objets pour voir les collisions
ctx.clearRect(x,y,l,h)Efface une zone (→ transparent)Même signatureclearRect(0,0,w,h) efface tout le canvas — obligatoire en début de boucle
ctx.fillText(txt,x,y)Texte pleiny = ligne de base (baseline), pas le haut du textePolice via ctx.font = "16px monospace" (syntaxe CSS exacte)
ctx.arc(cx,cy,r,0,Math.PI*2)Cercle (chemin)cx/cy = centre ; r = rayon ; 0→2π = cercle completObligatoirement encadré par ctx.beginPath() et ctx.fill() ou ctx.stroke()
ctx.drawImage(img,x,y,l,h)Image ou spriteimg = objet Image déjà chargéPrécharger avant la boucle avec img.onload = () => boucle() — sinon image noire

Schéma : (0,0) coin haut-gauche · x augmente vers la droite · y augmente vers le bas · panier : y grand = en bas

Flag B2

Production

Dessine un panier (rectangle large et plat) en bas de la toile, et un reçu (petit carré) en haut. Rends : ton code et une capture.

0 sur 9 dans cette étape

2. La boucle de jeu

Un jeu, c'est une image redessinée ~60 fois par seconde. À chaque image : on efface, on met à jour, on redessine. C'est la boucle de jeu.

Schéma : 1. Effacer · clearRect() · 2. Mettre à jour · positions, collisions · 3. Redessiner · fillRect, fillText · ~60 fois par seconde — requestAnimationFrame(boucle)

function boucle() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);  // 1. effacer

  ctx.fillStyle = "#5b2d8e";                          // 2. (re)dessiner
  ctx.fillRect(200, 320, 80, 16);

  requestAnimationFrame(boucle);                      // 3. recommencer
}
boucle();

requestAnimationFrame vs setInterval — pourquoi rAF ?

requestAnimationFramesetInterval(fn, 16)
CadenceSynchronisée sur l'écran (60, 90 ou 144 Hz selon le moniteur)Fixe à ~16 ms, non synchronisée → déchirures visuelles (tearing)
Onglet en arrière-planS'arrête automatiquement → CPU économisé, batterie préservéeContinue à tourner → gaspillage inutile
Précision du timingFournit un timestamp haute résolution (performance.now()) pour calculer le deltaTimeDrift possible : le minuteur glisse sur le temps
Pause / repriseRetourne un id → cancelAnimationFrame(id) pour stopperclearInterval(id) — même principe

Exemple travaillé — vitesse indépendante du taux de rafraîchissement

requestAnimationFrame passe automatiquement un timestamp en millisecondes à ta fonction. La différence entre deux appels donne le deltaTime :

let dernierTs = 0;

function boucle(ts) {                          // ts = ms depuis le chargement de la page
  if (dernierTs === 0) { dernierTs = ts; }     // 1ère image : évite le saut initial
  const dt = (ts - dernierTs) / 16.67;        // 16,67 ms = 1 image à 60 Hz
  dernierTs = ts;

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  recu.y += 3 * dt;           // 3 px/image à 60 Hz, identique sur un écran à 144 Hz

  requestAnimationFrame(boucle);
}
requestAnimationFrame(boucle);   // rAF injecte le ts tout seul
Moniteurdt (≈)déplacement effectif 3 * dtVitesse perçue
60 Hz1,003,0 pxRéférence
90 Hz0,672,0 pxIdentique (60 Hz × 0,67 = même px/s)
144 Hz0,421,25 pxIdentique
36 Hz (lag)1,675,0 pxIdentique — le jeu ralentit visuellement mais la physique reste correcte

Flag A1

Checkpoint

Décris les 3 étapes d'une image de jeu, et explique pourquoi on doit effacer la toile à chaque tour. Rends : ta réponse.

0 sur 9 dans cette étape

3. Bouger le panier

La position du panier devient une variable. On la modifie, et la boucle le redessine au bon endroit.

let panierX = 200;
const panierY = 320, panierL = 80, panierH = 16;

Écouter le clavier

const touches = {};
document.addEventListener("keydown", (e) => {
  touches[e.key] = true;
  if (e.key.startsWith("Arrow")) e.preventDefault();  // pas de scroll
});
document.addEventListener("keyup", (e) => { touches[e.key] = false; });

Déplacer dans la boucle

function boucle() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  if (touches["ArrowLeft"])  panierX -= 6;
  if (touches["ArrowRight"]) panierX += 6;
  // rester dans la toile
  panierX = Math.max(0, Math.min(canvas.width - panierL, panierX));

  ctx.fillStyle = "#5b2d8e";
  ctx.fillRect(panierX, panierY, panierL, panierH);

  requestAnimationFrame(boucle);
}
boucle();

Math.max / Math.min — le clamp expliqué

Math.max(0, Math.min(canvas.width - panierL, panierX)) est un clamp : force une valeur dans un intervalle [min, max]. Voici les trois cas possibles :

SituationpanierX brutRésultatEffet visible
Normal (dans la toile)200200Pas touché — dans l'intervalle
Sorti à gauche-100Bloqué au bord gauche
Sorti à droite (toile 480, panier 80)430400Bloqué au bord droit

Flag B3

Production

Fais bouger le panier au clavier, sans qu'il sorte de la toile. Rends : ton code et confirme que ← → fonctionnent et que le panier reste dans le cadre.

0 sur 9 dans cette étape

4. Faire tomber un reçu

Un reçu = un objet avec une position. Il descend à chaque image, et réapparaît en haut quand il sort en bas.

let recu = { x: 100, y: 0, taille: 20 };

function nouveauRecu() {
  recu.x = Math.random() * (canvas.width - recu.taille);
  recu.y = -recu.taille;   // juste au-dessus de la toile
}

Dans la boucle, après le panier :

  recu.y += 3;                              // il tombe
  if (recu.y > canvas.height) nouveauRecu(); // sorti en bas → on en relance un

  ctx.fillStyle = "#e8923a";
  ctx.fillRect(recu.x, recu.y, recu.taille, recu.taille);

Flag B4

Production

Fais tomber un reçu qui réapparaît en haut (à une position aléatoire) quand il atteint le bas. Rends : ton code et une capture.

0 sur 9 dans cette étape

5. Attraper : collision & score

Le cœur du jeu : détecter que le reçu touche le panier. Deux conditions : le reçu est à la hauteur du panier, ET aligné horizontalement avec lui.

Schéma : Chevauchement = prise : le reçu touche le panier · panier · reçu · panierX · panierX + panierL · recu.x

let score = 0;

function attrape() {
  return recu.y + recu.taille >= panierY &&   // descendu jusqu'au panier
         recu.x + recu.taille >= panierX &&    // pas trop à gauche
         recu.x <= panierX + panierL;          // pas trop à droite
}

Dans la boucle, après avoir bougé le reçu :

  if (attrape()) { score++; nouveauRecu(); }

  ctx.fillStyle = "#1a1426";
  ctx.font = "16px monospace";
  ctx.fillText("Score : " + score, 10, 24);

AABB — la collision nommée

Cette technique s'appelle AABB (Axis-Aligned Bounding Box) — boîtes alignées sur les axes. C'est la détection de collision la plus simple (et la plus rapide). Deux rectangles A et B se chevauchent si et seulement si les 4 conditions sont vraies simultanément :

ConditionSignification
A.x + A.w >= B.xLe bord droit de A dépasse le bord gauche de B
A.x <= B.x + B.wLe bord gauche de A n'a pas dépassé le bord droit de B
A.y + A.h >= B.yLe bord bas de A dépasse le bord haut de B
A.y <= B.y + B.hLe bord haut de A n'a pas dépassé le bord bas de B

Dans notre jeu on simplifie : le panier ne bouge qu'horizontalement et a une hauteur fixe, donc on teste seulement quand recu.y + recu.taille >= panierY (bord bas du reçu atteint le haut du panier).

Flag B5

Production (le jeu marche !)

Implémente la collision et le score. Rends : ton code complet et une capture avec un score > 0.

0 sur 9 dans cette étape

6. Vies & game over

Un jeu sans enjeu est vite ennuyeux. On ajoute des vies : rater un reçu en coûte une.

let vies = 3;

// dans la boucle, à la place de "if (recu.y > canvas.height) nouveauRecu();"
  if (recu.y > canvas.height) { vies--; nouveauRecu(); }

  if (vies <= 0) {
    ctx.fillStyle = "#b3261e";
    ctx.font = "28px monospace";
    ctx.fillText("Game Over — " + score + " pts", 90, 190);
    return;   // on arrête la boucle
  }

Affiche aussi les vies, à côté du score : ctx.fillText("Vies : " + vies, 380, 24);

Flag B6

Production

Ajoute les 3 vies, l'affichage, et l'écran « Game Over ». Rends : ton code et une capture du Game Over.

0 sur 9 dans cette étape

Le jeu complet — jeu.js en entier

Voici tout le fichier jeu.js assemblé, des chapitres 0 à 6. Compare-le au tien : si quelque chose cloche, l'erreur se voit ici. (Le index.html ne change pas.)

// --- 1. La toile ---
const canvas = document.getElementById("jeu");
const ctx = canvas.getContext("2d");

// --- 2. Le panier ---
let panierX = 200;
const panierY = 320, panierL = 80, panierH = 16;

// --- 3. Le clavier ---
const touches = {};
document.addEventListener("keydown", (e) => {
  touches[e.key] = true;
  if (e.key.startsWith("Arrow")) e.preventDefault();
});
document.addEventListener("keyup", (e) => { touches[e.key] = false; });

// --- 4. Le reçu ---
let recu = { x: 100, y: 0, taille: 20 };
function nouveauRecu() {
  recu.x = Math.random() * (canvas.width - recu.taille);
  recu.y = -recu.taille;
}

// --- 5. Score, vies, collision ---
let score = 0;
let vies = 3;
function attrape() {
  return recu.y + recu.taille >= panierY &&
         recu.x + recu.taille >= panierX &&
         recu.x <= panierX + panierL;
}

// --- 6. La boucle de jeu ---
function boucle() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // déplacer le panier
  if (touches["ArrowLeft"])  panierX -= 6;
  if (touches["ArrowRight"]) panierX += 6;
  panierX = Math.max(0, Math.min(canvas.width - panierL, panierX));
  ctx.fillStyle = "#5b2d8e";
  ctx.fillRect(panierX, panierY, panierL, panierH);

  // faire tomber le reçu
  recu.y += 3;
  if (attrape()) { score++; nouveauRecu(); }
  if (recu.y > canvas.height) { vies--; nouveauRecu(); }
  ctx.fillStyle = "#e8923a";
  ctx.fillRect(recu.x, recu.y, recu.taille, recu.taille);

  // score & vies
  ctx.fillStyle = "#1a1426";
  ctx.font = "16px monospace";
  ctx.fillText("Score : " + score, 10, 24);
  ctx.fillText("Vies : " + vies, 380, 24);

  // game over ?
  if (vies <= 0) {
    ctx.fillStyle = "#b3261e";
    ctx.font = "28px monospace";
    ctx.fillText("Game Over — " + score + " pts", 90, 190);
    return;
  }

  requestAnimationFrame(boucle);
}
boucle();

7. Plusieurs reçus (tableau + forEach)

Un seul reçu, c'est calme. Pour corser le jeu, on en gère plusieurs à la fois : au lieu d'un objet, un tableau d'objets, qu'on parcourt avec forEach.

let recus = [
  { x: 100, y: 0,    taille: 20 },
  { x: 250, y: -150, taille: 20 },
  { x: 380, y: -300, taille: 20 },
];

function reset(r) {
  r.x = Math.random() * (canvas.width - r.taille);
  r.y = -r.taille;
}

function attrape(r) {
  return r.y + r.taille >= panierY &&
         r.x + r.taille >= panierX &&
         r.x <= panierX + panierL;
}

Dans la boucle, on remplace le bloc d'un seul reçu par un forEach :

  recus.forEach((r) => {
    r.y += 3;
    if (attrape(r))      { score++; reset(r); }
    if (r.y > canvas.height) { vies--; reset(r); }
    ctx.fillStyle = "#e8923a";
    ctx.fillRect(r.x, r.y, r.taille, r.taille);
  });

forEach, for, for...of — lequel choisir ?

SyntaxeQuand l'utiliserParticularités
arr.forEach(r => { … })Parcourir et muter chaque objet (changer ses propriétés)Pas de break possible. Ne permet pas de supprimer des éléments en cours de route.
for (let i = 0; i < arr.length; i++)Quand tu as besoin de l'index, ou pour supprimer des éléments (splice)Permet break. Le seul choix sûr si tu modifies la longueur du tableau.
for (const r of arr)Simple et lisible, pas besoin d'indexPermet break. Ne modifie pas la longueur du tableau.
arr.filter(r => …)Créer un nouveau tableau en excluant certains élémentsRetourne un nouveau tableau (l'original est intact). Utile pour supprimer des reçus rattrapés.

Flag B7

Production

Passe le jeu à un tableau de reçus parcouru avec forEach. Rends : ton code et une capture avec plusieurs reçus à l'écran.

0 sur 9 dans cette étape

8. Mini-projet : ton jeu à toi

La mission

Tu as un jeu complet. Maintenant, améliore-le — c'est là que tu deviens créatif (et que tu progresses).

  1. La difficulté monte : la vitesse de chute augmente avec le score.
  2. Des reçus « piège » (couleur différente) qui font perdre des points si attrapés.
  3. Un rejeu : appuyer sur Espace après le Game Over relance une partie.
  4. Ta touche perso : son, image, thème… surprends-nous.

Flag B8

Mini-projet (production)

Ajoute au moins 2 améliorations de la liste (ou les tiennes). Rends : ton code et une courte démo (capture/vidéo) de ce que tu as ajouté.

0 sur 9 dans cette étape

Exemple travaillé — la difficulté qui monte

On remplace la vitesse fixe 3 par une variable calculée selon le score. Déclare en haut du fichier :

let vitesse = 3;

Dans le forEach de la boucle, après score++ :

if (attrape(r)) {
  score++;
  vitesse = 3 + Math.floor(score / 5) * 0.5;  // +0,5 px/image tous les 5 points
  reset(r);
}
// Plus loin, la chute :
r.y += vitesse;   // remplace r.y += 3
Ce que tu dois voir
Score  0  → vitesse 3,0 px/image
Score  5  → vitesse 3,5 px/image
Score 10  → vitesse 4,0 px/image
Score 30  → vitesse 6,0 px/image

Exemple travaillé — rejouer (touche Espace)

Ajoute un booléen enJeu pour savoir si la partie est en cours ou terminée :

let enJeu = true;

Au Game Over, remplace le return seul par :

if (vies <= 0) {
  enJeu = false;
  ctx.fillStyle = "#b3261e";
  ctx.font = "28px monospace";
  ctx.fillText("Game Over — " + score + " pts", 60, 190);
  ctx.fillStyle = "#1a1426";
  ctx.font = "14px monospace";
  ctx.fillText("Appuie sur Espace pour rejouer", 80, 230);
  return;   // ne pas appeler requestAnimationFrame → la boucle s'arrête
}

Dans le gestionnaire keydown, ajoute après la ligne e.preventDefault() :

if (e.key === " " && !enJeu) {
  score = 0; vies = 3; vitesse = 3; enJeu = true;
  recus.forEach(r => reset(r));
  boucle();   // relance le moteur
}

Pour finir

Ce que tu as (re)mobilisé

NotionDans le jeu
Variablesposition, score, vies
Fonctionsboucle, attrape, nouveauRecu
Conditionscollision, bords, game over
Boucle de jeurequestAnimationFrame
Événementsle clavier

Mini-quiz (corrige-toi)

  1. Pourquoi appelle-t-on clearRect au début de chaque tour de boucle ?
  2. Que renvoie canvas.getContext("2d"), et à quoi sert ce qu'il renvoie ?
  3. Dans fillRect(x, y, l, h), dans quel coin de la toile se trouve le point (0,0) et vers où augmente y ?
  4. À quoi sert l'objet touches alimenté par keydown et keyup ?
  5. Que se passe-t-il si tu oublies la ligne boucle(); tout en bas du fichier ?

Réponds de tête, puis vérifie avec le corrigé ci-dessous.

QuestionRéponse attendue
1. Pourquoi clearRect en début de boucle ?Sans effacement, chaque dessin s'accumule : les objets laissent une traînée car les images se superposent. On repart d'une toile vierge pour créer l'illusion du mouvement.
2. Que retourne getContext("2d") ?Un objet CanvasRenderingContext2D (ctx), le « pinceau » : toutes les méthodes de dessin (fillRect, fillText…) s'appellent sur lui.
3. Où est (0,0) et vers où va y ?(0,0) est le coin haut-gauche. x croît vers la droite, y croît vers le bas — inverse du repère mathématique.
4. À quoi sert l'objet touches ?Il mémorise l'état courant du clavier : true si la touche est enfoncée, false sinon. La boucle le consulte à chaque image plutôt que de réagir à des événements discrets → mouvement fluide.
5. Que se passe-t-il si on oublie boucle(); ?La fonction est définie mais jamais appelée : rien ne s'anime. La toile reste vide (ou ne dessine qu'une fois sans se relancer).

Glossaire

MotDéfinition
CanvasZone de dessin programmable dans une page web. Résolution fixée par les attributs width/height.
Contexte 2D (ctx)L'objet retourné par getContext("2d") : le « pinceau ». Toutes les méthodes de dessin lui appartiennent.
Boucle de jeuSéquence effacer → mettre à jour → redessiner, répétée ~60 fois par seconde.
requestAnimationFrame (rAF)Fonction du navigateur qui planifie le prochain appel de la boucle, synchronisé au rafraîchissement de l'écran. Retourne un id annulable.
cancelAnimationFrameAnnule une image planifiée par rAF avant qu'elle s'exécute. Permet de pauser le jeu.
AABBAxis-Aligned Bounding Box — détection de collision par chevauchement de rectangles alignés sur les axes.
ClampForcer une valeur dans un intervalle [min, max] avec Math.max(min, Math.min(max, valeur)).
deltaTimeTemps écoulé entre deux images (ms). Multiplier les déplacements par deltaTime / 16 rend la vitesse identique à 60, 90 ou 144 Hz.
devicePixelRatioRapport entre pixels physiques et pixels CSS (2 sur Retina). Doubler les attributs du canvas et réduire son affichage CSS donne un rendu net sur écrans haute densité.
localStorageStockage clé-valeur du navigateur, persistant entre recharges et sessions. Les valeurs sont des chaînes de caractères.
Référence (objet)En JS, un objet passé à une fonction est passé par référence : modifier une propriété à l'intérieur de la fonction modifie l'objet original.
CollisionTest de chevauchement entre deux objets graphiques (voir AABB).
ÉvénementAction détectée par le navigateur (keydown, touchmove, click…) à laquelle on réagit avec un écouteur (addEventListener).

TP terminé ?

Coche chaque livrable quand tu l'as : c'est la preuve que l'étape est acquise.

livrables
–
de série
–
Voir tous les TP