Créer un système de physique avec SDL
Cet article s’attarde sur la création de comportements liés à la physique (gravité, collisions, rebonds) dans le contexte du développement de DuckDuckGame. Elle ne couvre pas la représentation des objets dans l’espace, mais uniquement la physique appliquée à ces objets.
La gravité
Section titled “La gravité”Pour créer de la gravité, c’est très simple, il suffit qu’à un intervalle régulier de temps (chaque image), la vitesse verticale d’un objet augmente. Pour cela il suffit d’avoir une variable représentant cette vitesse, puis d’y ajouter une valeur définie tel que dans le code suivant :
speed += 0.2;personnage->rect->y += speed;Et voila ! Notre personnage tombe.
Il faut noter que cette approche dépend du nombre de fois que cette fonction est exécutée par seconde. Le plus, le plus vite sera la chute. Pour palier à cela il nous faudrait une variable contenant le temps depuis la dernière image (connu sous le nom de DeltaTime), et multiplier notre augmentation de vitesse par celle-ci (afin que si le nombre d’image par seconde est élevé, la distance parcouru soit petite, et inversement).
Cependant obtenir un tel nombre a ses propres défis techniques, et c’est pour cela que dans la suite de ce document nous assumerons que le nombre d’image par seconde est fixe.
Les collisions
Section titled “Les collisions”Le premier code permettant de gérer des collisions était celui-ci :
speed += 0.2;if (personnage->rect->y >= 500) { personnage->rect->y = 500; speed = 0;}personnage->rect->y += speed;render(personnage);Il se basait sur une variable speed (qui représentait la vitesse verticale) qui était ajoutée au personnage à chaque frame, le faisant ainsi tomber en accélérant.
En guise de sol, nous avions les coordonnées Y=500 auquel le personnage était ramené s’il les dépassait.
À noter que rect->y représente le bord supérieur du personnage : à y=500 son sommet est à 500, son bas à 500+h, donc légèrement dans le sol. La position correcte serait personnage->rect->y = 500 - personnage->rect->h, mais cela ne change pas la démonstration du principe.
Cependant cette méthode amenait un léger problème :
Quand le personnage tombe de haut, sa vitesse faisait qu’il dépassait visiblement la barrière des 500, et était ramené à l’image d’après, donnant un effet de rollback.
Anticipation de la prochaine position
Section titled “Anticipation de la prochaine position”La correction à ceci a été d’anticiper la prochaine position du personnage à l’image d’après pour le placer directement à la bonne position (nous aurions pu vérifier de nouveau la position Y du personnage avant de le render, mais cela revient un peu au même). Ce concept d’anticipation de la prochaine position sera un fondement dans la suite du développement de ce système de collision.
Voici le code implémentant cette idée :
speed += 0.2;if (personnage->rect->y + speed >= 500) { personnage->rect->y = 500; speed = 0;}personnage->rect->y += speed;render(personnage);(Le ”+ speed” après la position Y du perso, dans le if, modification discrète mais très efficace)
A partir de là, nous pouvons améliorer la structure d’un objet de manière à ce qu’elle puisse stocker les valeurs de vitesse X et Y de l’objet.
Et nous créerons une fonction (GetNextPosition) utilisant cela afin de retourner le Rect de la prochaine position de notre objet.
Collisions objet à objet
Section titled “Collisions objet à objet”Maintenant, la prochaine étape sera d’améliorer ce avec quoi notre joueur a une collision. Pour l’instant nous utilisons simplement une hauteur prédéfinie dans le code, alors essayons d’utiliser un autre objet du jeu !
Nous attaquons donc les collisions “Objet - Objet” :
Pour savoir si un objet est en collision avec un autre, nous devons savoir s’ils se chevauchent, en d’autres termes, s’il y a une intersection entre eux.
Comme nous n’utilisons uniquement des rectangles pour l’instant, nous pouvons utiliser de manière très pratique la fonction SDL nommée “SDL_IntersectRect”, qui permet de savoir s’il y a une intersection entre deux Rect, et si oui d’avoir le rectangle représentant cette intersection comme montré dans le schéma suivant :

