Projet : étape 3.1
Faune, Flore et Météo

Buts: Il s'agit lors de cette étape  :

  1. de faire en sorte que l'environnement soit doté d'une faune et d'une flore;
  2. d'introduire des épisodes pluvieux conditionnant l'évolution de la flore;
  3. et de revoir différents éléments de la conception établie jusqu'ici.

Paramètres de simulation

Avant de nous pencher sur le codage de cette étape, un petit détour sur la question de comment représenter les paramètres caractéristiques d'une simulation s'impose.

Il peut en effet y en avoir de très nombreux. Par exemple, quelles dimensions donner aux différents composants graphiques, quelles textures utiliser pour dessiner les animaux, etc.

Jusqu'ici, nous avons procédé de façon très ad hoc : quelques constantes codées dans le fichiers Constants.hpp sont utilisées par exemple pour configurer la texture à associer à un animal ou encore les caractéristiques de son champ de vision.

L'inconvénient de procéder ainsi est qu'à chaque fois que l'on veut modifier ces valeurs, il faut recompiler et relancer le programme.

Pour contourner cette limitation, nous allons désormais tirer pleinement parti de la classe fournie Config. Cette dernière contient la définition des constantes utiles au programme qu'elle va lire dans un fichier de configuration au format JSON (voir ci-dessous) . Le noyau de simulation fourni (Application) est doté d'un attribut de type Config et d'une méthode getAppConfig qui permet d'aller puiser des valeurs dans le fichier de configuration. Pour changer une valeur en cours de simulation, sans avoir à recompiler le code, il suffit d'éditer le fichier JSON utilisé, le sauvegarder puis d'aller dans la fenêtre de simulation et y recharger ces modifications au moyen de la touche 'C' ('C' pour "Configuration").

Ceci nous permettra de faire varier à souhait les exécutions en modifiant simplement le fichier de configuration et donc d'expérimenter facilement de nombreuses situations différentes sans avoir à recompiler et relancer le programme.

Format des fichiers de configuration

Il existe plusieurs formats standard pour la modélisation de données (XML, JSON, YAML etc), qui d'ailleurs ne sont pas uniquement utilisés dans le contexte de la programmation.

Pour ce projet, nous avons pris le parti d'utiliser le format JSON pour répertorier les paramètres caractéristiques de la simulation.

Les fichiers de configuration du projet sont les fichiers avec l'extension .json dans le dossier res/.

Le répertoire src/JSON contient toutes les fonctionnalités nécessaires à la récupération des données depuis un tel fichier. Ce sont ces fonctionnalités qu'utilise la classe Config pour aller lires des données dans les fichiers .json.

Dans les fichiers .json, les paramètres sont étiquetés de manière hiérarchique au moyen de chaîne de caractères qui permettent de les décrire.

Par exemple :

