Imprimer
Catégorie : SAÉ - Développement IHM
Affichages : 864

Préambule

L'objectif de ce troisième article est de présenter comment résoudre le challenge du TD n°2 qui a été laissé de côté : les obstacles inclinés.

 

Pour suivre correctement ce cours, il est conseillé d'ouvrir en parallèle les sources, dont l'archive est téléchargeable [ ici ] 

 

1°/ Un nouvel état : la glissade

Dans le TD n°2, si on incline suffisamment une des plateformes, le sprite se met à glisser. C'est un effet de bord de la mise en place de la glissade lorsque l'on arrive près du bord d'une plateforme. Le problème de cette solution est qu'elle consiste à décaler le sprite de quelques pixels puis de le mettre en chute verticale. Il va donc tomber de quelques pixels, s'arrêter (puisqu'il y a un obstacle), puis repartir en glissade, etc. Le mouvement résultant est donc haché car le sprite n'arrête pas de bouger puis s'arrêter. Selon la pente, on peut même voir le sprite faire un petit mouvement ondulatoire. Enfin, même si la pente est très accentuée, le sprite ne glisse pas forcément plus vite qu'avec une pente moindre.

La solution globale à ce problème est simple :

Cela suppose d'ajouter un nouvel état au sprite : "glissade" et donc, TOUTES les conditions dans lesquelles le sprite passe, reste ou sort de glissade. Malheureusement, ce n'est pas un problème très simple à résoudre, surtout quand les pentes évoluent et si on veut effectivement créer un effet de glissade proportionnel à la pente.

Pour simplifier le codage, il convient donc d'avoir une vue d'ensemble du comportement du sprite et de fixer des conditions précises pour entrer/rester/sortir des états possibles.

Dans cet article, le sprite peut être :

Dans le code source, il existe un 6ème état : bloqué, qui est temporaire. Il est seulement utilisé par le contrôleur pour signifier au modèle qu'au cours de la frame courante, le sprite est devenu bloqué par un obstacle et qu'il faut le mettre à une position bien précise. Quand le modèle du sprite se met à jour, le sprite est effectivement déplacé à cette position, puis il passe en état immobile. Un sprite ne commence donc ni ne termine une frame dans l'état bloqué.

 

2°/ Gestion des collisions

2.1°/ CollisionData et CollisionSummary

Toute la gestion des états exposée dans la section suivante repose sur la capacité à détecter des collisions. Les principes utilisés ici sont les mêmes que dans les articles précédents : on utilise la méthode Shape.intersect() pour obtenir l'intersection entre 2 objets Shape, sous entendu le sprite et un obstacle.

Pour rendre le code plus compact/lisible, la zone d'intersection (qu'elle soit vide ou non) est stockée dans un objet ColllisionData. Pour rassembler les informations de toutes les collisions, on utilise un objet CollisionSummary, contenant une liste de CollisionData. Chaque fois que l'on ajoute une collision à la liste, on demande au préalable à la vue du sprite de calculer les quadrants dans lesquels se produisent la collision.

Une fois que tous les objets collisions ont été ajoutés, la classe CollisionSumary contient des méthodes pour tester dans quels quadrants il y a collision, s'il y a collision dans au moins un quadrant du haut/bas, etc.

NB : cette façon de collecter les informations de collision n'est pas générique et n'est pas réellement utilisable lorsque différents sprites se déplacent.

2.2°/ Filtrer les collisions

Au lieu de tester tous les obstacles possibles, le code source met en place une stratégie de filtrage des obstacles qui seront potentiellement impliqués dans une collision pour la frame courante. Pour cela, il suffit de définir la zone maximale que peut occuper le sprite entre le début et la fin de la frame, en fonction de l'état courant et des états futurs possibles.

Dans ce but, un attribut filterArea a été ajouté à la classe ManLook. C'est une instance de Rectangle, non ajouté à la scène donc invisible (comme les hitbox). A chaque début de frame, le contrôleur appelle une méthode de ManLook dont le rôle est de mettre à jour les coordonnées + dimension de filterArea. Ensuite, le contrôleur vérifie pour chaque obstacle s'il y a collision avec le sprite. Si ce n'est pas le cas, l'obstacle ne sera pas utilisé lors des tests de collisions de la frame courante.

NB : Il existe d'autres stratégies pour réduire les tests de colllision, notamment les quad-tree qui sont les plus connus/utilisés.

 

3°/ Gestion des états