Dans notre cas, nous aurons un personnage, qui intersectionne avec un rectangle qui représente le sol, comme suit :

Nous constatons donc la collision entre ces deux objets, et la fonction SDL_IntersectRect nous retournerait bien TRUE, de plus nous récupèrerions aussi l’équivalent du rectangle bleu ici, qui représente l’intersection de ces deux Rect.
Additionnellement, ce schéma ne représente pas l’utilisation de la simulation de prochaine position que nous avons créée plus tôt. Dans les faits, à un état de repos le personnage serait situé sur le sol, et gagnerait à chaque frame de la vélocité verticale (due à la gravité). Cela déplacerait sa boite de prochaine position dans le sol, permettant ainsi de détecter la collision, et annulerait la vitesse verticale gagnée -> faisant donc effectivement rester le personnage immobile sur le sol, l’empêchant de le traverser.
Dans notre code, tout ce que nous aurons à faire c’est détecter avec la fonction SDL_IntersectRect s’il y a une collision entre la future position de notre personnage, et l’objet collisioné. Et si c’est le cas, déplacer le joueur au dessus de celui-ci :
personnage->y = obstacle->y - personnage->height
OU
personnage->y = personnageNextPos->y - IntersectRect->height
SDL_Rect* intersect = (SDL_Rect*) malloc(sizeof(SDL_Rect));SDL_Rect* colliderNextPos = GetNextPos(collider); // GhostBox : projection de la position future du joueur//N'oublions pas l'astuce d'utiliser la prochaine position de l'objet.
SDL_bool hasCollided = SDL_IntersectRect(colliderNextPos, collideePos, intersect);
if (hasCollided == SDL_TRUE) { collider->rect->y = collidee->rect->y - collider->rect->h;}
free(intersect);Maintenant, nous avons enfin un système permettant au personnage de tomber sur un objet, et d’y rester sans bouger !
Collisions multi-directionnelles
Section titled “Collisions multi-directionnelles”Cependant, nouveau problème :)
Le code présenté ci-dessus engendrerait des situations comme celle-ci :

Si notre joueur entre en collision depuis le côté avec ce qui représente le sol, il se fait téléporter au dessus de celui-ci.
Ce n’est de toute évidence absolument pas le comportement que nous désirons, nous devons donc adapter notre code afin qu’il puisse être plus généraliste.
L’objectif est de permettre de gérer proprement les collisions, qu’elles viennent aussi bien du côté, dessus ou dessous !
Mais pour commencer cantonnons nous à faire fonctionner les collisions pour 1 axe.
Notre code actuel fonctionne pour une collision sur 1 seul axe, ET en venant d’un seul côté de cet axe. En effet, si nous entrons en collision avec le sol que nous avons codé juste au dessus, nous nous faisons téléporter dessus ce dernier.
Pour commencer la résolution de ce problème, changeons d’abord d’axe. L’axe Y que nous utilisions jusqu’à présent était intuitif du point de vue de la gravité, cependant le fait que sa valeur augmente en descendant, ne l’est pas.
Prenons donc l’exemple d’un mur, et définissons les termes de l’explication :
Dans les exemples qui suivent, nous ferons référence à la prochaine position du joueur en tant que GhostBox, celle-ci est simplement une projection du joueur à partir de sa vélocité actuelle.


