But: Implémentation d'une classe de base représentant des contours circulaires.
Concepts nécessaires: classes d'objets, constructeurs/destructeurs, surcharge d'opérateurs.
Ces concepts sont expliqués dans les cours 16 à 19. Les exercices de la série 19 contiennent du matériel proche de ce que vous devez programmer.Fichiers utiles: partie1.zip
Indications pour la rédaction du fichier REPONSES: voir le forum.
Les indications ci-dessous présupposent que vous avez créé le 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 à l'aide de la commande unzip partie1.zip (tapez man unzip dans un terminal pour plus de détails sur cette commande). Si vous travaillez sur vos propres machines:
===============================================================================
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 :
Il ne s'agit bien sûr que d'une convention.
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 contours circulaires dans un espace à deux dimensions.
L'idée est que les entités du programme (bactéries, sources de nutriments 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 une bacterie rencontre une source de nutriments elle va pouvoir en consommer. 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 leurs formes effectives. Nous allons dans le cadre de ce projet, utiliser une simplification consistant à approximer la forme de toute entité au moyen d'un contours circulaire. Les tests de collision se feront alors simplement par rapport à ces contours, comme l'illustre l'image ci-dessous :
|
Le test de collision entre une bactérie et une source de nutriments est simplifiée en approximant chacune de ces entités au moyen d'un contour circulaire (cercles en rouge). |
La classe CircularBoundary qu'il vous est demandé de programmer permet de représenter les contours circulaires utilisés pour les tests de collisions.
Un objet de type CircularBoundary est caractérisé par :
Vous doterez la classe CircularBoundary 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).
des «getters» et «setters» (getPosition et setPosition ainsi que getRadius et setRadius) pour la position et le rayon. Le getter pour la position retournera une référence constante à un Vec2d ;
un constructeur de copie;
une méthode move ajoutant à la position de l'instance courante un Vec2d passé en argument (voir à ce propos l'opérateur + des Vec2d) ;
une méthode contains prenant en argument un CircularBoundary, other, et retournant true si other est à l'intérieur de l'instance courante et false sinon. Soient deux CircularBoundary, a et b, a est à l'intérieur de b si :
une méthode isColliding prenant en argument un CircularBoundary, other, et retournant true si other est en collision avec l'instance courante et false sinon. Soient deux CircularBoundary, 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 contains 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 CircularBoundary 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 CircularBoundary;
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 CircularBoundary;
l'opérateur > utilisable comme suit :
body > pointretournant true si point est à l'intérieur de body et false sinon. Les objets body sont de type CircularBoundary et point sont de type Vec2d;
[Question Q1.1] Comment coder les trois opérateurs précédemment décrits en évitant toute duplication de code dans le fichier CircularBoundary.cpp ? Réfléchissez et répondez à cette question dans votre fichier REPONSES.
CircularBoundary: position = <position> radius = <rayon>où <position> est la position du CircularBoundary et <rayon> son rayon.
[Question Q1.2] 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.3] 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.4] Quelles méthodes parmi celles que l'on vous a demandé de coder 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 circularBoundaryTest définie dans le fichier partie1/src/CMakeLists.txt et qui fait appel à plusieurs fichiers;
Cette cible permet de lancer le programme de test tests/UnitTests/CircularBoundaryTest.cpp. Ce programme fait appel aux différentes fonctionnalités que vous avez codées dans la classe CircularBoundary 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(circularBoundaryTest ${TEST_DIR}/UnitTests/CircularBoundaryTest.cpp ${CB_SOURCES} ${CACHE_SOURCES})
configure_sfml_executable(circularBoundaryTest ....)
permettant de lancer tous les tests CircularBoundaryTest.
===============================================================================
All tests passed (60 assertions in 9 test cases)
(et qui montre que la classe 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/CircularBoundaryTest.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 CircularBoundaryTest.cpp définit un scénario (« test case ») permettant de tester les fonctionnalités liées à la détection de collision en faisant appel aux méthodes que l'on vous a demandé de coder dans la classe CircularBoundary.
Le scénario contient un certain nombre de messages (étiquetés par les mots-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 et CHECK_FALSE.
Examinons par exemple le premier test du scénario (lignes 35 à 50).
Ce test commence par construire deux contours circulaires identiques (b1 et b2 de position (1,1) et de rayon 2), ils sont supposés être en collision si votre code est correct (c'est ce qui est affiché au moyen du THEN). Pour tester que votre code fonctionne correctement pour ce cas, il faut procéder à quatre CHECK :
Vous l'aurez compris, un CHECK réussit si la condition qui lui est passée en argument retourne true (de façon analogue un CHECK_FALSE 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 CircularBoundaryTest.cpp affichera :
-------------------------------------------------------------------------------
Scenario: Collision/IsInside with CircularBoundary
Given: Two identical bodies
Then: they collide
-------------------------------------------------------------------------------
src/Tests/UnitTests/CircularBoundaryTest.cpp:26
...............................................................................
src/Tests/UnitTests/CircularBoundaryTest.cpp:37: FAILED:
CHECK( b1.isColliding(b2) )
with expansion:
false
src/Tests/UnitTests/CircularBoundaryTest.cpp:38: FAILED:
CHECK( b2.isColliding(b1) )
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 ("Two identical bodies") et ce qui est supposé être vrai dans ce cas ("they collide") puis la liste des assertions échouées et leurs lignes.
Le lancement de la cible CircularBoundaryTest devrait produire la sortie suivante :
CircularBoundary: position = (1, 1), radius = 2 =============================================================================== All tests passed (42 assertions in 1 test case)
Note: dans QtCreator cette sortie est visible dans la fenêtre «Application output»
Ceci indique que 42 assertions (CHECK ou CHECK_FALSE ) ont été lancées avec succès dans le cadre d'un seul scénario ("1 test case").
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 isColliding (prenant en argument un CircularBoundary) n'est pas codée chez vous, vous aurez alors un message d'erreur tel que
error: no matching function for call to 'Body::isColliding(Body&)'
CHECK(b1.isColliding(b2));
Le lancement de la cible CircularBoundaryTest devrait produire la sortie suivante :
CircularBoundary: position = (1, 1), radius = 2 =============================================================================== All tests passed (42 assertions in 1 test case)
Le message CircularBoundary: position = (1, 1), radius = 2 doit en outre s'afficher pour montrer que l'exécution de la ligne 44 s'exécute correctement (c'est à dire que votre surcharge de l'opérateur << fait bien ce que l'on attend d'elle).