Maîtriser le Développement de Jeux Web : Créez Vos Propres Expériences Ludiques en HTML5 et JavaScript
Maîtriser le Développement de Jeux Web : Créez Vos Propres Expériences Ludiques en HTML5 et JavaScript

Détection de Collisions et Physique Simple : Rendre Vos Objets Interactifs

Bienvenue à cette leçon cruciale pour tout développeur de jeux ! Nous allons explorer deux piliers fondamentaux de la création d'expériences ludiques interactives : la détection de collisions et la physique simple. Comprendre et implémenter ces concepts est essentiel pour donner vie à vos personnages, rendre vos mondes réactifs et permettre des interactions significatives entre les différents éléments de votre jeu.

Que ce soit pour qu'un personnage ne traverse pas un mur, qu'une balle rebondisse sur une raquette, ou qu'un objet tombe sous l'effet de la gravité, les mécanismes que nous allons aborder sont omniprésents. Préparez-vous à transformer des pixels statiques en objets dynamiques et interactifs !

Pourquoi la Détection de Collisions est-elle Indispensable ?

Dans le monde réel, les objets occupent un espace et ne peuvent pas se traverser mutuellement. Les jeux vidéo doivent simuler cette réalité pour être crédibles et offrir une expérience cohérente. La détection de collisions est le processus par lequel nous déterminons si deux objets ou plus se chevauchent ou se touchent dans l'espace de notre jeu.

Sans détection de collisions, vos jeux seraient :

  • Incohérents : Les personnages traverseraient les murs et les sols.
  • Non interactifs : Il serait impossible de ramasser des objets, de frapper des ennemis ou de franchir des portes.
  • Dépourvus de défi : Pas de dangers, pas d'obstacles à éviter.

C'est la base de toute interaction spatiale dans un jeu.

Principes Fondamentaux de la Détection de Collisions

La détection de collisions est avant tout une question de géométrie et de mathématiques. Pour simplifier les calculs, nous utilisons souvent des formes englobantes (bounding shapes) ou formes de collision (collision shapes) qui sont des représentations simplifiées de nos objets. Plutôt que de vérifier chaque pixel d'un sprite complexe, nous utilisons des formes géométriques simples comme des rectangles ou des cercles qui "englobent" l'objet réel.

Les formes de collision les plus courantes sont :

  • Boîtes englobantes alignées sur les axes (AABB - Axis-Aligned Bounding Box) : Des rectangles dont les côtés sont toujours parallèles aux axes X et Y de votre système de coordonnées. Très simples à gérer.
  • Cercles englobants (Bounding Circles) : Des cercles. Idéaux pour les objets de forme ronde ou lorsque l'orientation n'a pas d'importance.
  • Polygones englobants : Des polygones arbitraires. Plus précis mais plus complexes à calculer.
  • Capsules : Un cylindre avec des hémisphères à chaque extrémité. Utiles pour les personnages debout.

Compromis : Précision vs. Performance

Le choix de la forme de collision dépend d'un compromis entre la précision et la performance.

  • Précision : Une forme qui colle parfaitement aux contours de l'objet réduira les "faux positifs" (une collision est détectée alors qu'il n'y en a pas visuellement).
  • Performance : Des formes simples comme les AABB et les cercles nécessitent moins de calculs, ce qui est crucial pour les jeux avec de nombreux objets.

Pour la plupart des jeux 2D, les AABB et les cercles sont amplement suffisants et offrent un excellent équilibre.

Détection de Collisions par Boîtes Englobantes Alignées sur les Axes (AABB)

L'AABB est la méthode la plus simple et la plus rapide pour la détection de collisions entre deux objets rectangulaires. Elle est particulièrement efficace car elle ne nécessite aucune trigonométrie et est facile à comprendre.

