Projet : étape 2.2
Hamsters et Granulés

Buts:

  1. les hamsters (et les tas de granulés) font leur apparition;
  2. ils sont incapables de mouvement mais .. peuvent néanmoins mourir de vieilliesse.

Notre classe Lab contient pour l'heure un ensemble de cages vides qu'il s'agit de peupler. Le but est d'y voir évoluer des hamsters lesquels seront nourris de tas de granulés. Plus fondamentalement, il s'agit de mettre en place les éléments du programme qui permettront aux entités simulées d'évoluer au cours du temps.

On pourrait a priori se contenter de créer un classe Hamster et une classe Pellets (granulés) ainsi que des méthodes qui permettront d'en placer des instances dans un Lab.

Il est toujours bon cependant, lors de la conception d'un programme, d'essayer de prendre un peu de recul et d'imaginer les évolutions futures : et s'il nous prenait l'envie d'élever des gerbilles, des rats voire des lapins plus tard ou encore de nourrir nos hamsters avec autre chose que des tas de granulés ?

Nous vous suggérons donc de partir plutôt sur la hiérarchie de classes ébauchée dans le descriptif général de cette partie du projet

Image: hiérarchie d'entités simulées

Entity représente ici toute entité pouvant évoluer en cours de simulation. C'est là raison pour laquelle nous y prévoirons d'emblée une méthode update(sf::Time dt) . Une instance de Pellets représente un tas de granulés. Un tel objet varie aussi au cours du temps du temps : il peut se faire grignoter et donc changer de taille ou encore moisir et donc changer d'aspect par exemple.

Vous constaterez que nous n'avons pas créé de classes intermédiaire Food (une idée de pourquoi ? ... indication: et si nous élevions des serpents qui mangent des hamsters :-/).

Ebauchons maintenant le contenu de chaque classe de la hiérarchie ci-dessus.

La classe Entity

Cette classe va nous permettre de regrouper les caractéristiques communes aux entités simulées dans le laboratoire. Pour l'instant, ces caractéristiques communes se bornent aux faits:

A cela, nous ajouterons le fait qu'une entité simulée peut être orientée dans l'espace, a une «âge» et un niveau d'énergie (un double): pour le hamster, ce dernier attribut reflétera son état de santé et pour un tas de granulés, l'énergie qu'il peut fournir à qui le consomme :-).

Pour l'orientation vous pourrez simplement utiliser un angle, le type Angle est fourni dans Utility.hpp :

Image: angle d'orientation
On choisira de travailler en radians car les fonctions trigonométriques de base utilisent cette unité en C++.

Pour l'âge, vous utiliserez le type sf::Time (incluez <SFML/Graphics.hpp>).

Nous partirons aussi de l'hypothèse que toute entité simulée peut être confinée dans un cage et qu'elle a connaissance de la cage dans laquelle elle se trouve.

Pour être compatible avec les tests fournis, vous doterez la classe Entity d'un constructeur prenant en paramètre sa position et son niveau d'énergie. La cage à laquelle elle appartient sera initialisée à nullptr. L'orientation sera initialisée au moyen d'un angle tiré au hasard. La méthode uniform, définie dans Random, permet de générer un nombre aléatoirement entre deux bornes, vous pourrez générer l'angle entre 0 et 2 *PI (notez que nous avons fourni la constante TAU dans Utility/Utility.hpp). Tout entité aura pour âge la valeur prédéfinie sf::Time::Zero à la naissance.

Evolution d'une entité

Comment une entité simulée va-t-elle évoluer au cours du temps? Eh bien à ce stade elle se contentera de prendre de l'âge. Concrètement, à chaque cycle de simulation, son âge augmentera de dt.

Les classes Animal, Hamster, et Pellets

Ces classes devront définir les constructeurs de façon appropriée. Le niveau d'énergie du hamster à la naissance est getAppConfig().hamster_energy_initial. Pour le tas de granulés, vous prendrez getAppConfig().food_initial_energy.

Dessin

Pour dessiner un Hamster ou un Pellets sous la forme d'un objet texturé, vous doterez ces classes d'une méthode de dessin (drawOn) faisant appel à la tournure suivante :

  sf::Sprite  entitySprite = buildSprite(position_entite, une_taille_graphique, getAppTexture(uneTexture),
  orientation_entite/ DEG_TO_RAD); // conversion en degrés car la SFML utilse cette unité
    target.draw(entitySprite);

Pour la texture du hamster vous pourrez prendre le paramètre configurable getAppConfig().hamster_texture_brown et pour sa taille getAppConfig().hamster_size. Pour le tas de granulés, prenez la valeur getAppConfig().food_texture en guise de texture et son niveau d'énergie en guise de taille (plus il est gros plus il est nourissant et a donc beaucoup d'énergie à donner :-)).

Vous noterez qu'un certain nombre de textures sont fournies dans le répertoire res et que la méthode Application::getAppTexture permet de charger une texture de ce répertoire dans un objet de type sf::Texture (lien). Enfin, c'est le fichier fourni Utility.[hpp][cpp] qui offre la méthode buildSprite permettant de construire un objet texturé à afficher.