Très bien, grâce à ce que nous avons défini précédemment, cette collision serait résolue en plaçant le player juste collé au mur.
Voici le code complet implémentant les collisions de tous côtés, avec plusieurs objets simultanément :
WindowElement* collider = personnage;SDL_Rect* colliderNextPos = GetNextPosition(collider); // GhostBox : projection de la position future du joueur
// Sachant que obs représente les objets du mondefor (unsigned i = 0; i < obs->lenght; ++i) { WindowElement* collidee = obs->objects + i; //mieux que &obs->objects[i] SDL_Rect* collideeNextPos = GetNextPosition(collidee);
SDL_Rect* intersect = (SDL_Rect*) malloc(sizeof(SDL_Rect)); SDL_bool hasCollided = SDL_IntersectRect(colliderNextPos, collideeNextPos, intersect);
if (hasCollided == SDL_TRUE) {
// À chacun de ces `if`, on vérifie la position du collider par rapport au collidee en utilisant leur position actuelle, // donc PAS leur future position. Mais tout en sachant que leur prochaine position est bien entrée en collision // Ici par exemple on sait qu'il VA y avoir collision, et on regarde si la collision vient du haut if (collider->rect->y + collider->rect->h <= collidee->rect->y) { collider->vY = 0; colliderNextPos->y -= intersect->h; } else if (collider->rect->y >= collidee->rect->y + collidee->rect->h) // collision vient du bas { collider->vY = 0; colliderNextPos->y += intersect->h; }
if (collider->rect->x + collider->rect->w <= collidee->rect->x) // collision vient de gauche { collider->vX = 0; colliderNextPos->x -= intersect->w; } else if (collider->rect->x >= collidee->rect->x + collidee->rect->w) // collision vient de droite { collider->vX = 0; colliderNextPos->x += intersect->w; } }
free(intersect);
collidee->rect->x = collideeNextPos->x; collidee->rect->y = collideeNextPos->y;}// Maintenant que nous avons crafté une prochaine position cohérente, nous l'appliquonscollider->rect->x = colliderNextPos->x;collider->rect->y = colliderNextPos->y;Ordre de résolution des collisions
Section titled “Ordre de résolution des collisions”Cette implémentation a une subtilité : les collisions sont résolues dans l’ordre du tableau obs->objects. Dans DuckDuckGame, deux sols se téléportent en boucle pour simuler un défilement infini, et le joueur peut se retrouver simultanément en collision avec les deux à leur jonction.

Si A est résolu en premier, seule une collision verticale est détectée, elle est résolue correctement et B ne pose plus problème. En revanche si B est résolu en premier, la GhostBox déborde à la fois sur Y et sur X : les deux résolutions sont mutuellement exclusives. Notre code vérifiant Y avant X, le comportement dépend de laquelle des deux conditions est satisfaite, et au niveau d’une jonction, le joueur glissant horizontalement peut déclencher la collision X en premier, le faisant bloquer comme contre un mur invisible.
À noter que résoudre X avant Y ne réglerait pas le problème au niveau moteur : cela inverserait simplement le cas problématique. Deux murs superposés verticalement formeraient alors une plateforme invisible.
Plusieurs approches ont été envisagées pour déterminer l’ordre de résolution :
- Distance naïve (centre à centre) : trier par distance entre le centre du joueur et le centre de chaque objet. Rejeté : si A et B ont des tailles radicalement différentes, la distance est biaisée par la taille et non par la proximité réelle.
- Surface d’intersection : résoudre en priorité l’objet avec la plus grande surface de collision. Rend le problème moins probable mais ne le supprime pas.
- Double calcul : calculer les deux résolutions possibles et retenir la plus éloignée. Coûteux et probablement soumis à des edge cases.
- Projeté orthogonal : pour chaque objet, projeter orthogonalement le centre du joueur sur l’objet pour obtenir le point le plus proche lui appartenant, puis trier par cette distance. Règle le biais de taille tout en restant simple.

Dans la pratique, le problème était imperceptible dans le jeu final, donc cette approche est restée au stade de la réflexion.
Tentative : collisions continues
Section titled “Tentative : collisions continues”Cette approche est parfaitement fonctionnelle pour une utilisation classique, cependant elle a certaines limites dans des cas extrêmes. Imaginons que le Player aille à une vitesse très élevée, il serait possible que sa “GhostBox” passe directement derrière l’obstacle et par conséquent que le joueur le traverse. Voici un schéma illustrant ceci :

