Projet : étape 3.1
Confinement

Buts:

  1. revoir différents éléments de la conception établie jusqu'ici en exploitant le polymorphisme;
  2. et confiner les entités simulées dans les cages (elles ne pourront désormais être créées que dans des cages).

Collection héterogène

Il est bien évident qu'avoir un seul hamster et un seul tas de granulés dans un Lab est quelque peu limitatif. Nous percevions jusqu'ici chaque entité simulée sous sa forme propre (Pellets ou Hamster) et c'est en tant que telle qu'elle était simulée et dessinée. Le polymorphisme nous permet désormais de manipuler ces entités sous une étiquette plus générique et de produire du code qui s'adapte automatiquement à la nature des objets auquel il s'applique.

Le but est donc maintenant de disposer d'une collection unifiée d'entités à simuler (contenant aussi bien des hamsters que des tas de granulés) et de coder Lab::update et la méthode Lab::drawOn de façon beaucoup plus synthétique  :

for (auto& entite: lesEntites)
  entite->update(dt); //mettre à jour le Lab c'est mettre à jour chacune de ses entités
  //....
for (const auto& entite: lesEntites)
  entite->drawOn(target); //dessiner le Lab c'est dessiner chacune de ses entités
  //....

L'intérêt est qu'il devient possible désormais d'ajouter n'importe quel nouvel animal ou nouvelles formes de nourriture à notre hiérarchie et le placer dans le Lab sans avoir à retoucher le code de cette classe.