Le principe du contrôleur général est de prendre l'état courant du sprite en début de frame, et en fonction des touches enfoncées et de cet état, de déterminer son prochain état et sa prochaine position. On doit donc définir toutes les transitions possibles entre le début et la fin de frame, comment sont testées ces transitions, et éventuellement dans quel ordre. En effet, au cours d'une même frame, il est possible que le sprite passe par différents états. Par exemple, si le sprite est immobile au début de la frame, qu'aucune touche n'est enfoncée, on doit tester s'il repose de façon stable sur un sol. Si ce n'est pas le cas, il peut passer à l'état glissade. Dans ce cas, vu que cela provoque un déplacement, il faut tester si la glissade est possible. Si ce n'est pas le cas, alors le sprite passe à l'état bloqué, puis immobile. Au final, entre le début et la fin de frame, il aura changé 3 fois d'état, malgré le fait qu'il commence et termine dans l'état immobile. L'ordre dans lequel on teste les conditions est donc très important.

Ces conditions peuvent être dictées par la physique, ou tout simplement par le gameplay voulu par les développeurs, qui ne sera pas forcément très réaliste mais plus adapté au jeu, plus réactif, etc. Par exemple, dans cet article, le sprite est une boule. On s'attend donc à ce qu'elle roule vers le bas en cas de pente négative. Qu'arrive-t-il quand elle heurte une surface horizontale après un saut ou une chute non verticale ? Dans la plupart des jeux de plateforme, le sprite d'arrête, quelle que soit la forme qu'il a. Ce n'est pas du tout réaliste mais utile au gameplay et surtout bien plus simple à gérer : un sprite qui bouge tout seul avec inertie + frottements est plutôt compliqué à modéliser.

Du point de vue algorithmique, ces conditions imposent toutes de détecter des collisions. Si l'on veut le faire finement, cela doit être au pixel près. On peut donc utiliser l'algorithme de bresenham pour déterminer le chemin entre la position actuelle et la future position, en fonction du type de mouvement. En suivant ce chemin, on détermine s'il y a collision ou non, et si oui, on vérifie les conditions pour changer éventuellement l'état du sprite.

La détection des collisions utilisée dans le code repose sur les mêmes principes que dans l'article précédent, avec notamment la prise en compte du quadrant dans lequel a lieu une collision. Pour rendre le code plus compact/lisible, la zone de collision avec un obstacle est stockée dans un objet CollisionData. Comme le sprite est

 

Dans les sections suivantes, les conditions sont exposées globalement dans l'ordre de test. Des remarques viennent expliquer cet ordre et/ou comment sont faits les tests.

Sauf mention d'un OU en début de condition, les conditions doivent toutes être réunies.

3.1°/ immobile

1- conditions pour passer en mouvement latéral :

2 - conditions pour passer en saut :

3 - conditions pour rester en immobile :

4 - conditions pour passer en chute :

5 - conditions pour passer en glissade :

 

Remarques :

 

3.2°/ mouvement latéral

1 - conditions pour passer en immobile :

2 - conditions pour passer en saut :

3 - conditions pour rester en mouvement latéral :

4 - conditions pour passer en glissade :

5 - conditions pour passer en chute :

6 - conditions pour passer en bloqué :

En l'absence de pente, un mouvement latéral correspond à déplacer le sprite en {x,y} de P pixels à gauche/droite, donc vers la position {x+P,y}. On peut considérer P comme la "vitesse" actuelle du sprite. Dès lors qu'il y a une pente, le sprite va se retrouver dans une position entre {x+P,y+P} et {x+P,y-P}, ou bien être bloqué/en chute.

Le choix d'une pente maximale de 45° permet de beaucoup simplifier les tests. En effet, 45° correspond simplement à un pixel plus haut ou plus bas que le précédent.

Pour connaître sa position/état final, il suffit de suivre l'algorithme suivant :

height = 0
pour i de 1 à P (ou de -1 à -P pour aller à gauche) :
   C1 = tester collision en {x+i,y+height}
   si C1 = quadrant bas :
      tester collision en {x+i,y+height-1}
      si collision => bloqué
      sinon height--
   sinon :
      C2 = tester collision en {x+i,y+height+1}
      si C1 = quadrant haut et C2 = quadrant-bas => bloqué
      sinon si C1 = aucun quadrant : 
         tester collision en {x+i, y+height+2}
         si aucune colllision => chute avec une vitesse en X = i-1
         sinon height++
      fsi
   fsi
fpour

 

Remarque : quand le sprite est bloqué ou bien en chute, sa position finale est {x+i-1,y+height}, c'est-à-dire l'avant dernière position testée, dans laquelle le sprite n'était pas en collision.

 

3.3°/ chute

1 - conditions pour passer en immobile :

2 - conditions pour passer en saut :

3 - conditions pour passer en mouvement latéral :

4 - conditions pour passer en glissade :

5 - conditions pour rester en chute :

6 - conditions pour passer en bloqué :

 

Remarques :

 

3.4°/ saut