Les entités simulées intègrent le laboratoire

Pour tester nos récents développements, nous allons considérer pour commencer qu'il n'y a possiblement qu'un seul hamster et qu'un seul tas de granulés dans un Lab.

Bien entendu, cela est amené à évoluer par la suite.. mais attendons pour cela que les collections hétérogènes (polymorphiques) soit entrées dans la danse pour tirer pleinement profit de notre classe Entity ;-)
Ajoutez à Lab un attribut de type Hamster* et un attribut de type Pellets* initialisés à nullptr, ainsi que deux méthodes bool addAnimal(Hamster*) et bool addFood(Pellets*) permettant d'affecter des valeurs à ces attributs. La valeur de retour indique si l'on a pu ajouter l'animal ou le tas de granulés au Lab.

Pour addAnimal, si le paramètre vaut nullptr ou s'il existe un animal dans le laboratoire, on n'ajoute rien au laboratoire et on retourne false sinon on ajoute l'animal et la valeur true est retournée. Un raisonnement analogue sera tenu pour addFood.

[Question(s) Q2.4] Pourquoi addFood et addAnimal peuvent elle potentiellement causer des fuites de mémoire et comment y remédier ?

Quelle(s) autre(s) modification(s) devez vous apporter à la classe Lab pour que s'affichent désormais la hamster et le tas de granulés en plus des cages et quelle(s) précaution(s) devez-vous prendre pour éviter un «crash» du programme s'il n'y pas de hamster ou pas de tas de granulés (valeur à nullptr) ?

Répondez à ces questions dans votre fichier REPONSES et apporter à votre programme les modifications qu'elles suggèrent.

Méthode reset

Il est maintenant temps de compléter la méthode reset de Lab pour le cas où son paramètre vaut false. Dans ce cas, vous ferez en sorte que le laboratoire soit vidé des entités simulées tout en préservant les cages existantes.

Il est plus simple de considérer que lorsque l'on ajoute ou supprime des cages (méthodes addCageToRow et removeCageFromRow), la simulation se vide aussi de ses entités; sinon, il faudrait repositionner les entités dans les nouvelles cages. Vous êtes libre d'implémenter cet algorithme plus complexe en bonus, mais l'option simple est celle attendue à la base.

[Question Q2.5] Quelle modification apporter à Lab pour mettre en oeuvre cette fonctionnalité en sans fuites de mémoire ?