Commencez donc par supprimer vos attributs de type Animal* et Pellets* dans Lab et remplacez les par un collection de Entity* (par exemple au moyen d'un std::vector).

[Question Q3.1] Pourquoi le symbole & après auto dans la boucle réalisant les upate?

[Question Q3.2] Pourquoi une collection de pointeurs  ?

[Question Q3.3] Les méthodes de dessin des Hamster et Pellets sont pour l'heure probablement quasiment identiques (seul l'accès à la texture change), ce qui induit des duplications de code. Sachant que la façon de dessiner les informations de debugging peut être amenée à être spécifique (plus d'informations à afficher en mode «debug» pour les animaux que pour les tas de granulés par exemple), quelles modifications suggérez vous d'apporter au code existant pour éviter les duplications de codes non nécessaires lors du dessin des objets?

Répondez succinctement aux questions précédentes dans votre fichier REPONSES et modifiez votre code en conséquence.

Après ces modifications, une première question qui se pose est de savoir s'il est pertinent de conserver une méthode addAnimal et une méthode addPellets dans Lab? La réponse est a priori non. Il suffit désormais d'avoir une méthode bool addEntity(Entity*) qui s'occupe d'ajouter un Entity* à la liste des entités simulées. Si cette entité vaut nullptr, elle ne sera pas ajoutée et la méthode retournera false. Autrement, l'entité est ajoutée à l'ensemble des entitées simulées et la méthode retourne true.

Ne jetez pas pour autant vos addAnimal et addPellets. Remplacez simplement le corps de la première par un appel tel que:
return addEntity(the_animal); //the_animal est le paramètre  de addAnimal, qui peut devenir Animal*, si ce n'était déjà le cas
et procédez de façon analogue pour la seconde. Nous aurons l'occasion d'y revenir dans un autre contexte.

[Question Q3.4] Quelles retouches devez vous apporter à votre programme pour que les entités en fin de vie continuent de disparaître proprement et pour que le «reset» et la destruction du Lab demeurent corrects ?

Attention : supprimer un élément d'un ensemble pendant que l'on en train de le parcourir au moyen d'une boucle «for auto» invalide l'itération («segmentation fault» en vue !). Il est donc plutôt recomandé de remplacer les entités à supprimer par nullptr, puis de supprimer tous les nullptr d'un coup une fois que l'on a terminé d'itérer.
Dans un ensemble de pointeurs, pointerCollection, il est possible de supprimer tous les éléments valant nullptr au moyen de la tournure suivante :

  pointerCollection.erase(std::remove(pointerCollection.begin(), pointerCollection.end(), nullptr), pointerCollection.end());
  
Faites attention à inclure le fichier d'entête <algorithm> pour pouvoir utiliser la fonction remove. Prenez aussi garde aux fuites de mémoire!

Test 9 : polymorphisme

Lancez la cible labTest et ajoutez un hamster et un de tas de granulés. Votre programme devrait fonctionner comme auparavant. Vérifiez que vous pouvez désormais ajouter plusieurs hamsters et tas de granulés (la touche E diminue la quantité du dernier tas ajouté).

Vérifiez enfin également, que le «reset» fonctionne comme précédemment (il vide bien les cages de leur contenu en préservant les cages avec la touche 'R' et reconstruit des cages vides comme au début de la simulation avec 'Shift-R').

Les entités simulées vues comme des CircularBody

Nous allons maintenant continuer à exploiter nos nouvelles connaissances sur le polymorphisme pour intégrer la classe CircularBody à notre conception. Cette dernière avait été introduite lors de la première étape du projet afin de permettre de tester la collision entre deux corps circulaires. C'est précisément ce qui va nous permettre de gérer les rencontres éventuelles entre un hamster et ses sources de nourriture.

Une conception possible consiste à considérer qu'une entité simulée est (aussi) un « objet circulaire», et donc d'instaurer un lien d'héritage entre la classe Entity et la classe CircularBody . Nous opterons pour ce choix. Ceci nous permet de disposer d'un test de collision, certes imparfait, mais très simple entre deux Entity.

Nous nous heurtons cependant à une petite question d'ordre conceptuel : un CircularBody avait été conçu à la base comme une entité autonome dotée d'un centre. Pour qu'un CircularBody puisse représenter le cercle englobant une Entity, il faut que le centre du CircularBody soit assimilée à la position de cette dernière (une discussion analogue peut se faire autour de la notion de rayon).

Une conception naturelle consiste ici à considérer que le centre du CircularBody et son rayon sont, en fait, dictés par l'objet qu'il est amené à englober (et que par conséquent, le centre et le rayon ne sont pas propres au CircularBody). En clair, c'est l'objet englobé qui décide de qui est son centre et son rayon.

Retouchez votre conception de sorte à ce que CircularBody deviennent une classe abstraite, sans attributs pour le rayon et le centre.

Le rayon et le centre doivent donc désormais être communiqués par les classes concrètes dérivant de CircularBody, ce qui implique que getRadius et getCenter deviennent polymorphiques (et sans définition possibles au niveau de la classe CircularBody !). Vous noterez qu'un CircularBody n'a désormais plus besoin de constructeur explicite.

[Question Q3.5] : comment redéfinir getCenter dans Entity pour que la position de l'entité correspondent à son centre en tant que CircularBody?

Vous redéfinirez getRadius de façon approriée dans les sous classes pour que le rayon d'une Hamster soit la moitié de la taille que nous avions utilisée pour le graphisme (getAppConfig().hamster_size) et vous procéderez de façon analogue pour un Pellets (en prenant par exemple getAppConfig().food_initial_energy comme taille).

Par souci de cohérence, modifiez également les méthodes de dessin de Hamster et Pellets de sorte à ce que la taille de l'objet dessiné corresponde au double de son rayon.

[Question Q3.6] : quelle autre conception est-il possible de mettre en place pour utiliser la classe CircularBody en vue de faire des tests de collision entre Entity ? Quel avantage/inconvénient y voyez-vous à ce stade ? Répondez à ces questions dans votre fichier REPONSES.

Test 10 : Corps circulaires abstraits

Pour garantir que vos retouches n'ont pas altéré les fonctionnalités de la classe CircularBody, lancez le test fourni dans Tests/UnitTests/CircularBodyTest.cpp (il s'agit d'une variante de celui fourni à l'étape 1 et qui s'adapte aux changements de conception voulus). La cible à utiliser est circularBodyTest.

N'oubliez pas de décommenter cette cible dans le fichier CMakeLists.txt (aux deux endroits où elle est définie) et d'exécuter la commande Build > Run CMake.

Vous devriez obtenir à nouveau l'affichage suivant :

===============================================================================
All tests passed (36 assertions in 1 test case)

Affichage en mode debugging

Ajoutez enfin la possibilité qu'en mode «debug», le CircularBody englobant chaque entité simulée s'affiche en transparence (ceci permettra de vérifier la bonne gestion des situations de collision). Pour cela, il suffira de doter la classe CircularBody d'une méthode de dessin réalisant ce traitement :

auto circle(buildCircle(getCenter(), getRadius(), sf::Color(20,150,20,30)));
target.draw(circle);
et de l'invoquer de façon appropriée.

Test 11 : Entités simulées vues comme des CircularBody

Utilisez à nouveau la cible labTest et créez un hamster et des tas de granulés.

Vous devriez obtenir un affichage ressemblant à ceci en mode «debug»  :

Dessin des obstacles englobant

Confinement

On souhaite maintenant faire en sorte  :

Il faut, pour cela, retoucher la méthode addEntity de sorte à ce qu'elle ne permette plus d'ajouter inconditionnellement une entité à la collection d'entités simulées.

Vous pourrez mettre en oeuvre l'algorithme simple suivant: