Jeux Libres
       
           

» Les Tutoriels » Création d'un jeu vidéo » La gestion des collisions

La gestion des collisions


La gestion des collisions est sans doute l'une des plus grandes difficultés auquel sont confrontés les développeurs. Il n'est pas rare de voir des bugs de gestion de collisions dans les jeux vidéo. Dans notre cas, cette gestion est simple et nous allons essayer de la faire comme il faut.





Chapitre précédent     Sommaire     Chapitre suivant


Le principe


Il n'est pas possible de tester tous les polygones avec tout les murs et personnages. Généralement, les collisions entre adversaires ne sont pas gérés par c'est compliqué et plutôt gênant.

A mon avis, dans notre cas, le plus simple et de considérer le personnage comme un carré. Étant donné que les murs sont des carrés, il suffira de tester si un côté du carré du personnage est en collision avec un côté du carré d'un mur et si c'est le cas, on repositionne le personnage à la limite de la collision.

Le personnage est plus petit qu'une unité. Son centre est toujours dans un carré, et autour de lui il y a 8 murs ou creux.

Prenons un exemple. L'angle horizontal du personnage est de -135° (Sud-Est). Sa position initiale est P et la destination pour l'image à construire est P'. Il faut donc vérifier si le personnage est en collision avec l'un des murs 4, 6 et 7. Pour le mur 4, il y a collision si le côté Est du personnage est plus à l'Est que le côté Ouest du mur.


Voici un exemple de collision :


Bien entendu, tout les tests devront être effectués. Il est important de bien comprendre le principe pour comprendre la suite de ce chapitre.


Connaissance de l'environnement


Tel qu'est structuré le jeu, le personnage sait où il est mais n'a pas connaissance de son environnement car il n'y a pas de lien entre la carte et le personnage. En revanche, la scène contient les deux et peut prendre connaissance de l'environnement du personnage pour lui décrire. Connaissance son environnement, le personnage pourra se déplacer en prenant en compte les informations relatives à son environnement.

Position sur la carte



Pour demander à la carte de lui fournir une vue de l'environnement, la scène à besoin de récupérer la position du personnage sur la carte de façon discrète.
1
2
3
4
5
6
void Personnage::positionSurLaCarte(sint32* x, sint32* y)
{
   // Recupere la position du personnage sur la carte
   *x = (sint32)this->positionX;
   *y = (sint32)this->positionY;
}

La scène peut maintenant prendre connaissance de la position du personnage et la fournir à la scène pour lui demander de lui construire une vue de l'entourage du personnage.

Construction de la vue de l'entourage



La méthode prend en paramètre la position (X;Y) de la case à partir de laquelle sera construite la vue de l'entourage. Ce dernier sera écrit dans le tableau à 8 cases fourni dans un 3ème paramètre.

Ensuite, connaissant la largeur de la carte, on peut déterminer s'il y a un mur ou non sur les 8 cases qui entour la case sur laquelle repose le personnage.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void Carte::entourage(sint32 x, sint32 y, bool8 entourage[8])
{
   sint32 largeurCarte = this->largeurCarte;
 
   // Par defaut, pas de murs
   entourage[0] = 0;
   entourage[1] = 0;
   entourage[2] = 0;
   entourage[3] = 0;
   entourage[4] = 0;
   entourage[5] = 0;
   entourage[6] = 0;
   entourage[7] = 0;
 
   // Construction de la vue de l'entourage dans le milieu de la carte
   if (y > 0 && y < (sint32)hauteurCarte - 1 && x > 0 && x < largeurCarte - 1)
   {
       entourage[0] = this->carte[ (x-1) + ((y-1) * largeurCarte) ];
       entourage[1] = this->carte[ (x)   + ((y-1) * largeurCarte) ];
       entourage[2] = this->carte[ (x+1) + ((y-1) * largeurCarte) ];
       entourage[3] = this->carte[ (x-1) + (  (y) * largeurCarte) ];
       entourage[4] = this->carte[ (x+1) + (  (y) * largeurCarte) ];
       entourage[5] = this->carte[ (x-1) + ((y+1) * largeurCarte) ];
       entourage[6] = this->carte[ (x)   + ((y+1) * largeurCarte) ];
       entourage[7] = this->carte[ (x+1) + ((y+1) * largeurCarte) ];
   }
 
   // ATTENTION : Pour une gestion correcte des collisions,
   // la carte doit etre entouree de murs.
}

Afin d'assurer le fait que la case existe lors de sa récupération, j'ai fait le test que la case du personnage ne se trouve pas sur un bord. Si c'est le cas, la lecture est faussée. C'est pourquoi il sera indispensable d'empêcher le personnage s'approcher de la bordure de la carte en plaçant un mur tout autour.


Lorsqu'il s'agira d'avancer...



Pour le moment, lorsqu'on souhaite faire avancer le personnage, on fait un simple appel à la méthode Personnage::avancer(). Maintenant, les choses se passeront de façon un peu différente. On récupèrera la case sur laquelle est placé le personnage, on demandera à la carte de nous fournir une vue de l'entourage du personnage puis nous demanderons au personnage d'avancer en tenant compte de son entourage.
1
2
3
4
5
6
7
8
        // Recuperation de la case du personnage sur la carte
       this->personnage->positionSurLaCarte(&positionCarteX, &positionCarteY);
 
       // Recuperation des murs qui entour le personnage
       this->carte->entourage(positionCarteY, positionCarteX, entouragePersonnage);
 
       // Fait avancer le personnage dans son environnement
       this->personnage->avancer(distance, entouragePersonnage);

L'axe des X pour le personnage dans la scène correspond à l'axe des Y pour la carte. De même, l'axe des Y du personnage correspond à l'axe des X pour la carte. Il y a donc une "inversion" des axes entre l'opération de lecture de la position du personnage et celle de la demander de création de l'entourage.


Remarquez que j'ai ajouté un paramètre à la méthode Personnage::avancer(). Dans la partie qui suis, nous déterminerons la destination du personnage en prenant en faisant la gestion des collisions grâce à l'entourage maintenant connu de la méthode avancer().


La gestion des collisions


Détection de collision



Nous l'avons vu, détecter la collision n'est pas très compliquée. Faisons le test avec un mur Est (mur 4).

On se rend compte qu'il nous manque encore une donnée : le "rayon" du personnage.


On l'ajoute donc en attribut et on fait le test :
1
2
3
4
5
6
7
// S'il y a un mur  droite
// et que la droite du personnage arrive dans le mur
if(  (1 == entourage[4])
    && ((sint32)(positionCibleY + this->rayon) != (sint32)this->positionY)  )
{
 
}

Dans l'expression du test, on parle de la position cible du personnage.

1
2
3
// Calcul de la position cible du personnage
float16 positionCibleY = this->positionY - distance * sin(this->angleHorizontal * M_PI / 180.0);
float16 positionCibleX = this->positionX - distance * cos(this->angleHorizontal * M_PI / 180.0);

La droite du personnage se calcule en  faisait (positionCibleY + this->rayon). Et celle-ci est dans le mur si elle est dans une case différente de celle du centre, ce qui parait logique. Pour récupérer la "case", on tronque pour ne garder que la partie entière, d'où les deux (sint32).


Gestion de la collision



La collision est détectée. Il ne reste plus qu'à la gérer : faire revenir le personnage en limite de collision.

Par exemple, si la droite du personnage est en Y=4.247, et qu'il y a un mur en case 4, le dépassement est alors de 0.247.


Le calcule du débordement devient maintenant évident : ((positionCibleY + this->rayon) - (sint32)(positionCibleY + this->rayon)). On le soustrait au déplacement théoriquement prévu suivant les Y de la scène.
1
2
// On rectifie la translation en Y (3D)
positionCibleY -= (positionCibleY + this->rayon) - (sint32)(positionCibleY + this->rayon);

Vous savez maintenant comment gérer les collisions avec les murs Nord, Sud, Ouest et Est. Je vous laisse les gérer.

N'oubliez pas d'affecter vos résultats aux coordonnées finales de votre personnage.
1
2
    this->positionY = positionCibleY;
   this->positionX = positionCibleX;

Test des collisions gérées



Pour tester, il vous suffit d'essayer de traverser les murs. N'hésitez pas à modifier la carte ou déplacer la caméra.


Les Y vers la gauche, les X vers la droite.
Seule la collision Est est gérée.
Remarquez la collision qui est n'est pas gérée lorsque
le personnage est en bordure de carte, au début de la vidéo.

Lorsque les 4 collisions sont gérées



Lorsque vos 4 collisions sont gérées, vous devez obtenir le résultat suivant.


En réalité, il y a encore un problème. Regardez la vidéo :


Lorsque le personnage passe "parfaitement" par un angle de mur, la collision n'est pas gérée.

Explication du bug



Le bug vient du fait qu'il manque encore des tests. Les tests actuels ne sont pas suffisant. Regardez l'exemple ci-dessous :


Le personnage est dans l'angle Sud-Est de la case centrale et se dirige vers le Sud-Est. Il n'y a de mur ni en case 4 ni en case 6. Le personnage passe donc les 4 tests de collision sans y entrer ; le personnage avance donc sans rencontrer de mur. Le personnage arrive alors en case 7, il est déjà trop tard, la collision a été raté.

Correction du bug



Pour résoudre ce problème nous allons gérer le cas du personnage qui passe le sommet Sud-Est dans la case 7.
1
2
3
4
5
    // Gestion des collisions avec un mur au Sud-Est
   if (1 == entourage[7]
       && (sint32)(positionCibleX + this->rayon) != (sint32)this->positionX
       && (sint32)(positionCibleY + this->rayon) != (sint32)this->positionY
       )

Ensuite, on distingue 3 cas possibles, les voici en image :


Pour les deux premiers cas, la détection et la gestion de la collision sont simple. En revanche, dans le dernier cas, il faudra déterminer le mur sur lequel va glisser le personnage.

Après réflexion, je me suis rendu compte qu'il été assez simple de savoir comment arrive le personnage sur le mur pour déterminer de quel côté il doit glisser. Regardez ces 2 schémas :


Ici, le personnage doit glisser vers les X+ (vers le bas).


Ici, le personnage doit glisser vers les Y+ (vers la droite).

Si le décision du glissement ne pouvait pas être prise, les pentes des deux flèches serait identique. Je n'ai pas cherché à le démontrer, j'ai simplement suivis mon intuition, ce qui n'est pas forcément une bonne chose. Ceci dit, je n'ai pas rencontré de problème par la suite.


On va donc calculer les 2 pentes et en fonction de la plus grande, le personnage sera translaté soit vers les X- soit vers les Y-.

Voici le code complet de la gestion de la collision avec le mur 7 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
if (1 == entourage[7]
   && (sint32)(positionCibleX + this->rayon) != (sint32)this->positionX
   && (sint32)(positionCibleY + this->rayon) != (sint32)this->positionY
   )
{
   if (positionCibleX > this->positionX && positionCibleY > this->positionY) // Approche
   {
       // Si ce sont les bord en Y qui ont t en contact lors de la collision
       if ( ((positionCibleX + this->rayon) - (sint32)(positionCibleX + this->rayon))
           / ((positionCibleY + this->rayon) - (sint32)(positionCibleY + this->rayon))
           > (positionCibleX - this->positionX) / (positionCibleY - this->positionY))
       {
           positionCibleY -= (positionCibleY + this->rayon) - (sint32)(positionCibleY + this->rayon);
       }
       else
       {
           positionCibleX -= (positionCibleX + this->rayon) - (sint32)(positionCibleX + this->rayon);
       }
   }
   else if (positionCibleX < this->positionX) // Eloigne en X
   {
       positionCibleY -= (positionCibleY + this->rayon) - (sint32)(positionCibleY + this->rayon);
   }
   else if (positionCibleY < this->positionY) // Eloigne en Y
   {
       positionCibleX -= (positionCibleX + this->rayon) - (sint32)(positionCibleX + this->rayon);
   }
}

Je vous laisse réfléchir et coder la gestion des collisions avec les autres murs de vos propre mains. Il s'agit d'un exercice difficile malgré tout.


Bugs rencontrés



Collision non gérée pour les angles multiples de 90°



Lorsque l'azimut du personnage est un multiple de 90° exactement, aucun des 3 test n'est validé. La solution naïve est de se dire qu'il suffirait d'accepter l'égalité lors de la comparaison pour entrer dans l'un des tests. Tant qu'à faire, on évite que ce soit celui qui provoquerait une division par zéro. Il nous reste alors deux choix.

Mais en acceptant l'égalité, on provoquerait la détection d'une collision lorsque le personnage glisserait le long d'un long mur composé de plusieurs murs. Ce qui rendrait le jeu insupportable pour le joueur.

La solution pour laquelle j'ai opté, c'est de m'arranger pour que l'azimut du personnage ne soit jamais proche d'un multiple de 90°.

J'ai demandé conseil sur developpez.net pour résoudre ce genre de problème. Merci à stardeath.

Je vous laisse implémenter ce code à votre jeu.

Cette méthode n'est pas très jolie. Si vous avez mieux à proposer, merci de me le faire savoir. Je vous en remercie d'avance.


Déplacement accéléré en diagonale



Temps que nous sommes dans les corrections de bug, j'ai remarqué un déplacement accéléré lorsqu'on appui sur Z et D (avancer / droite) en même temps. Ce bug est très simple à comprendre. On se déplace deux fois. Pour compenser la vitesse de déplacement, il faut diviser par racine de 2 lorsque le test suivant est validé.
1
2
3
4
5
6
7
8
9
10
11
    #define VITESSE_DEPLACEMENT (2.0f)
 
   // Calcule de la distance parcourir
   float16 distance = (float)tempsDernierPas * VITESSE_DEPLACEMENT / 1000.0f;
 
   // Si le personnage se deplace en diagonal
   if ( (touches[SDLK_w]||touches[SDLK_s]) && (touches[SDLK_a]||touches[SDLK_d]) )
   {
       // Atenuation de la vitesse par racine de 2
       distance *= 0.7071067812f;
   }

