WebGL pas à pas

Séance 3 · Travaux pratiques

TP 3 · Esquive

Un petit jeu complet : vous dirigez un carré bleu au clavier et devez éviter des balles orange qui rebondissent et se multiplient. Le but n'est pas le jeu lui-même mais sa structure : état, mise à jour, rendu, boucle.

Durée 1 h 30Dossier tp3/depart/Cours Animation et boucle de jeu

Télécharger le code de départtp3-depart.zip · 5 Ko · à décompresser, puis ouvrir le dossier tp3/depart/ dans VS Code

Objectifs

  • Dessiner plusieurs objets avec une seule géométrie et des uniforms.
  • Écrire une boucle requestAnimationFrame avec delta time.
  • Séparer update et render, gérer le clavier.
  • Passer à un pas de temps fixe et mesurer FPS et UPS.

Lancez la page : un grand carré bleu recouvre tout. C'est le joueur, qui n'est ni réduit ni placé. On commence par là. Lisez d'abord tout main.js : l'objet etat contient tout ce qui décrit le jeu à un instant donné.

Placer et dimensionner avec des uniforms

Le buffer contient un seul carré, de −1 à 1. Pour dessiner le joueur et les balles, on réutilise ce carré en le réduisant et en le déplaçant dans le vertex shader.

  • 1.1 Ajoutez les uniforms u_deplacement (vec2) et u_taille (float), et calculez a_position * u_taille + u_deplacement.
  • 1.2 Récupérez leurs emplacements.
  • 1.3 Complétez dessinerCarre avec gl.uniform2f et gl.uniform1f.
Vérification

Un petit carré bleu en bas et un petit carré orange en haut, immobiles.

Question 1

Pourquoi ne pas modifier les sommets dans le buffer pour déplacer un objet ? Dans quel ordre sont appliquées l'échelle et la translation ? Que se passerait-il en écrivant (a_position + u_deplacement) * u_taille ?

La boucle et le mouvement

  • 2.1 Écrivez la fonction image(t) : calculez dt en secondes à partir de t et du temps précédent, appelez update(dt) puis render(), et redemandez l'image suivante. Lancez la boucle à la fin du fichier avec requestAnimationFrame(image).
  • Comptez les images et affichez une fois par seconde le nombre de FPS dans hud.textContent.
  • 2.2 Dans update, déplacez chaque balle : b.x += b.vx * dt, idem en y. Si la balle dépasse un bord (en tenant compte de sa taille), replacez-la contre le bord et inversez la composante de vitesse correspondante.
Vérification

La balle rebondit sur les quatre bords sans jamais en sortir ni s'y « coller ». Le HUD affiche environ 60 FPS (ou la fréquence de votre écran).

Question 2

Pourquoi faut-il replacer la balle contre le bord avant d'inverser sa vitesse ? (Indice : imaginez un dt très grand.)

Je suis bloqué sur dt
let tPrecedent = performance.now();
function image(t) {
  const dt = (t - tPrecedent) / 1000;
  tPrecedent = t;
  // ...
  requestAnimationFrame(image);
}

Le clavier

  • 3.1 Créez const touches = new Set();. Sur keydown, ajoutez e.code ; sur keyup, retirez-le. Appelez e.preventDefault() pour les flèches et l'espace, sinon la page défile.
  • 3.2 Dans update, calculez une direction dx, dy à partir des touches ("ArrowLeft", "ArrowRight"…, et "KeyA", "KeyD", "KeyW", "KeyS" qui correspondent à Q, D, Z, S sur un clavier AZERTY). Déplacez le joueur de dx * vitesse * dt et empêchez-le de sortir du cadre avec Math.max / Math.min.
Vérification

Le joueur se déplace en douceur, y compris en diagonale avec deux touches à la fois, et reste dans le cadre.

Question 3

En diagonale, le joueur va-t-il plus vite qu'en ligne droite ? De combien ? Comment corriger ?

Collisions et fin de partie

  • 4.1 Écrivez collision(a, b) (voir le cours, section 6).
  • 4.2 Si le joueur touche une balle, etat.enJeu = false. Quand la partie est finie, update ne fait plus bouger le jeu, sauf si la touche Espace est enfoncée : elle appelle reinitialiser(). Pendant le jeu, faites avancer etat.temps.
  • 4.3 Ajoutez une nouvelle balle toutes les 5 secondes (un compte à rebours dans etat).
  • Dans render, changez la couleur de fond quand la partie est perdue, et affichez le temps de survie dans le HUD.
Vérification

Le jeu est jouable : les balles s'accumulent, un contact arrête tout, Espace relance une partie.

Pas de temps fixe

Cochez « Simuler un ordinateur lent » : chaque rendu prend alors 40 ms de plus, et le jeu ne tourne plus qu'à environ 20 FPS.

Question 4

Avec votre boucle actuelle, la vitesse des balles change-t-elle quand on coche la case ? Le jeu est-il pour autant identique ? Pensez aux collisions avec une balle très rapide.

  • Transformez image(t) en boucle à pas de temps fixe, avec const DT = 1 / 60 et une réserve de temps (voir le cours, section 3). N'oubliez pas de plafonner le temps ajouté à la réserve.
  • Comptez aussi les mises à jour et affichez les UPS à côté des FPS.
Vérification

Sans la case : environ 60 FPS et 60 UPS. Avec la case : environ 20 FPS mais toujours 60 UPS. Le jeu va à la même vitesse dans les deux cas, plus saccadé dans le second.

Bonus

  • A. Pause avec la touche P : update n'est plus appelée, mais le rendu continue.
  • B. Clignotement : ajoutez un uniform u_temps au fragment shader et faites pulser la couleur des balles avec sin(u_temps * 6.0).
  • C. Meilleur score conservé dans localStorage.
  • D. Écran non carré : passez le canvas en 900 × 600. Le joueur devient un rectangle. Corrigez-le avec un uniform u_aspect (largeur / hauteur) qui divise x dans le vertex shader. Pensez aux bords et aux collisions. La séance suivante règlera ce problème de façon générale.