Un rectangle est défini par sa position (x, y), sa largeur (width) et sa hauteur (height). Deux AABB, rectA et rectB, sont en collision si les quatre conditions suivantes sont VRAIES simultanément :

  1. Le côté droit de rectA est à droite du côté gauche de rectB.
  2. Le côté gauche de rectA est à gauche du côté droit de rectB.
  3. Le bas de rectA est en dessous du haut de rectB.
  4. Le haut de rectA est au-dessus du bas de rectB.

Formulé mathématiquement : rectA.x < rectB.x + rectB.width rectA.x + rectA.width > rectB.x rectA.y < rectB.y + rectB.height rectA.y + rectA.height > rectB.y

Si toutes ces conditions sont remplies, alors il y a collision. Notez que nous vérifions si elles se chevauchent, pas seulement si elles se touchent.

Implémentation en JavaScript pour l'AABB

Voici une fonction JavaScript simple pour détecter une collision entre deux objets qui ont les propriétés x, y, width et height.

/**
 * Détecte une collision entre deux boîtes englobantes alignées sur les axes (AABB).
 * @param {object} rect1 - Le premier objet rectangle avec x, y, width, height.
 * @param {object} rect2 - Le second objet rectangle avec x, y, width, height.
 * @returns {boolean} Vrai s'il y a collision, faux sinon.
 */
function checkAABBCollision(rect1, rect2) {
    // Vérifie si les rectangles se chevauchent sur l'axe X
    if (rect1.x < rect2.x + rect2.width &&
        rect1.x + rect1.width > rect2.x &&
        // Vérifie si les rectangles se chevauchent sur l'axe Y
        rect1.y < rect2.y + rect2.height &&
        rect1.y + rect1.height > rect2.y) {
        return true; // Collision détectée
    }
    return false; // Pas de collision
}

// Exemple d'utilisation :
const player = { x: 50, y: 50, width: 30, height: 30 };
const wall = { x: 70, y: 40, width: 20, height: 80 };
const coin = { x: 100, y: 100, width: 10, height: 10 };

console.log("Collision joueur-mur :", checkAABBCollision(player, wall)); // Output: true
console.log("Collision joueur-coin :", checkAABBCollision(player, coin)); // Output: false
  • La fonction checkAABBCollision prend deux objets en paramètres, chacun représentant un rectangle avec ses coordonnées x, y et ses dimensions width, height.
  • Les quatre conditions logiques sont combinées avec l'opérateur && (ET logique). Si toutes sont vraies, cela signifie que les rectangles se chevauchent, et la fonction retourne true.
  • Sinon, si l'une des conditions est fausse, il n'y a pas de chevauchement sur cet axe, et donc pas de collision. La fonction retourne false.

Détection de Collisions Circulaires

Pour les objets de forme circulaire ou pour des collisions moins précises mais plus fluides, la détection de collisions par cercles est une excellente alternative. Elle est également relativement simple et efficace.

Un cercle est défini par son centre (x, y) et son rayon (radius). Deux cercles, circleA et circleB, sont en collision si la distance entre leurs centres est inférieure ou égale à la somme de leurs rayons.

La formule de la distance entre deux points (x1, y1) et (x2, y2) est : distance = sqrt((x2 - x1)^2 + (y2 - y1)^2)

Donc, la condition de collision est : sqrt((circleB.x - circleA.x)^2 + (circleB.y - circleA.y)^2) <= (circleA.radius + circleB.radius)

Pour optimiser les performances, il est courant d'éviter l'opération coûteuse sqrt(). Au lieu de comparer les distances, nous comparons les distances au carré avec les sommes des rayons au carré. Cela donne le même résultat sans le calcul de la racine carrée.

Condition optimisée : (circleB.x - circleA.x)^2 + (circleB.y - circleA.y)^2 <= (circleA.radius + circleB.radius)^2

Implémentation en JavaScript pour les Collisions Circulaires

/**
 * Détecte une collision entre deux cercles.
 * @param {object} circle1 - Le premier objet cercle avec x, y (centre) et radius.
 * @param {object} circle2 - Le second objet cercle avec x, y (centre) et radius.
 * @returns {boolean} Vrai s'il y a collision, faux sinon.
 */
function checkCircleCollision(circle1, circle2) {
    // Calcul de la distance entre les centres des cercles
    const dx = circle2.x - circle1.x;
    const dy = circle2.y - circle1.y;
    const distanceSquared = (dx * dx) + (dy * dy);

    // Calcul de la somme des rayons au carré
    const radiusSum = circle1.radius + circle2.radius;
    const radiusSumSquared = radiusSum * radiusSum;

    // Comparaison de la distance au carré avec la somme des rayons au carré
    if (distanceSquared <= radiusSumSquared) {
        return true; // Collision détectée
    }
    return false; // Pas de collision
}

// Exemple d'utilisation :
const ball1 = { x: 100, y: 100, radius: 20 };
const ball2 = { x: 110, y: 110, radius: 15 };
const ball3 = { x: 200, y: 200, radius: 10 };

console.log("Collision ball1-ball2 :", checkCircleCollision(ball1, ball2)); // Output: true
console.log("Collision ball1-ball3 :", checkCircleCollision(ball1, ball3)); // Output: false
  • La fonction checkCircleCollision calcule la différence sur l'axe X (dx) et l'axe Y (dy) entre les centres des deux cercles.
  • Elle calcule ensuite la distance au carré entre les centres (distanceSquared).
  • La somme des rayons au carré (radiusSumSquared) est également calculée.
  • Si distanceSquared est inférieure ou égale à radiusSumSquared, les cercles se chevauchent ou se touchent, et il y a collision.

