But: Implémentation d'une classe de base représentant des corps circulaires pouvant se déplacer dans un monde torique.
Concepts nécessaires: classes d'objets, constructeurs/destructeurs, surcharge d'opérateurs.
Ces concepts sont expliqués dans les cours 15 à 18. Les exercices de la série 19 contiennent du matériel proche de ce que vous devez programmer.Fichiers utiles: partie1.zip
Les indications ci-dessous présupposent que vous avez créé un dossier particulier pour le projet (nous supposerons ici qu'il s'agit d'un répertoire cpp/projet). Adaptez les commandes à l'endroit où se trouve le projet chez vous.
Copiez l'archive fournie partie1.zip dans votre répertoire cpp/projet et décompressez-la :
=================================================================== All tests passed (60 assertions in 9 test cases)La signification de cette exécution vous sera expliquée un peu plus bas.
L'archive fournie contient :
Pour augmenter la modularité de votre projet, le prototypage des méthodes et la déclaration des attributs devront se trouver dans un fichier avec l'extension .hpp tandis que leurs définitions se trouveront dans un fichier avec l'extension .cpp.
À propos de la compilation, quelques indications utiles :Comment éviter les inclusions multiples :
#pragma once
Convention de nommage pour les fichiers :
Cette partie du projet ne comporte qu'un seul module (volet).
Il s'agit ici de mettre en pratique vos premières connaissances de l'orienté-objet en programmant une classe utilitaire représentant des corps circulaires pouvant se déplacer dans un espace torique à deux dimensions.
L'idée est que les entités du programme (scorpions, lézards, nourriture des lézards etc.) vont évoluer dans un espace à deux dimensions.
Ces entités seront amenées à se «rencontrer» et il faudra pouvoir tester ce fait de façon simple : par exemple, si un scorpion rencontre un lézard, il va pouvoir s'en nourrir. Il est donc important de pouvoir tester que la rencontre a lieu.
Tester la collision/rencontre de deux objets peut-être très complexe selon leur formes effectives. Nous allons dans le cadre de ce projet, utiliser une simplification consistant à approximer la forme de toute entité au moyen d'un corps circulaire. Les tests de collision se feront alors simplement par rapport à ces corps, comme l'illustre l'image ci-dessous :
![]() |
← Le test de collision entre une lézard et un scorpion est simplifiée en approximant chacune de ces entités au moyen d'un corps circulaire (visible en vert pâle). |
Par ailleurs, l'espace à deux dimensions dans lequel vont évoluer les entités du programme devra être interprété comme un environnement torique : c'est à dire qu'une entité dépassant les limites de l'environnement doit se retrouver sur sa face opposée (selon la petite vidéo ci-dessous) :
[Video : déplacement dans un monde torique] |
La classe Collider qu'il vous est demandé de programmer permet de représenter les corps circulaires entre lesquels il sera possible de faire des tests de collision et qui peuvent se mouvoir dans un environnement torique.
Un objet de type Collider est caractérisé par :
Vous doterez la classe Collider des membres suivants :
un constructeur initialisant la position et le rayon de l'objet circulaire au moyen d'un Vec2d et d'un double passés en paramètre (dans cet ordre). Vous pouvez traiter le cas du rayon négatif en lançant une exception que vous ne chercherez pas à rattraper (c'est une erreur fatale qui doit causer l'arrêt du programme). Le constructeur devra garantir que les valeurs fournies en paramètre pour la position sont bien interprétées dans le monde torique : une fois affectées à la position de this, cette dernière devra être rectifiée selon l'algorithme de "clamping" suivant : tant que la valeur de la coordonnée x est inférieure à zéro l'augmenter de la largeur du monde et tant qu'elle est supérieure à la largeur du monde, la décrémenter de cette même valeur. On procédera de façon analogue pour la coordonnée y.
double worldSize = getAppConfig().simulation_world_size; auto width = worldSize; // largeur auto height = worldSize; // hauteurIl faudra pour cela faire l'inclusion de <Application.hpp> dans Collider.cpp (attention pas dans .hpp pour éviter des dépendances circulaires entre classes).
[Question Q1.1] L'algorithme de «clamping» de la position devra être appliqué dans d'autres contextes que celui de la construction. Comment le coder pour que sa réutilisation dans une autre méthode de la classe Collider n'implique pas de duplication de code ? Réfléchissez et répondez à cette question dans votre fichier REPONSES (adaptez votre code en conséquence).
des «getters» (getPosition et getRadius) pour la position et le rayon. Le getter pour la position retournera une référence constante à un Vec2d ;
un constructeur de copie (sa définition par défaut convient bien, mais il est mieux de l'expliciter);
[Question Q1.2] Si les définitions par défaut des constructeurs de copie et de l'opérateur de (re)copie nous conviennent pourquoi est-il préférable de les coder explicitement ? Réfléchissez et répondez à cette question dans votre fichier REPONSES .
Ajoutez ensuite à votre classe Collider les méthodes permettant de le modéliser comme un objet pouvant se déplacer dans un monde torique :
une méthode directionTo prenant en argument un Vec2d, to. Supposons que from soit la position de l'instance courante, la méthode directionTo doit calculer et retourner le Vec2d d'extrémités from et to dans le monde torique :
Pour calculer à quel vecteur les deux extrémités correspondent dans le monde torique, il y a en fait plusieurs choix possibles. L'algorithme appliqué consistera à retenir parmi les candidats possibles pour to celui de plus faible distance à from.
Il y a 9 candidats possibles pour to dans le monde torique :
où width est la largeur du monde et height sa hauteur.
[Question Q1.3] Comment utiliser des boucles pour éviter d'avoir un code trop répétitif dans la mise en oeuvre de cet algorithme ? Réfléchissez et répondez à cette question dans votre fichier REPONSES (adaptez votre code en conséquence).
une méthode directionTo faisant le même travail que la méthode précédente mais prenant en argument un Collider. Le rôle de to est alors joué par la position du Collider en paramètre. Attention à ne pas dupliquer de code.
une méthode distanceTo prenant en argument un Vec2d, to et retournant la longueur du vecteur torique calculé par directionTo(to).
une surcharge de la méthode précédente prenant en paramètre un Collider. Le rôle de to est alors joué par la position du Collider passé en paramètre.
une méthode move ajoutant à la position de l'instance courante un Vec2d, dx, passé en argument (voir à ce propos l'opérateur + des Vec2d) ; la position ainsi obtenue doit subir l'algorithme de «clamping» pour garantir son appartenance au monde torique.
le codage du même traitement au moyen de l'opérateur +=.
[Question Q1.4] Quels arguments de méthodes, parmi celles qu'il vous a été demandé de coder ci-dessus, vous semble-t-il judicieux de passer par référence constante ? Répondez à cette question dans votre fichier REPONSES.
[Question Q1.5] Quelles méthodes parmi celles que l'on vous a demandé de coder ci-dessus vous semble-t-il judicieux de déclarer comme const ? Répondez à cette question dans votre fichier REPONSES.
Ajoutez enfin à votre classe Collider, les méthodes permettant de l'utiliser pour gérer les collisions avec d'autres objets du même type :
une méthode isColliderInside prenant en argument un Collider, other, et retournant true si other est à l'intérieur de l'instance courante et false sinon. Soient deux Collider, a et b, a est à l'intérieur de b si :
une méthode isColliding prenant en argument un Collider, other, et retournant true si other est en collision avec l'instance courante et false sinon. Soient deux Collider, a et b, a est en collision avec b si la distance entre les centres de a et b est inférieure ou égale à la somme des rayons de a et b;
une méthode isPointInside prenant en argument un point (de type Vec2d) et retournant true si le point est à l'intérieur de l'instance courante et false sinon. Un point p est à l'intérieur d'un Collider body si la distance entre p et body est inférieure ou égale au rayon de body;
l'opérateur > utilisable comme suit :
body1 > body2retournant true si body2 est à l'intérieur de body1 et false sinon. Les objets body1 et body2 sont de type Collider;
l'opérateur | utilisable comme suit :
body1 | body2retournant true si body2 est en collision avec body1 et false sinon. Les objets body1 et body2 sont de type Collider;
l'opérateur > utilisable comme suit :
body > pointretournant true si point est à l'intérieur de body et false sinon. Les objets body est de type Collider et point est de type Vec2d;
[Question Q1.6] Comment coder les trois opérateurs précédemment décrits en évitant toute duplication de code dans le fichier Collider.cpp ? Réfléchissez et répondez à cette question dans votre fichier REPONSES.
Collider: position = <position> radius = <rayon>où <position> est la position du Collider et <rayon> son rayon.
[Question Q1.7] Quelle surcharge choisissez-vous pour les opérateurs qu'il vous a été demandé de coder (interne ou externe) et comment justifiez-vous ce choix ? Réfléchissez et répondez à cette question dans votre fichier REPONSES.
[Question Q1.8] Quels arguments de méthodes, parmi celles qu'il vous a été demandé de coder ci-dessus, vous semble-t-il judicieux de passer par référence constante ? Répondez à cette question dans votre fichier REPONSES.
[Question Q1.9] Quelles méthodes parmi celles que l'on vous a demandé de coder ci-dessus vous semble-t-il judicieux de déclarer comme const ? Répondez à cette question dans votre fichier REPONSES.
Pour tester cette partie du projet vous disposez :
de la cible colliderTest définie dans le fichier partie1/src/CMakeLists.txt et qui fait appel à plusieurs fichiers;
cette cible permet de lancer le programme de test src/Tests/UnitTests/ColliderTest.cpp. Ce programme fait appel aux différentes fonctionnalités que vous avez codées dans la classe Collider pour contrôler qu'elles sont correctes.
Jetez petit coup d'oeil au fichier CMakeLists.txt pour voir comment est définie cette cible:
# add_executable (colliderTest ${TEST_DIR}/UnitTests/ColliderTest.cpp ${SOURCES} ${CACHE_SOURCES}) # target_link_libraries(...)
=============================================================================== All tests passed (60 assertions in 9 test cases)(et qui montre que la classe Vec2d que nous avons fournie passe avec succès un certain nombre de tests)
Afin de comprendre le principe de fonctionnement de ces tests, ouvrez le fichier src/tests/UnitTests/ColliderTest.cpp pour l'examiner.
Ce programme de test utilise un framework particulier, appelé Catch, permettant de faire ce que l'on appelle dans le jargon de la programmation des tests unitaires. Il s'agit en fait d'écrire des scénarios de tests pour différentes fonctionnalités codées.
Nous vous expliquons ci-dessous le principe de fonctionnement de ces tests.
Le programme de test fourni dans ColliderTest.cpp définit des scénarios (« test case ») permettant de tester les fonctionnalités de la classe Collider en faisant appel aux méthodes que l'on vous a demandé d'y coder.
Chaque scénario contient un certain nombre de messages (étiquetés par les mot-clés GIVEN, THEN ou AND_THEN) qui vont décrire les conditions de déroulement des différents tests effectués et leur résultats. Ces messages ne s'afficheront qu'en cas d'échec d'un test.
Si tous les tests d'un scénario se déroulent correctement, vous verrez simplement s'afficher un message ressemblant à ceci :
=============================================================================== All tests passed
Les tests consistent à vérifier un certain nombre d'assertions exprimées au moyen de fonctionnalités telles que CHECK, CHECK_FALSE, CHECK_APPROX_EQUAL .
Le test fourni ne contient que deux scénarios (de la ligne 24 à la ligne 159 puis de la ligne 161 jusqu'à le fin), mais il peut y en avoir plus en général. Examinons une partie du premier scénario.
Les lignes 28 et 29 construisent deux Collider identiques en position {1,1} et de rayon 2. Les deux objets ainsi construits sont censés être en collision et c'est ce qui est indiqué dans le THEN en ligne 31. Pour tester que votre code fonctionne correctement pour ce cas, le test demande de vérifier 4 assertions :
Vous l'aurez compris, un CHECK réussit si l'assertion en paramètre retourne true. De même un CHECK_FALSE (par exemple en ligne 57) réussit si la condition qui lui est passée en argument retourne false.
Supposons que votre méthode isColliding soit mal codée, le lancement du programme de test ColliderTest.cpp affichera :
------------------------------------------------------------------------------- Scenario: Collision/IsColliderInside with Collide Given: Two identical bodies Then: they collide ------------------------------------------------------------------------------- ... src/Tests/UnitTests/ColliderTest.cpp:35: FAILED: CHECK( o1.isColliding(o2) ) with expansion: false ...............................................................................
Le test affiche donc le nom associé au scénario, les messages associés à GIVEN et THEN pour indiquer ce que l'on a en entrée (deux Collider identiques) et ce qui est supposé être vrai dans ce cas ("...they collide") puis la liste des assertions échouées et leur lignes. La partie with expansion indique que l'on a essayé de prouver qu'une assertion était vraie alors que ce qui a été trouvé ("with expansion") est false.
Le lancement de la cible colliderTest devrait, une fois le programme corrigé, produire la fin de sortie suivante :
using ./../res/app.json for configuration. =============================================================================== All tests passed (109 assertions in 2 test cases)
Note: dans QtCreator cette sortie est visible dans la fenêtre «Application output»
Ceci indique que 109 assertions (CHECK, CHECK_FALSE etc.) ont été lancées avec succès dans le cadre de deux scénarios ("2 test cases").
Notez que dans le cas où les fonctionnalités testées ne sont pas présentes dans votre code, le lancement du test provoquera des erreurs de compilation.
Par exemple, supposez que le constructeur n'est pas codé chez vous conformément à ce qui a été demandé (arguments manquants ou dans le désordre), vous aurez alors un message d'erreur tel que :
error: no matching function for call to 'Collider::Collider(const Vec2d&, double)'
Le lancement de la cible colliderTest devrait donc produire la fin de sortie suivante :
Collider: position = (1, 1), radius = 2 =============================================================================== All tests passed (109 assertions in 2 test cases)