[Question Q2.6] Un laboratoire est non copiable (n'a pas vocation a partager son contenu avec un autre laboratoire). Un Lab peut donc être considérée comme responsable de la durée de vie des Entity amenés à être créées dans la simulation. Quelle incidence cela a t-il sur la destruction d'un Lab ? Répondez à ces questions dans votre fichier REPONSES et mettez en oeuvre les développements ainsi suggérés.

Test 5 : Un laboratoire avec un hamster et un tas de granulés

Vos derniers développements peuvent toujours être testés au moyen de la cible labTest.cpp. Décommentez-y la création des hamsters et portions de granulés (ainsi que des inclusions nécessaires). Les touches H et F devraient respectivement ajouter un hamster et une portion de granulés à l'endroit où est positionné le curseur.

Vérifiez:

Notez que la classe Application dont hérite LabTest permet de «zoomer/de-zoomer» au moyen de la molette de la souris. Elle permet aussi de se déplacer dans la vue au moyen des flèches gauche et droite et de quitter le programme au moyen de la touche Esc.

Mode «debugging»

Le niveau d'énergie d'une entité est une information utile en cours de «debuggage» du programme.

Le fichier de configuration app.json contient une étiquette "debug"

{
   "debug":true,
...
}

Il est possible de l'exploiter pour avoir des affichages différenciés selon que l'on est en mode « debugging » ou pas.

La fonction isDebugOn(), définie dans les fichiers Application.[hpp][cpp], retourne true si la valeur associée à l'étiquette "debug" dans le fichier .json qui a servi à lancer le programme vaut true et false sinon. La méthode switchDebug(), définie dans Config.[hpp][cpp], permet de basculer du mode «debugging» au mode normal et vice-versa. Vous pouvez l'invoquer en utilisant la tournure getAppConfig().switchDebug().

complétez vos méthode de dessin de sorte à ce qu'elles affichent aussi le niveau d'énergie lorsque le mode «debugging» est activé. Pour réaliser des affichages textuels, vous utiliserez la fonction utilitaire fournie buildText (définie dans Utility/Utility.[hpp][cpp]). En voici un exemple d'utilisation:


auto text = buildText(un_texte,
                      position,
                      getAppFont(),
                      getAppConfig().default_debug_text_size,
                      couleur_du_texte,
                      une_rotation_en_radian / DEG_TO_RAD); // si l'on souhaite faire subir une rotation au texte
target.draw(text);
position est la position du texte (celle de l'entité pour simplifier), un_texte est le texte à afficher (une string) et couleur_du_texte est une sf::Color (par exemple sf::Color::Yellow, sf::Color::Blue ou sf::Color::Magenta). La couleur par défaut est paramétrable et accessible via getAppConfig().debug_text_color
Pour convertir un double en string, vous pouvez utiliser la fonction utilitaire fournie to_nice_string (définie dans Utility/Utility.[hpp][cpp]). Dans notre modèle les angles s'expriment en radians. La SFML utilise les angles en degré. Pour faire la conversion en degrés d'un angle en radians, il suffit de le diviser par la constante DEG_TO_RAD fournie dans le fichier Config.hpp.

[Question Q2.7] Comment proposez-vous de procéder pour afficher les niveaux d'énergie sans répéter dans Hamster et Pellets les instructions servant à l'affichage du texte voulu ?

L'ordre dans lequel se font les affichages a un impact: si vous affichez le text avant l'image il apparaîtra en dessous de cette dernière, ce qui n'est pas souhaitable.

Complétez enfin votre méthode reset de sorte à ce qu'en plus de ce qu'elle faisait jusqu'ici, elle fasse appel à la fonction switchDebug() si la simulation est en mode «debugging» (le «reset» doit ainsi causer le retour automatique au mode de simulation normal).

Test 6 : Mode «debugging»

La classe Application dont hérite LabTest définit la touche 'D' qui permet d'activer/désactiver le mode «debugging».

Lancez la cible labTest et créez une hamster. Appuyez ensuite sur la touche 'D'. Vous devriez voir le fond chnager de couleur et observer l'affichage du niveau d'énergie du hamster (et idem pour le tas de granulés). La vue est zoomée dans l'exemple ci-dessous:

Image: labo vide

Vérifiez que la touche D permet de re-basculer dans le mode de simulation normal.

Evolution au cours du temps

Nous avions prévu de voir vieillir nos entités simulée à chaque pas de la simulation.

[Question Q2.8] Quelle autre modification devez vous apporter à la classe Lab pour que ce soit désormais concrètement le cas pour notre hamster et notre tas de granulés (encore une fois attention, ils peuvent ne pas exister et attention aux fuites de mémoire!).

Pour que ce viellissement ait un impact, faites en sorte que les entités simulées meurent et disparaissent de la simulation si elles ont un niveau d'énergie inférieur à zéro où si elles sont dépassé leur longévité maximale. Vous considérerez qu'une Entity a une longévité très grande par défaut (1E+9) mais que les hamsters ont une "longévité" spécifique donnée par le paramètre configurable getAppConfig().hamster_longevity.

Le temps sera géré en secondes.Utilisez la tournure sf::seconds(1E9) pour convertir 1E9 en sf::Time.

[Question Q2.9] Nous faisons ici une première petite incursion dans l'application du polymorphisme. Pourquoi la méthode retournant la longévité doit-elle ici être marquée comme virtuelle?

Vous ajouterez aussi une méthode Quantity provideEnergy(Quantity qte) à la classe Pellets. Cette méthode permet concrètement de consommer une portion du tas de granulés en décrémentant son niveau d'énergie de qte. Il ne peut être prélevé plus d'énergie qu'il n'y en a. Si le niveau d'énergie est inférieur à qte, on prélevera donc ce niveau d'énergie restant et pas plus. La valeur retournée est le niveau d'énergie (la quantité) effectivement prélevé.

Codez les développements ainsi suggérés.

Test 7 : Hamster mortel

Pour tester vos nouveaux développements vous utiliserez à nouveau la cible labTest.

Modifiez res/app.json de sorte à doter les hamsters d'une longévité réduite, par exemple 1 (pour ne pas faire durer trop longtemps le supplice).

Ensuite:

  1. lancez la simulation et mettez la simulation en pause (barre d'espace).
  2. créez une hamster;
  3. relancez la simulation (barre espace);
au bout d'un moment, le hamster devrait s'éteindre de vieillesse et disparaître (un peu abruptement ... il faut le dire) du laboratoire.

[Video : disparition des hamsters trop âgés]

Test 8 : Tas de granulés consommables

La cible labTest définit le touche 'E' qui permet de consommer une quantité donnée du dernier tas de granulés créé. Décommentez l'instruction qui correspond à la touche 'E'. Lancez ce test, créez un tas de granulés et mettez la simulation en mode «debug». Actionnez la touche 'E'. A chaque utilisation de cette touche, la quantité de tas de granulés devrait être décrémentée de 10. Vérifiez que la taille du tas de granulés baisse en conséquence et que le tas de granulés disparaît de la simulation une fois entièrement consommé (le texte associé ne doit plus s'afficher); comme dans la petite vidéo ci-dessous :

[Video : consommation de tas de granulés (ici sur une vue zoomée)]
Vous pouvez jouer avec les valeurs du test pour vérifier que le prélévement des quantités est toujours correct (par exemple, mettre la valeur 9 au lieu de 10 en guise de quantité consommée à chaque fois que la touche 'E' est employée).
Vérifiez enfin que vos tests de l'étape 1 fonctionnent toujours.

Le but de la prochaine étape est de généraliser notre conception quelque peu restreinte du contenu du laboratoire et de donner un peu plus de dynamique à la simulation en permettant aux hamsters de se déplacer et de se nourrir.


Retour à l'énoncé du projet (partie 2)