Projet : étape 1
"Classe utilitaire"

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


Mise en place

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 :

Dans QtCreator, configurez ensuite le projet en suivant scrupuleusement les instructions données ici (il s'agit d'ouvrir le fichier CMakeLists.txt du répertoire src/ et des instructions qui suivent). La seule différence avec la marche à suivre est que vous allez utiliser partie1.zip à la place de sfml_with_cmake.zip comme nom d'archive et partie1/ à la place de sfmlCmake/ comme nom de dossier. Une fois le projet configuré, vous devriez pouvoir le compiler et l'exécuter. L'exécution doit produire la sortie suivante dans la fenêtre «Application output»:
	===================================================================
	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 :
  1. Comment éviter les inclusions multiples :

    Les classes que vous allez programmer dans le projet vont se combiner de façon relativement complexe et vous allez parfois devoir inclure de nombreux fichiers .hpp existants au début d'un nouveau fichier .hpp. Si des fichiers ont été inclus de façon redondante dans ces différents .hpp, le compilateur réagira par un message d'erreur. Pour éviter cette situation, il est nécessaire de mettre au début de chaque fichier monfichier.hpp la directive :
    #pragma once
    
  2. Convention de nommage pour les fichiers :

    Les fichiers porteront les mêmes noms que les classes qu'ils contiennent : par exemple, les fichiers Lizard.[hpp][cpp] contiendront le code relatif à la classe Lizard. Il ne s'agit bien sûr que d'une convention.
    Certains systèmes d'exploitation comme Windows sont insensibles à la casse (c'est le cas aussi des VMs de l'EPFL)  : Vec2d.hpp sera donc interprété de la même manière que vec2d.hpp (commençant par un petit "v"). Inclure le fichier vec2d.hpp sera donc interprété comme correct par le compilateur, même si votre fichier s'appelle Vec2d.hpp. Ce n'est pas le cas dans un environnement Unix classique. Faites donc attention à la cohérence car votre projet doit pouvoir s'exécuter partout.
  3. Compilation dans un terminal (Linux, MacOs):
    Le matériel fourni est intégrable à QtCreator dans lequel vous pouvez compiler et exécuter. Sous Linux et MacOs, vous pouvez aussi parfaitement l'utiliser en ligne de commande comme indiqué ici.

Modules à programmer

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 :

item ← 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  :

Méthodes générales

Vous doterez la classe Collider des membres suivants :

[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 .

Gestion du déplacement dans un monde torique

Ajoutez ensuite à votre classe Collider  les méthodes permettant de le modéliser comme un objet pouvant se déplacer dans un monde torique :

[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.

Gestion des collisions

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 :

[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.

Programmes de test

Pour tester cette partie du projet vous disposez :

  1. de la cible colliderTest définie dans le fichier partie1/src/CMakeLists.txt et qui fait appel à plusieurs fichiers;

  2. 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(...)

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.

Principe de fonctionnement des tests unitaires (non graphiques)

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 :

  1. que le premier objet est en collision avec le second (selon les retours de votre méthode isColliding et de votre opérateur operator|);
  2. et que le second objet est en collision avec le premier (selon des modalités identiques).

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)'
Si vous voulez tester les méthodes au fur et à mesure que vous les codez, ce qui est recommandé, il vous suffit de commenter les CHECK ou CHECK_FALSE qui font appel à des méthodes non encore codées.

Test 1 : Fonctionnalités de Collider

Les tests sont numérotés globalement dans tout le projet. Il s'agit ici du premier test du projet. Chaque test est un élément noté.

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)

Retour au document principal