Nous pouvons essayer d’implémenter une solution avec SDL_UnionRect permettant de récupérer le rectangle contenant le joueur jusqu’à sa GhostBox (position future).
La seule différence par rapport au code précédent est la détection : au lieu d’utiliser directement la GhostBox, on construit une deltaBox couvrant le joueur de sa position actuelle jusqu’à sa GhostBox, puis on vérifie l’intersection avec celle-ci. La résolution reste identique.
SDL_FRect* deltaBox = (SDL_FRect*) malloc(sizeof(SDL_FRect));SDL_UnionFRect(collider->rect, colliderNextPos, deltaBox);SDL_bool hasCollided = SDL_HasIntersectionF(deltaBox, collideeNextPos);free(deltaBox);Après avoir testé cette méthode je vois un problème et en théorise un autre.
Le problème que je constate est que si le personnage est sur une plateforme qui monte, il ne peut plus sauter. Après investigation cela est dû au fait que dans ce code la boite d’union comprend la position actuelle du personnage et n’est pas basé uniquement sur ses positions futures. Cela a pour effet de créer une collision alors que dans le futur il n’y en aurait pas eu.
Ce problème peut être mitigé en enlevant la largeur et hauteur du Rect du perso à cette box et en la décalant dans la bonne direction.
Cependant ce problème m’a fait penser à un autre, ce système de boite d’union est présent pour avoir des collisions continues même si l’objet va à une haute vitesse. Cependant si l’objet va en diagonale la boite va s’étendre dans les deux directions diagonales tangentes et potentiellement entrer en collision avec un mur alors que l’objet serait simplement passé à côté avec un calcul normal. Pour régler cela il faudrait utiliser des raycasts afin de vérifier si une intersection LINEAIRE existe. Voir le schéma suivant :

Le système initial étant déjà suffisant pour ce que nous faisons, et n’ayant pas un temps illimité pour expérimenter, nous allons revenir à la version précédente du système de collision qui fait déjà suffisamment l’affaire. Cependant nous savons que si nous nécessitons éventuellement d’une version plus robuste, nous avons le modèle ici.
Le rebond
Section titled “Le rebond”Implémenter un système de rebond une fois les bases physiques posées est plutôt simple.
Il nous suffit de définir un coefficient de rebond pour les objets du jeu, et de multiplier la vitesse du joueur sur l’axe de la collision par le négatif de ce coefficient ; inversant ainsi sa direction d’un facteur défini.
Exemple :
Le joueur avance de 5 vers la droite (mouvement sur X), il rencontre un obstacle avec un coefficient de rebond de 1.
Nous avons donc vitesse_joueur * -(coeff_rebond) = new_vitesse_joueur,
donc ici 5 * -1 = -5, notre joueur ira donc dans l’autre sens sans perte de vitesse, soit le comportement attendu.
Voici ce que cela donne en code :
// ...// Si collision :
// Coefficient de rebond :// 0 -> Aucun rebond// 1 -> Rebond total, aucune perte de momentumfloat bounciness = 1;
// Si collision sur axe Y, venant du dessusif (collider->rect->y + collider->rect->h <= collidee->rect->y){ // Négation de la vitesse Y et multiplication par le coefficient de rebond collider->vY *= -bounciness;
// Gestion de la collision}// Gestion de l'axe Y venant du dessous et axe X similaire// ...À noter que cette formule suppose que le collidee est statique, ou du moins qu’il ne réagit pas à la collision (il pourrait par exemple être en mouvement linéaire, comme une plateforme qui se déplace). Dans le cas où les deux objets sont en mouvement et réagissent l’un à l’autre, il faudrait introduire la notion de masse et appliquer la conservation de la quantité de mouvement pour calculer les nouvelles vélocités des deux objets.