1 - conditions pour passer en immobile :

2 - conditions pour rester en saut :

3 - conditions pour passer en mouvement latéral :

4 - conditions pour passer en glissade :

5 - conditions pour passer en chute :

6 - conditions pour passer en bloqué :

 

Remarques :

 

3.5°/ glissade

1 - conditions pour passer en immobile :

2 - conditions pour passer en saut :

3 - conditions pour passer en mouvement latéral :

4 - conditions pour passer en bloqué :

5 - conditions pour passer en chute :

6 - conditions pour rester en glissade :

 

Remarques :

 

Les conditions de glissade sont les plus complexes à tester car elles doivent tenir compte du fait qu'au début d'une frame la vitesse en x/y va définir un chemin de pixels éventuellement très différent de la pente réelle.Il faut donc retrouver la pente réelle et positionner le sprite le long de cette pente. Cette recherche doit tenir compte de cas épineux, d'où la difficulté.

 

Quand la pente est plus faible que la vitesse en x/y initiale de la glissade, à chaque pixel du chemin, il faut chercher une position sans collision. La direction de cette recherche doit tenir compte de la vitesse en x/y. En effet, si vy > vx, alors le chemin de pixel va plus vers le bas. Il faut donc rechercher les positions sans collision à droite/gauche, selon le signe de vx. En revanche, si vx > vy alors, il faut chercher les positions vers le haut.

Au cours de cette recherche, principalement quand c'est vers la gauche/droite, on va potentiellement beaucoup modifier la position du sprite par rapport au chemin initial. Cette modification peut être suffisamment importante pour que la position finale du sprite soit irréaliste par rapport à la vitesse finale prévue. Par exemple, supposons un chemin prévu vertical sur 10 pixels, et qu'au 3ème pixel, on tombe sur une pente de 30° vers le bas droite. Il y a donc collision et on cherche à droite la première position sans collision, qui devrait se trouver en gros 2-3 pixels plus loin. Idem pour tout le reste du chemin. Cela implique qu'à la fin du chemin, la position finale en y sera celle prévue, mais celle en x sera en gros 20 pixels à droite. Cela implique que la vitesse absolue en fin de chemin sera sqrt(10*10+20*20) = 22. Ce n'est pas possible ! La vitesse maximale atteignable est celle définie par la longueur du chemin, en l'occurence 10. En conclusion, lorsque l'on parcourt le chemin, il faut également vérifier que la nouvelle position ne conduit pas à dépasser la vitesse maximale. Sinon, on arrête le parcours et on positionne le sprite à la position précédente, en le laissant à l'état glissade.

Quand la pente est plus grande que la vitesse en x/y initiale, il n'y aura pas de collisions et le sprite se retrouvera "en l'air" à la fin de la glissade. Dans ce cas, il faut chercher si le sprite va rester "collé" à la pente, ou bien va entrer en chute. Une solution est de calculer la position maximale en y comme si le sprite était en chute libre au lieu d'une glissade. Dans tous les cas, le sprite ne peut pas aller plus bas. Si à la fin de la glissade, on cherche une collision vers le bas et qu'elle intervient avant cette position maximale, alors le sprite est déplacé à cette position-1. Sinon, il chute.

Fort heureusement, si la pente change au cours de la glissade, les deux cas précédents permettent d'en tenir compte.


4°/ Améliorations possibles

Comme énoncé en section 2, la collecte des informations de collision devrait être plus générique. Cela pourrait se faire grâce à une classe interface qui serait implémentée par toutes les classe de Look nécessitant une détection de collision. Cette interface contiendrait les méthodes permettant de tester une collision (NB : on pourrait donc utiliser un autre principe que Shape.intersect() ), de trouver les quadrants, les hauteurs/largeurs des zones de collision, etc.

A ce propos, on pourrait ajouter une gestion plus complexe des chocs pour ralentir plus ou moins le sprite en cas de collision. Par exemple, si le sprite chute vers la droite et qu'il entre en collision dans son quadrant bas-gauche, on peut calculer la "largeur" de la zone de collision par rapport au bord gauche du sprite. Cette largeur représente en quelque sorte la largeur de l'impact et la perte de vitesse qui en découle. En effet, si l'impact à lieu plutôt sur le côté gauche d'une bille, on peut dire qu'elle glissera plus facilement que si elle heurte un obstacle plutôt sous elle, près du centre. Traduit en code, la perte de vitesse est proportionnelle à la largeur de l'impact.

La glissade s'arrête dès que le sprite arrive en position stable. On pourrait reprendre le système du mouvement latéral pour qu'elle puisse continuer son mouvement, mais en ralentissant proportionnellement à la pente qu'il suit. Cela pourrait même permettre de remonter une pente si la vitesse en suffisante.