Détection de Collisions Plus Avancées (Vue d'Ensemble)

Bien que les AABB et les cercles couvrent la majorité des besoins, des scénarios plus complexes peuvent nécessiter des techniques plus sophistiquées :

  • SAT (Separating Axis Theorem) : Pour la détection de collisions entre polygones convexes arbitraires. C'est plus complexe mais très précis.
  • Pixel-Perfect Collision : Compare les pixels non transparents de deux images. Très coûteux en performance, généralement utilisé pour des cas spécifiques ou entre un petit nombre d'objets critiques.
  • Partitionnement Spatial : Lorsque vous avez un grand nombre d'objets, vérifier chaque objet avec chaque autre objet (O(n²)) devient inefficace. Des techniques comme les Quadtrees, les Grilles uniformes ou les BSP Trees permettent de limiter le nombre de paires à tester en divisant l'espace de jeu.

Introduction à la Physique Simple

La détection de collisions vous dit qu'une interaction s'est produite. La physique définit comment les objets réagissent à cette interaction, ou même comment ils se comportent sans interaction directe (gravité, mouvement).

La "physique simple" dans un jeu 2D fait généralement référence à :

  • Mouvement et Vélocité : Comment les objets se déplacent.
  • Gravité : L'attraction vers le bas.
  • Friction / Résistance de l'air : Ralentissement du mouvement.
  • Restitution / Rebond : Comment les objets rebondissent après une collision.
  • Résolution des chevauchements : Empêcher les objets de rester imbriqués après une collision.

Gestion des Mouvements de Base

La base du mouvement est la mise à jour de la position d'un objet en fonction de sa vélocité (vitesse et direction).

nouvelle_position = ancienne_position + vélocité * temps_écoulé

En JavaScript, dans votre boucle de jeu, vous feriez :

// Définition de l'objet
const object = { x: 0, y: 0, vx: 5, vy: 0 }; // vx, vy sont les composantes de la vélocité

// Dans la boucle de jeu, à chaque frame
function update(deltaTime) { // deltaTime est le temps écoulé depuis la dernière frame
    object.x += object.vx * deltaTime;
    object.y += object.vy * deltaTime;
}
  • vx et vy représentent la vélocité sur les axes X et Y respectivement.
  • deltaTime est crucial pour rendre les mouvements fluides et indépendants de la fréquence d'images (frame rate). Si deltaTime n'est pas utilisé, un jeu tournant à 60 FPS déplacerait les objets deux fois plus vite qu'un jeu à 30 FPS.

Gravité

La gravité est une force constante qui tire les objets vers le bas. C'est une accélération, ce qui signifie qu'elle modifie la vélocité, pas directement la position.

nouvelle_vélocité_Y = ancienne_vélocité_Y + gravité * temps_écoulé

const object = { x: 0, y: 0, vx: 0, vy: 0, mass: 1 };
const gravity = 9.8; // Une valeur arbitraire pour la gravité (peut être ajustée)

function applyGravity(object, deltaTime) {
    object.vy += gravity * deltaTime; // La vélocité verticale augmente avec le temps
}

function updateObject(object, deltaTime) {
    applyGravity(object, deltaTime);
    object.x += object.vx * deltaTime;
    object.y += object.vy * deltaTime;
}
  • La variable gravity représente la force de gravité par unité de temps.
  • applyGravity incrémente la vélocité verticale (vy) de l'objet, simulant l'accélération.

Réponse aux Collisions (Post-collision)

Une fois qu'une collision est détectée, que se passe-t-il ? C'est là que la physique de réponse entre en jeu. Les réponses de base incluent :

  • Arrêt : L'objet arrête simplement son mouvement dans la direction de la collision (ex: un joueur contre un mur).
  • Rebond (Restitution) : L'objet change de direction et peut perdre une partie de son énergie (ex: une balle rebondissant sur le sol).
    • nouvelle_vélocité = -ancienne_vélocité * coefficient_de_restitution
    • Le coefficient_de_restitution est une valeur entre 0 (pas de rebond, absorption totale) et 1 (rebond parfait, pas de perte d'énergie).
  • Résolution du chevauchement : Si les objets se chevauchent après une collision, il faut les déplacer pour les "séparer" afin qu'ils ne soient plus imbriqués.

Mise en Pratique : Un Monde Simple avec Collisions et Gravité

Combinons ces concepts dans un petit exemple où une balle tombe sous l'effet de la gravité et rebondit sur le sol (représenté par un rectangle).

<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Collision et Physique Simple</title>
    <style>
        body { margin: 0; overflow: hidden; background-color: #333; }
        canvas { display: block; background-color: #eee; border: 1px solid #ccc; }
    </style>
</head>
<body>
    <canvas id="gameCanvas"></canvas>

    <script>
        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');

        canvas.width = window.innerWidth;
        canvas.height = window.innerHeight;

        // --- Paramètres du jeu ---
        const GRAVITY = 9.8; // Accélération de la gravité
        const RESTITUTION = 0.7; // Coefficient de restitution (0=pas de rebond, 1=rebond parfait)
        let lastTime = 0;
        let deltaTime = 0;

        // --- Objet Balle ---
        const ball = {
            x: canvas.width / 2,
            y: 50,
            radius: 20,
            color: 'red',
            vx: 50, // vélocité horizontale (pixels/seconde)
            vy: 0,  // vélocité verticale (pixels/seconde)
            mass: 1 // pour des calculs futurs si besoin
        };

        // --- Objet Sol (AABB) ---
        const ground = {
            x: 0,
            y: canvas.height - 50,
            width: canvas.width,
            height: 50,
            color: 'green'
        };

        // --- Fonctions de collision ---

        /**
         * Détecte une collision entre deux AABB.
         * (Réutilisation de la fonction précédente)
         */
        function checkAABBCollision(rect1, rect2) {
            return rect1.x < rect2.x + rect2.width &&
                   rect1.x + rect1.width > rect2.x &&
                   rect1.y < rect2.y + rect2.height &&
                   rect1.y + rect1.height > rect2.y;
        }

        // --- Fonctions de dessin ---

        function drawBall() {
            ctx.beginPath();
            ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
            ctx.fillStyle = ball.color;
            ctx.fill();
            ctx.closePath();
        }

        function drawGround() {
            ctx.fillStyle = ground.color;
            ctx.fillRect(ground.x, ground.y, ground.width, ground.height);
        }

        // --- Boucle de jeu principale ---

        function gameLoop(currentTime) {
            deltaTime = (currentTime - lastTime) / 1000; // Convertir en secondes
            lastTime = currentTime;

            // 1. Mise à jour de la physique de la balle
            updateBallPhysics(deltaTime);

            // 2. Détection et réponse aux collisions
            handleCollisions();

            // 3. Dessin des éléments
            ctx.clearRect(0, 0, canvas.width, canvas.height); // Effacer le canvas
            drawBall();
            drawGround();

            requestAnimationFrame(gameLoop); // Appel de la prochaine frame
        }

        function updateBallPhysics(dt) {
            // Appliquer la gravité (affecte la vélocité verticale)
            ball.vy += GRAVITY * dt * 100; // Multiplié par 100 pour une gravité plus visible

            // Mettre à jour la position de la balle en fonction de sa vélocité
            ball.x += ball.vx * dt;
            ball.y += ball.vy * dt;

            // Gérer les bords du canvas pour la balle (pour le x)
            if (ball.x + ball.radius > canvas.width) {
                ball.x = canvas.width - ball.radius;
                ball.vx *= -1; // Inverser la direction horizontale
            } else if (ball.x - ball.radius < 0) {
                ball.x = ball.radius;
                ball.vx *= -1;
            }
        }

        function handleCollisions() {
            // Collision balle-sol (la balle est un cercle, le sol est une AABB)
            // Pour simplifier, nous allons traiter la balle comme un AABB pour la collision avec le sol
            // ou considérer le point le plus bas de la balle pour la collision avec le sol.
            // Optons pour la collision avec le point le plus bas de la balle sur le Y du sol.
            
            // Si le bas de la balle touche ou dépasse le haut du sol
            if (ball.y + ball.radius > ground.y) {
                // Résolution du chevauchement : repositionner la balle juste au-dessus du sol
                ball.y = ground.y - ball.radius;
                
                // Réponse au rebond : inverser la vélocité verticale et appliquer la restitution
                ball.vy *= -RESTITUTION;
                
                // Si la vélocité est très faible, l'arrêter pour éviter des micro-rebonds infinis
                if (Math.abs(ball.vy) < 50) { // Seuil arbitraire, ajustez si besoin
                    ball.vy = 0;
                }
            }
        }

        // --- Démarrer la boucle de jeu ---
        requestAnimationFrame(gameLoop);
    </script>
</body>
</html>

Explication du Code Intégré

  1. Structure HTML et Canvas : Un simple fichier HTML avec un élément <canvas> où le jeu sera dessiné.
  2. Variables Globales :
    • canvas et ctx : Références au canvas et à son contexte de dessin 2D.
    • GRAVITY et RESTITUTION : Constantes pour la simulation physique.
    • lastTime et deltaTime : Variables essentielles pour un jeu frame-rate indépendant. deltaTime est le temps écoulé entre deux frames, permettant de scaler les mouvements.
  3. Objets ball et ground :
    • ball : Un objet avec des propriétés de position (x, y), rayon (radius), couleur, et surtout de vélocité (vx, vy).
    • ground : Un objet AABB simple représentant le sol avec position, dimensions et couleur.
  4. Fonctions de Dessin (drawBall, drawGround) : Ces fonctions utilisent l'API Canvas 2D pour dessiner les objets à leur position actuelle.
  5. gameLoop(currentTime) : C'est le cœur du jeu, appelé à chaque frame par requestAnimationFrame.
    • Il calcule deltaTime.
    • Il appelle updateBallPhysics pour mettre à jour la position et la vélocité de la balle.
    • Il appelle handleCollisions pour détecter et gérer les interactions.
    • Il efface le canvas (ctx.clearRect) et redessine tous les objets.
  6. updateBallPhysics(dt) :
    • Gravité : ball.vy += GRAVITY * dt * 100; augmente la vélocité verticale de la balle. Le * 100 est un facteur d'échelle pour que la gravité soit plus visible à l'écran, car les pixels ne sont pas des mètres.
    • Mouvement : ball.x += ball.vx * dt; et ball.y += ball.vy * dt; mettent à jour la position en fonction de la vélocité et du temps écoulé.
    • Bords latéraux : Simple gestion des bords gauche et droit du canvas pour que la balle rebondisse horizontalement.
  7. handleCollisions() :
    • Collision balle-sol : Nous vérifions si le bas de la balle (ball.y + ball.radius) dépasse le haut du sol (ground.y).
    • Résolution du chevauchement : Si une collision est détectée, ball.y = ground.y - ball.radius; repositionne la balle juste au-dessus du sol pour qu'elle ne s'enfonce pas.
    • Réponse au rebond : ball.vy *= -RESTITUTION; inverse la vélocité verticale et réduit son intensité en fonction du RESTITUTION. Une petite vérification (Math.abs(ball.vy) < 50) est ajoutée pour arrêter la balle si elle n'a presque plus d'énergie, évitant un rebond infini minuscule.

Cet exemple démontre comment lier la détection de collision avec une réponse physique simple pour créer un comportement réaliste.

Optimisation et Considérations Avancées

Pour des jeux plus complexes avec de nombreux objets, l'efficacité de la détection de collisions devient primordiale :

  • Collision Filtering/Layers : Ne pas vérifier les collisions entre des objets qui n'ont aucune raison de jamais interagir (ex: des balles de joueur ne se touchent pas entre elles).
  • Phase de Broad-Phase / Narrow-Phase :
    • Broad-Phase : Utilise des techniques de partitionnement spatial (comme les quadtrees mentionnés précédemment) pour rapidement identifier un sous-ensemble de paires d'objets susceptibles d'entrer en collision.
    • Narrow-Phase : Applique ensuite des tests de collision précis (AABB, Cercles, SAT, etc.) uniquement sur ces paires réduites.
  • Moteurs Physiques : Pour des simulations plus réalistes et complexes (corps rigides, articulations, contraintes, empilements), l'utilisation d'une bibliothèque ou d'un moteur physique dédié est recommandée. Des exemples populaires en JavaScript incluent :
    • Matter.js : Un moteur physique 2D populaire.
    • P2.js : Un autre moteur 2D robuste, successeur de Box2D.
    • Box2D.js : Port JavaScript du célèbre moteur physique Box2D.

Ces moteurs s'occupent de la détection de collisions, de la résolution des chevauchements, des réponses physiques, de la gravité, et bien plus encore, vous permettant de vous concentrer sur la logique de jeu.

Conclusion et Résumé

Félicitations ! Vous avez maintenant une solide compréhension des fondamentaux de la détection de collisions et de la physique simple en développement de jeux web.

Voici les points clés à retenir :

  • La détection de collisions est essentielle pour rendre vos objets interactifs et votre monde crédible.
  • Les formes englobantes (AABB, Cercles) sont des simplifications géométriques qui optimisent les calculs de collision.
  • Les AABB sont parfaites pour les rectangles alignés et sont très rapides à tester.
  • Les collisions circulaires sont idéales pour les objets ronds et sont également efficaces en évitant la racine carrée.
  • La physique simple gère la manière dont les objets se déplacent et réagissent aux forces (comme la gravité) et aux collisions (rebond, arrêt).
  • L'utilisation de deltaTime est cruciale pour une simulation physique cohérente et indépendante du framerate.
  • Pour des jeux complexes, considérez l'optimisation (partitionnement spatial) et l'utilisation de moteurs physiques dédiés.

La maîtrise de ces concepts vous ouvre les portes vers la création d'expériences de jeu beaucoup plus riches et dynamiques. N'hésitez pas à expérimenter avec les valeurs de gravité et de restitution dans l'exemple de code pour voir comment elles affectent le comportement de la balle !