Maintenant, le déplacement du personnage est constant. Il reste encore un petit bug : dans le cas ou le joueur appuis sur 3 touches de déplacement, la vitesse est atténuée alors qu'elle ne le devrait pas. L'appuie de 3 touches n'étant pas une situation logique, on peut considérer qu'il est normal de pénaliser le joueur.

Division par zéro



Vous avons 4 tests semblables à celui-ci :
1
2
3
if ( ((positionCibleX + this->rayon) - (sint32)(positionCibleX + this->rayon)) /
   ((positionCibleY + this->rayon) - (sint32)(positionCibleY + this->rayon))
   > (positionCibleX - this->positionX) / (positionCibleY - this->positionY))

On se rend compte qu'il y a 2 divisions et que chacune d'elle peut provoquer une division par zéro.

Pour éviter la division par zéro, on détecte le cas critique à l'aide d'un test puis on apporte une correction. Dans le cas où le problème ne se pose pas, on laisse faire.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Evite la division par zero
if ( ((positionCibleY + this->rayon) - (sint32)(positionCibleY + this->rayon)) == 0.0
  || (positionCibleY - this->positionY) == 0.0 )
{
   positionCibleY -= (positionCibleY + this->rayon) - (sint32)(positionCibleY + this->rayon);
}
else
{
   // Si ce sont les bord en Y qui ont t en contact lors de la collision
   if ( ((positionCibleX + this->rayon) - (sint32)(positionCibleX + this->rayon)) /
      ((positionCibleY + this->rayon) - (sint32)(positionCibleY + this->rayon))
      > (positionCibleX - this->positionX) / (positionCibleY - this->positionY))
   {
       positionCibleY -= (positionCibleY + this->rayon) - (sint32)(positionCibleY + this->rayon);
   }
   else
   {
       positionCibleX -= (positionCibleX + this->rayon) - (sint32)(positionCibleX + this->rayon);
   }
}

On fait la même chose pour les 3 autres tests.




Comme vous avez pu le constater, même pour un simple jeu de FPS, la gestion des collisions n'est pas évidente. Vous comprendrez maintenant pourquoi il n'est pas rare de voir que dans les jeux vidéo de nos jours les collisions ne sont pas toujours très bien gérées.

Pour ce chapitre, je ne vous donne volontairement pas les sources pour que vous cherchiez par vous même et que vous puissiez être fier du travail que vous aurez fournit. Si vous rencontrez des difficultés pour en venir à bout, le forum est là pour vous aider.

Après réflexion, je pense qu'il aurait été préférable de ne faire qu'une seule méthode avancer() qui prendrait en paramètre la distance et l'angle de déplacement : 0° pour aller tout droit, 90° pour aller à gauche... et l'appuie simultané des touches Z et Q aurait définit l'angle à 45°. Si vous mettez en place cette méthode, je serait ravi de voir la façon dont elle a été implémenté et le résultat obtenu. Merci.


Dans ce chapitre, je comptais faire en sorte que la cameras soit le point du vu du personnage mais le chapitre est déjà trop long. Je vous laisse donc le faire vous même dans une méthode Personnage::regarder().

Dans le chapitre suivant, nous allons nous amuser à afficher la scène en relief grâce à des lunettes 3D.




Chapitre précédent     Sommaire     Chapitre suivant



Rédigé par David
Consulté 20931 fois



Hébergeur du site : David
Version PHP : 5.4.45-0+deb7u2
Uptime : 126 jours 4 heures 18 minutes
Espace libre : 2117 Mo
Dernière sauvegarde : inconnue
Taille de la sauvegarde : 1101 Mo


4149576 pages ont été consultées sur le site !
Dont 653 pages pendant les 24 dernières heures.

Page générée en 0.401 secondes


Nos sites préférés
- Création d'un jeu de plateforme de A à Z avec SDL
- Zelda ROTH : Jeux amateurs sur le thème de Zelda
- Zeste de Savoir : la connaissance pour tous et sans pépins
- YunoHost : s'héberger soi-même en toute simplicité
- Site de Fvirtman : recueil de projets et de codes en C et C++


  © 2005-2017 linor.fr - Toute reproduction totale ou partielle du contenu de ce site est strictement interdite.