{
    "simulation" : {
       ...
        "animal" : {
             ...

           },
           "scorpion":{
            "mass":0.7,
            "size":100,
            "longevity":80000,
            "max speed":100,
            ...
            "texture": "scorpion.png"
         ...
        },
      ...
}
se lit comme :
« un scorpion ("scorpion") est un animal ("animal") de la simulation ("simulation" ) dont les paramètres configurables sont la vitesse de déplacement ("max speed"), la texture employée pour le graphisme ("texture") etc. »

La classe utilitaire fournie Config permet d'accéder à la valeur d'un paramètre donné (en l'associant à une constante publiquement accessible). Par exemple la constante scorpion_texture permettra d'accéder à la valeur du paramètres étiqueté ["simulation"]["animal"]["scorpion"]["texture"] dans le fichier .json.

Comme dit plus haut, la classe Application dispose d'un attribut de type Config, qu'elle d'initialise au moyen du bon fichier .json, et c'est par ce biais que vous pourrez accéder aux paramètres par une tournure de ce type :

getAppConfig().scorpion_texture;
Rappel : les tournures faisant appel à getAppConfig() ne doivent pas être utilisées pour initialiser des constantes globales.

Utilisation des fichiers de configuration

Chaque test que vous lancerez, graphique ou pas, pourra être paramétré par le fichier de configuration souhaité, par exemple, en ligne de commande  :

./ppsTest app.json
permettra de lancer la cible ppsTest en utilisant app.json comme fichier de configuration.
Dans QTCreator vous pouvez passer les arguments à votre programme (app.json dans l'exemple ci-dessus) en procédant tel qu'indiqué ici: https://iccsv.epfl.ch/series-prog/install/installation-tutorial.php#commandLineArgs_in_qtcreator.
Si l'on ne met rien comme argument, le fichier .json utilisé sera celui dont le nom correspond à la constante DEFAULT_CFG définie dans Utility/Constants.hpp. Pour cette étape cette valeur par défaut est app3.json mais libre à vous de le changer.

Un petit mot au sujet des «contrôles»

Le noyau de simulation fourni prévoit que certains éléments essentiels soient contrôlables par des touches, typiquement:

Si vous ouvrez le fichier fourni Application.hpp, vous constaterez (ligne 146) qu'un type énuméré modélise ces contrôles. La méthode Application::handleEvent (ligne 486 de Application.cpp) est programmée de sorte à ce que la touche Q permette de basculer d'un contrôle à l'autre et une fois un contrôle donné choisi, les touches PgUp et PgDn (ou 9 et 8) permettent de faire varier les valeurs du paramètre contrôlé. Ici, si le paramètre contrôlé est la température, les touches PgUp/9 et PgDn/8 appeleront respectivement des méthodes Environment::increaseTemperature et Environment::decreaseTemperature pour faire varier le contrôle «température».

Pour que le code soit compilable ces méthodes (et quelques autres) doivent être complétées dans votrs classe Environment.

Commencez par modéliser le fait que la température devienne un élément caractéristique de l'environnement. Dotez ensuite votre environnement de méthodes publiques permettant d'initialiser de consulter ou de modifier cette température:

Hiérarchie d'«entités organiques»

Nous allons maintenant tirer parti de l'héritage pour revisiter un peu la conception de notre programme.

Jusqu'ici, un environnement était constitué d'un ensemble d'animaux et de cibles, aucun des animaux ne pouvant être assimilé à une cible pour un autre.

Nous allons, à cette étape, plutôt considérer que l'environnement est constitué d'un ensemble d'êtres vivants/entités organiques, dont certains sont « consommables » par d'autres (et peuvent par conséquent devenir des cibles).

Dans le répertoire Environment/, créez une nouvelle classe OrganicEntity dont héritera votre classe Animal : désormais, un animal est une entité organique de l'environnement. Il en sera de même pour les cactus que mangeront nos lézards.

Il doit être possible de faire des tests de collision avec toute entité organique car le fait d'en rencontrer une sur son chemin peut avoir une incidence sur le cours de la simulation. Par conséquent, le lien d'héritage liant jusqu'à présent un Animal avec un Collider devra être déplacé un cran plus haut. La hiérarchie à laquelle nous voulons aboutir est en effet la suivante :

Modele

La classe OrganicEntity aura pour le moment uniquement un attribut pour son niveau d'énergie (un double) . Nous anticipons ici le fait que toute entité organique est mortelle (elle meurt notamment si elle a un niveau d'énergie trop bas).

On considère à ce stade que seuls les animaux se déplacent selon les modalités programmées à l'étape précédente (promenades aléatoires). Tous attributs relatifs à ces déplacements demeurent donc naturellement dans la classe Animal.

Dotez la classe OrganicEntity d'un constructeur prenant en paramètre une position, une taille et un niveau d'énergie (dans cet ordre). Le paramètre relatif à la taille servira à initialiser le rayon de l'entité en tant que Collider (la taille fournie en paramètre sera considérée comme étant le double du rayon).

Adaptez ensuite votre constructeur de la classe Animal de sorte à prendre en compte ces modifications. Ce dernier prendra désormais en paramètre une position, une taille et un niveau d'énergie.

Dans la foulée et puisque les animaux vont se reproduire d'ici à la fin de cette étape, on peut aussi doter les animaux d'un attribut supplémentaire : un booléen indiquant s'il s'agit d'une femelle ou pas. Cet attribut sera aussi initialisé au moyen d'une valeur passée comme dernier paramètre du constructeur.

Des lézards et des scorpions

Nous sommes maintenant prêts à créer animaux spécifiques : des lézards (classe Lizard) et des scorpions (classe Scorpion). Pour l'instant ces classes n'ont rien de particulier en terme d'attributs. Elles seront codées dans le répertoire Animal/.

Notre classe Animal doit par contre subir quelques retouches:

  1. les méthodes getStandardMaxSpeed, getMass, getRandomWalkRadius, getRandomWalkDistance, getRandomWalkJitter, getViewRange et getViewDistance allaient puiser, de façon ad hoc dans le fichier Constants.hpp, des valeurs relatives à un animal quelconque. Ceci n'a pas vraiment de sens : on ne sait pas vraiment définir ces méthodes à ce niveau d'abstraction;
  2. la méthode update ne sait plus trop bien pour l'heure comment identifier les cibles de l'environnement.

Nous nous attaquerons un peu plus loin à la méthode update. Commentez son corps pour le moment.

Les getters peuvent, par contre, trouver des définitions concrètes dans les classes Lizard et Scorpion :

La taille et le niveau d'énergie initial sont aussi des paramètres configurables spécifiques à chaque catégorie d'animal: respectivement, getAppConfig().lizard_energy_initial et getAppConfig().lizard_size pour les lézards et getAppConfig().scorpion_energy_initial et getAppConfig().scorpion_size pour les scorpions.

[Question Q3.1] Quelles méthodes avez-vous décidé de déclarer comme virtuelles pures dans la classe Animal ? A quels endroits l'utilisation du mot clé override vous semble t-il pertinent ? Répondez à ces questions, en justifiant vos choix, dans votre fichier REPONSES.

Vous doterez les classes Lizard et Scorpion de deux constructeurs chacune : l'un prenant en paramètre la position de départ de l'animal (un Vec2d), son niveau d'énergie et un booléen indiquant s'il s'agit d'une femelle ou pas. L'autre, prenant en paramètre uniquement une position et initialisant le niveau d'énergie avec un niveau par défaut dont la valeur est configurable (getAppConfig().lizard_energy_initial et getAppConfig().scorpion_energy_initial). Le sexe de l'animal sera tiré au hasard par ce constructeur.

Pour tirer un booléen au hasard vous pouvez utiliser la tournure :
uniform(0, 1) == 0
ce qui permet d'avoir une chance sur deux d'avoir une femelle.
Ce constructeur sera typiquement utilisé pour mettre en place la reproduction (une animal qui nait aura une chance sur deux d'être une femelle).

[Question Q3.2] : que doit-on retoucher dans le fichier .json si l'on souhaite par exemple modifier en cours de simulation le niveau d'énergie initial par défaut des lézards? Répondez à cette question, dans votre fichier REPONSES.

Dessin polymorphique

Il vous a été expliqué précédemment comment dessiner un animal au moyen d'une texture. Il vous est demandé d'adapter votre code de sorte à ce que le code du dessin demeure dans la classe Animal mais que le choix de la texture se fasse de façon polymorphique.

Pour le scorpion, le nom du fichier de texture est paramétrable dans les fichiers .json sous l'étiquette "texture" de "scorpion" et est accessible via getAppConfig().scorpion_texture.

Pour la lézard, vous utiliserez getAppConfig().lizard_texture_female si c'est une femelle
et getAppConfig().lizard_texture_male sinon.

Affichage en mode "debugging"

Les fichiers de configuration .json contiennent 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.

Faites en sorte que le champ de vision et la cible virtuelle utilisées pour le déplacement aléatoires ne soient plus visibles que lorsque le mode «debugging» est activé (isDebugOn() retourne true).

Notez que dans les programmes de test fournis, il est possible d'activer/désactiver le mode «debugging» au moyen de la touche D.

Voilà, nous disposons, en principe, à ce stade d'une petite hiérarchie d'animaux concrets. Il nous reste à apporter les adaptations adéquates à l'environnement pour prendre en compte les changements de conception opérés jusqu'ici.

Retouches à la classe Environment

Jusqu'ici, la classe Environment recensait la liste des animaux présents et un ensemble de cibles, arbitrairement définies, à atteindre par ces animaux. Ce second attribut n'a plus vraiment raison d'être puisque les cibles seront à trouver parmi les entités organiques de l'environnement.

Pour adapter l'environnement à nos récents développements, une façon de procéder consiste à :

  1. transformer la liste des animaux en une liste d'entités organiques;
  2. remplacer la méthode addAnimal par une méthode addEntity (les corps sont quasiment identiques);
  3. faire en sorte que les méthodes draw et update travaillent désormais sur les entités organiques sans faire aucun test de type (là aussi les adaptations devraient être mineures).
    Votre collection hétérogène d'entités organiques doit être traitée de façon polymorphique.
  4. et remplacer la méthode getTargetInSightForAnimal par une méthode getEntitiesInSightForAnimal.

[Question Q3.3] : coder le type de retour de getEntitiesInSightForAnimal de façon analogue à celui de getTargetInSightForAnimal n'est pas une bonne solution du point de vue de l'encapsulation. Expliquez pourquoi dans votre fichier REPONSES et proposer une alternative de codage assurant une meilleure encapsulation.

Retouche à la méthode clean

La méthode clean doit désormais vider le monde de toutes ses entités organiques (vous pourrez ainsi plus tard, repeupler l'environnement à votre guise!). Vous prendrez soin de gérer la mémoire de façon adéquate.

Test 10  : affichages polymorphique des animaux

Si vous avez correctement mis en place les modifications suggérées jusqu'ici vous devriez pouvoir tester l'affichage polymorphique des animaux de l'environnement.

Le test graphique Tests/GraphicalTests/PPSTest.[hpp][cpp] est fourni à cet effet. Il va servir de base à tous les tests graphiques de l'étape 3 («PPS» pour «PredatorPreySimulation»). Pour l'exécuter, décommentez la cible ppsTest dans votre fichier CMakeLists.txt (comme d'habitude, 4 lignes sont à décommenter).

Ouvrez le fichier Tests/GraphicalTests/PPSTest.cpp et examinez la méthode onEvent. Vous constaterez qu'elle est programmée de sorte que l'appui sur la touche 'S' ajoute un scorpion dans votre collections hétérogène d'entités organiques, et la touche 'L' y ajoute un lézard (la touche 'R' qui provoque l'invocation de la redoutable clean pourra les faire disparaître). N'oubliez pas de décommenter les appels à addEntity dans ce fichier lorsque vos classes Scorpion et Lizard sont prêtes à être testées.

Vous pouvez commenter momentanément l'inclusion du fichier Cactus.hpp qui sera introduit un peu plus tard.
Nous fournissons dans le dossier res/ plusieurs fichiers de configuration dont app3.json, qui est le fichier par défaut pour l'étape 3 du projet. Une copie de ces fichiers, appX.orig.json est aussi fournie permettant de restaurer les valeurs initialement fournies si vous le souhaitez.

Lancez le test fourni. vous devriez voir s'afficher un scorpion (idem pour les lézards avec la touche 'L') (notez que vous pouvez zoomer/dezoomer avrec la molette de la souris)  :

Affichage du Scorpion
Affichage de la lézard
Exemple d'affichage avec la touche 'S'
Exemple d'affichage avec la touche 'L'

Activez ensuite le mode «debugging» (en appuyant sur la touche 'D', puis en appuyant sur la touche 'C' dans la fenêtre. Vous devriez voir s'afficher des caractéristiques distinctes pour le champ de vision (entre la lézard et le scorpion).

Vérifiez que la touche 'R' vide bien l'environnement de tout contenu.

Changez différentes valeurs (paramètre du champs de vision par exemple) dans votre fichier app3.json et vérifiez que cela se répercute proprement dans la simulation sans que vous ayez besoin de recompiler le code (ceci est valable pour tous les paramètres sauf ceux dimensionnant le monde et la fenêtre graphique).

Animation des lézards (bonus)

si vous souhaitez que les lézards se dessinent d'une façon moins figée, une façon simple de procéDer est de faire en sorte que leur texture s'alternent aléatoirement entre deux images possibles. Vous disposez de textures additionnelles pour gérer cet aspect (getAppConfig().lizard_texture_male_down et getAppConfig().lizard_texture_female_down).

Cactus

Sur la base de ce que vous avez fait jusqu'ici, ajoutez à votre conception une classe Cactus (dans le répertoire Environment/) matérialisant les cactus dont nos lézards pourront se nourrir.

Vous considérerez qu'un objet de type Cactus est-un OrganicEntity (mais pas un animal !). Le niveau d'énergie est assimilié à la "valeur nutritive" qu'il apporte.

La position initiale d'une Cactus sera transmise au constructeur et vous pourrez utiliser la valeur de getAppConfig().food_energy pour donner un niveau d'énergie initial de départ. Pour un cactus, la taille et le niveau d'énergies sont identiques.

Pour le dessin, vous procéderez de façon analogue aux animaux en utilisant getAppConfig().food_texture en guise de texture.

Vous pouvez considérer à ce stade qu'un Cactus évolue ... en ne faisant rien (mais il peut être amené à croitre/vieillir/se déseccher dans le futur). Vous pouvez ajouter un commentaire dans sa méthode update

    // TODO : Evolution du cactus à programmer
    

Test 11  : affichage polymorphique (Cactus)

Pour tester votre codage, utilisez à nouveau le test PPSTest.cpp.

Vous prendrez soin au préalable d'ouvrir le fichier Tests/GraphicalTests/PPSTest.cpp et d'y décommenter l'ajout de nourriture au moyen de la touche 'G' (pour "Grove", la touche 'C' étant prise :-/).

Voici l'affichage que vous devriez obtenir en lançant le programme et en appuyant sur la touche 'G' :

Affichage des cactus

Vérifiez que la touche 'R' fait aussi disparaître correctement les Cactus.

Si vous essayez de créer des animaux et des cactus dans l'environnement, vous pourrez constater que les animaux se dessinent parfois sous les cactus. Ceci vient du fait que les objets sont dessinés dans leur ordre d'insertion dans la collection des entités simulées. Nous allons y remédier plus tard. Pour le moment, il faut donc créer les cactus avant les animaux pour ne pas subir ce désagrément.

Météo

Il s'agit maintenant de simuler le fait qu'en fonction de la température, des nuages puissent apparaître et causer des «chutes de pluie» qui auront une incidence sur la croissance des cactus.
À noter que les parties suivantes du projet peuvent être abordées avant que cet aspect ne soit complètement fonctionnel.

Génération automatique des nuages

Vous considérerez qu'un nuage est un Collider dont la position est la taille sont données à la construction. Il évolue au cours du temps selon la modalité suivante: si la température excède la valeur getAppConfig().cloud_evaporation_temperature, il perd getAppConfig().cloud_evaporation_rate de son rayon (rayon = rayon - taux d'évaporation * rayon). Lorsque son rayon devient inférieur à getAppConfig().cloud_evaporation_size il disparaît. Un nuage se dessine au moyen de la texture désignée par getAppConfig().cloud_texture.

Les nuages doivent apparaître aléatoirement dans l'environnement lorsque certaines conditions sont remplies (voir plus bas).

Pour permettre la génération aléatoire de nuages, complétez la classe CloudGenerator (dans le répertoire Environment/). Cette dernière est caractérisée par un compteur de type sf::Time, mesurant le temps écoulé depuis la précédente génération de nuage. Le constructeur par défaut initialisera ce temps à sf::Time::Zero.

Pour faire évoluer ce compteur au cours du temps, il faut que la classe CloudGenerator dispose d'une méthode update(sf::Time dt).

La méthode update va mettre en oeuvre l'algorithme suivant :

  1. augmenter le compteur de dt;
  2. si sa valeur dépasse le seuil sf::seconds(getAppConfig().cloud_generator_delta)
    1. le remettre à zéro;
    2. ajouter à l'environnement un nuage placé aléatoirement selon une loi de distribution normale de centre taille_environnement/2 et de variance taille_environnement/4 * taille_environnement/4. La taille du nuage sera une valeur aléatoire uniformément distribuée entre les bornes getAppConfig().cloud_min_size et getAppConfig().cloud_max_size.

Pour finir, ajoutez à votre classe Environment :

Condition d'apparition des nuages

Vous considérerez que l'environnement à des périodes de sécheresse de durée sf::seconds(getAppConfig().environment_drought_duration). Le générateur de nuages ne doit entrer en action que s'il a fait sec pendant une telle durée et que la température est inférieure au seuil getAppConfig().environment_rain_temperature.

Vous veillerez enfin à ce que le nombre de nuages ne puisse dépasser la valeur getAppConfig().environment_max_clouds. Enfin, la touche R doit désormais aussi supprimer les nuages.

Inspirez vous du compteur de temps implémenté pour la génération automatique des nuages, pour mettre en oeuvre celui relatif au temps de sécheresse.

Test 12 : génération automatique de nuages

Ouvrez le test PPSTest.cpp et décommentez l'appel à addGenerator dans la méthode onSimulationStart.Cette méthode, qui exécute les traitements nécessaires au démarrage de l'application, ajoute un générateur de nuages à la collection de générateurs de l'environnement.

Lancez à nouveau PPSTest vous devriez voir apparaître spontanément des nuages dans l'environnement lorsque la température seuil est atteinte, et les voir cesser d'être générés et s'évaporer quand la température remonte, comme dans la vidéo ci-dessous:

[Video : Génération automatique des nuages + évaporation à la chaleur]

Vous pouvez jouer sur les constantes getAppConfig().simulation_time_factor et sur getAppConfig().cloud_generator_delta pour accélérer ou décélérer la génération automatique de nuages et sur la constante getAppConfig().environment_drought_duration pour augmenter ou diminuer la durée des période de sécheresse. Vérifiez que les nuages cessent d'etre générés lorsque leur nombre maximal est atteint ou lorsque vous faites remonter la température.

Épisodes de pluie et impact sur les cactus

Il vous est maintenant demandé de modéliser l'occurence d'épisodes pluvieux selon l'algorithme suivant:
Utilisez la tournure:
      if (bernoulli(getAppConfig().environment_raining_probability)...)
    
pour tester la probabilité d'occurence d'un épisode pluvieux.
Attention : supprimer l'élément d'un ensemble parcouru au moyen d'une boucle «for auto» peut invalider l'itération («segmentation fault» en vue !).
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());
  
(revoir le cours du semester passé sur la STL) Faites attentions à inclure le fichier d'entête <algorithm> pour pouvoir utiliser la fonction remove.

Les nuages doivent désormais aussi s'évaporer pendant les épisodes pluvieux. Pour pouvoir repérer visuellement les épisodes pluvieux, vous ferez en sorte que pendant qu'ils ont lieu, la position des nuages s'incrémente de Vec2d(uniform(-1.0, 1.0),uniform(-1.0, 1.0)); ce qui permet de les faire trembloter pour mimer une activité orageuse.

Impact de la pluie sur les cactus

Pour terminer cette partie, il vous est demandé de modéliser le fait que lorsqu'un épisode pluvieux s'achève, le sol reste humide pendant une durée de getAppConfig().environment_humidity_duration. Pendant cette période d'humidité, le niveau d'énergie est multiplié par getAppConfig().food_growth_rate à chaque pas de simulation. Ce niveau ne peut toutefois dépasser la valeur getAppConfig().food_max_energy.

Test 13 : Pluies

Lancez à nouveau PPSTest. Placez quelques cactus et faites baisser la température. Vous devriez observer des phénomènes pluvieux cyclique qui ont un impact sur la croissance des cactus:

[Video : Épisodes pluvieux (en accéléré)]

Amélioration de la conception

Le répertoire fourni Interface/ fournit deux entêtes de classe abstraites Updatable et Drawable.

Pour plus de clarté dans la conception on peut faire hériter tout objet dessinable de Drawable et tout objet qui évolue au cours du temps de Updatable.

Nous allons le voir en détail un peu plus tard dans le semestre, mais en C++ il est possible qu'une classe hérite de plusieurs super-classes. Le principe quant à l'héritage lui-même reste fondamentalement le même. Pour spécifier un héritage multiple (ici une classe d'objets dessinable et évoluant au cours du temps), il suffit de lister les héritages en les séparant par des virgules :

class MaClasse : public Drawable, public Updatable
(la classe MaClasse hérite de Drawable et de Updatable). Il n'est bien sûr pas indispensable d'hériter systématiquement des deux.

[Question Q3.4] Comment proposez-vous d'utiliser les classes Updatable et Drawable dans votre conception (on peut considérer que toute entité organique évolue au cours du temps, par exemple les cactus évoluent en poussant). Répondez à ces questions, dans votre fichier REPONSES et adaptez votre code en conséquence.

Destructeurs virtuels

Vous avez appris récemment que dans une hiérarchie polymorphique il était conseillé de programmer les destructeurs comme vituels... pensez-y!

Vous disposez maintenant de l'essentiel de votre architecture ! Tous les modules suivants sont dédiés à mettre au point la méthode update des animaux pour qu'ils puissent à nouveau bouger un peu..

Retour à l'énoncé du projet (partie 3) Module suivant (partie 3.2)