Buts:
Mettre en place le lieu de vie des futurs hamsters
Préambule
Il s'agit lors de cette étape d'aboutir à un premier programme graphique nous permettant de visualiser le «laboratoire» simulé (classe Lab) et les différentes entités qui y évoluent au cours du temps (élevage de hamsters, sources de nourriture etc.).
Notre programme va désormais avoir pour composant essentiel une classe Application; le "noyau de simulation" capable de faire du graphisme et de gérer des événements, fourni dans src/Application.[hpp/cpp].
La classe Application joue le même rôle que la classe du même nom fournie dans le corrigé avancé du mini-projet graphique. Plus précisément, cette classe dispose :
d'un attribut de type Lab;
de méthodes permettant d'appeler en boucleune méthode de mise à jour (update) et une méthode de dessin (drawOn) sur cet attribut. La méthode de mise à jour sera en charge de faire évoluer au cours du temps les entités qui évoluent dans l'environnement et le méthode de dessin d'en faire un rendu graphique. Le paramètre target de cette dernière représente la fenêtre graphique sur laquelle se fait le dessin.
C'est par le biais de l'attribut de type Lab que ce fait lien entre le noyau de simulation fourni et ce que vous codez.
La classe Application dispose à ce propos d'une méthode handleEvent (ligne 520 de Application.cpp) qui permet à la simulation de réagir à des touches du clavier: par exemple la touche Esc permet de mettre fin à la simulation (fermeture de la fenêtre). Prenez note de l'existence de cette méthode pour la suite.
Paramètres de simulation
Comme évoqué lors de l'étape précédente, les paramètres conditionnant la simulation sont fournis dans des fichiers res/appX.json.
Chaque test que vous lancerez, graphique ou pas, pourra être paramétré par le fichier de configuration souhaité, par exemple, en ligne de commande :
./labTest app.json
permettra de lancer la cible labTest en utilisant app.json comme fichier de configuration.
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 app.json mais libre à vous de le changer.
La plupart des paramètres ont été anticipés, mais libre à vous d'en rajouter par la suite dans Config.[hpp][cpp] et les fichiers .json.
Pourquoi pas de simples constantes ?
L'inconvénient d'utiliser de simples constantes globales est qu'à chaque fois que l'on veut modifier leur valeur, il faut recompiler et relancer le programme. L'utilisation des fichiers .json permettra de changer des valeurs de paramètres pendant que la simulation se déroule ce qui facilitera certaines observations. Nous y reviendrons dès que le monde simulé sera «habité» :-)
Il s'agit maintenant de commencer à modéliser le lieu de vie de nos futurs hamsters. Une classe Lab (pour laboratoire) est un choix plutôt naturel ici. Nous partirons de l'hypothèse qu'un Lab contient forcément un certain nombre de cages (classe Cage) dans lesquelles seront élevés les hamsters. C'est à la mise en oeuvre de cette première classe que nous allons nous atteler dans un premier temps.
Vous coderez toutes les classes suggérées ci-dessous dans le répertoire src/Env.
La classe Cage
Losrque vous serez prêts à lancer votre première compilation, référez vous aux indications sous "Test 3" pour voir quelle cible décommenter.
Une cage sera caractérisée par :
la position de son centre (un Vec2d);
sa largeur;
sa hauteur;
quatre murs de même épaisseur;
l'épaisseur des murs.
Un mur sera caractérisé par une paire de Vec2d représentant respectivement la position de son coin inférieur droit, et celle de son coin supérieur gauche :
Vous définirez l'alias de type suivant :
typedef std::pair <Vec2d, Vec2d> Wall; //bottom right corner, top left corner
Pour simplifier les calculs, prenez des murs de mêmes tailles positionnés comme suit les uns par rapport aux autres :
Vous coderez la classe Cage dans les fichiers src/Env/Cage.[hpp][cpp] et définirez l'alias de type Wall dans Cage.hpp avant le prototype de la classe Cage.
Vous doterez la classe Cage des méthodes utilitaires suivantes :
Un constructeur prenant en paramètre la position du centre, la largeur et la hauteur de la cage ainsi que l'épaisseur des murs (dans cet ordre pour être compatible avec les tests fournis) et initialisant de façon appropriée les murs de la cage. Vous prévoirez des valeurs par défaut pour la largeur, la hauteur et l'épaisseur des murs (300 pour les deux premiers et 10 pour le dernier).
Les «getters» getCenter, getWidth, getHeight et getWallWidth retournant respectivement la position du centre la largeur et la hauteur (murs compris) de la cage et l'épaisseur des murs.
La méthode double getLeftLimit(bool intern) retournant la coordonnée en x de la face interne du mur de gauche si le booléen intern vaut true et de la face externe si le booléen vaut false. Par défaut la valeur du paramètre sera false.
La méthode double getRightLimit(bool intern) retournant la coordonnée en x de la face interne du mur de droite si le booléen intern vaut true et de la face externe si le booléen vaut false. Par défaut la valeur du paramètre sera false.
La méthode double getTopLimit(bool intern) retournant la coordonnée en y de la face interne du mur du haut si le booléen intern vaut true et de la face externe si le booléen vaut false. Par défaut la valeur du paramètre sera false.
La méthode double getBottomLimit(bool intern) retournant la coordonnée en y de la face interne du mur du bas si le booléen intern vaut true et de la face externe si le booléen vaut false. Par défaut la valeur du paramètre sera false.
La méthode bool isPositionInside(const Vec2d& position) retournant vrai si la position passée en paramètre est complètement à l'intérieur de la cage (un point sur le bord interne d'un mur ne sera pas considéré comme à l'intérieur);
la méthode bool isPositionOnWall(const Vec2d& position) retournant vrai si la position passée en paramètre est sur un des murs de la cage.
Rappelez-vous que dans SFML, l'axe des y pointe vers le bas.
Pour les méthodes isPositionInside et isPositionOnWall, vous prendrez soin d'utiliser les méthodes getTopLimit, getBottomLimit etc. afin d'éviter toute duplication dans les calculs de positions.
La classe Lab
Le laboratoire sera pour le moment simplement caractérisé par l'ensemble des cages qu'il contient. Sa largeur et sa hauteur valent toutes deux getAppConfig().simulation_lab_size. Vous couvrirez complètement sa surface de cages comme dans la figure ci-dessous :
Codez l'ensemble des cages comme un ensemble de Cage* si vous souhaitez être conforme aux tests fournis. Vous êtes libre de la structure de données à adopter mais il doit être à tout moment possible de connaître le nombre de cages par rangées (au travers d'un getter, voir plus bas).
Les méthodes suivantes sont à prévoir pour la classe Lab :
Une méthode maxCageNumber() donnant le nombre maximal de cages tolérées par ligne. Le calcul de ce nombre se fait comme suit : le nombre maximal de cages par ligne vaut la largeur du laboratoire divisé par la taille minimale d'une cage. Cette taille minimale est par défaut donnée dans le fichier de configuration (getAppConfig().simulation_lab_min_box_size). Si cette taille par défaut est trop petite (fichier de configuration spécifiant une préférence pas raisonnable), elle sera ramenée à la taille des hamsters multipliée par 2. Par simplification, cette taille est supposée identique pour tous et vaut getAppConfig().hamster_size. Enfin, le nombre maximal de cages ne peut être en dessous de zéro.
Une méthode makeBoxes(unsigned int nbCagesPerRow) permettant de quadriller le laboratoire avec un nombre donné de cages par ligne. Ce nombre doit être ramené au nombre maximal possible de cages s'il est trop grand. Si nbCagesPerRow vaut zéro, la méthode makeBoxes lancera une exception de type std::invalid_argument avec un message approprié. Vous pourrez prendre comme épaisseur de mur le 5% de la largeur d'une demi-cage (la largeur d'une cage étant donnée par la largeur du Lab divisée par le nombre de cages voulu).
Il s'agit dans cette méthode de calculer proprement le centre de chacune des
cages pour les construire et les ajouter à l'ensemble des cages du Lab)
Un constructeur par défaut qui fait appel à la méthode makeBoxes en lui passant getAppConfig().simulation_lab_nb_boxes en paramètre (nombre de cages par ligne initial correspondant à l'étiquette ["simulation"]["lab"]["initial nb boxes"] dans le fichier app.json ).
Un getter getNbCagesPerRow donnant le nombre de cages par ligne.
Une méthode addCageToRow permettant d'ajouter une cage par rangée. Ce nombre ne pourra pas dépasser le nombre maximal de cages toléré par ligne (dans ce cas la méthode ne fait rien).
Une méthode removeCageFromRow permettant de supprimer une cage par rangée. Le nombre de cages ne peut pas devenir inférieur à 1 (dans ce cas la méthode ne fait rien).
Une méthode void update(sf::Time dt) qui sera en charge de faire évoluer le contenu de chaque cage au cours du temps. Cette méthode dicte comment les cages, ou plutôt leur contenu, va évoluer après écoulement d'un pas de temps dt. Pour l'instant cette méthode ne fait rien (parce que nos hamsters et nos portions de granulés sont encore un peu dans les limbes...). Dans le corps de la méthode mettez simplement un commentaire tel que
// faire évoluer le contenu des cages au cours du temps :
pour vous rappeler de la tâche à coder plus tard.
Une méthode drawOn(sf::RenderTarget& targetWindow) qui sera destinée à dessiner le contenu du laboratoire. Pour le moment, cette méthode se contentera donc de dessiner chaque cage:
l'inclusion de <SFML/Graphics.hpp> est nécessaire pour l'accès aux fonctionnalités graphiques de la SFML;
Une méthode reset prenant en paramètre un booléen indiquant s'il s'agit d'un «reset» total (avec destruction puis reconstruction des cages) ou pas. La valeur par défaut de ce paramètre sera false, car on souhaite que par défaut on puisse vider les cages sans les détruire. Pour le moment, la méthode reset ne fait quelque chose que s'il s'agit d'un «reset» total (valeur du paramètre à true). Dans ce cas les cages sont détruites (ensemble de cages vidé), puis reconstruites comme elles l'était par le constructeur (nombre de cages par rangée valant getAppConfig().simulation_lab_nb_boxes). Si le paramètre vaut false, comme pour le moment les cages sont encores inhabitées, vous vous contenterez d'un commentaire dans le corps du "else":
// vider les cages de leur contenu :
Libre à vous par la suite d'ajouter toute autre méthode vous semblant utile.
[Question Q2.1] Quelle précaution faut il prendre lors de la destruction des cages pour éviter les fuites de mémoire ?
[Question Q2.2] On souhaite ne pas permettre la copie d'un Lab. Quelle(s) solution proposez-vous pour satisfaire cette contrainte? Pourquoi le respect de cette contrainte est-il souhaitable ?
Réfléchissez à ces questions et répondez-y dans votre fichier REPONSES. Apportez les modifications nécessaires à votre code.
Dessin d'une cage
Revenons donc maintenant à la méthode Lab::drawOn. Cette dernière doit maintenant dessiner chacune des cages, ce qui présuppose qu'une cage doit être dessinable (disposer d'une méthode de dessin spécifique).
Notez que vous disposez d'une méthode utilitaire buildRectange dans Utility.hpp permettant de dessiner un rectangle SFML texturé.
En guise de texture vous pourrez prendre la texture configurable dans le fichier app.json (sous l'étiquette ["lab"]["fence"]) et qui est accessible par la tournure :
[Question Q2.3] Dans quelles classes les lignes de code relative au dessin d'une cage doivent elle apparaître selon vous ?
Justifiez vos choix dans votre
fichier REPONSES.
Test 3 : Fonctionnalités de Cage
Pour tester le travail fait jusqu'ici, vous utiliserez pour commencer un test non graphique. Ce test fourni dans src/Tests/UnitTests/CageTest.cpp.
Le fichier CMakeLists.txt fourni permet de lancer la compilation du test par le biais de la cible cageTest.
N'oubliez pas de décommenter cette cible dans le fichier CMakeLists.txt (lignes 94, 95 et les lignes 136, 137) et d'exécuter Build >Run Cmake lorsque vous êtes prêt à compiler/tester.
Une exécution correcte de votre programme devrait produire la sortie suivante :
===============================================================================
All tests passed (50 assertions in 4 test cases)
Test 4 : Un laboratoire plein de cages
Pour tester le travail fait jusqu'ici, vous utiliserez le test fourni dans src/Tests/GraphicalTests/LabTest.cpp. La classe LabTest hérite de Application. Elle a de ce fait comme attribut un objet de type Lab sur lequel seront invoquées en boucles les méthodes de dessin et de mise à jour que vous avez ébauchée. Les touches R et Shift-R du clavier (gérée par Application::handleEvent) se traduit en fait par un appel à la méthode Lab::reset que vous venez de programmer.
La classe LabTest définit aussi dans sa méthode onEvent des contrôles plus spécifiques pour ajouter un hamster et des granulés dont il pourra plus tard se nourrir. :
Les appels aux méthodes à venir sont commentés pour le moment.
Le fichier CMakeLists.txt fourni permet de lancer la compilation du test par le biais de la cible labTest.
N'oubliez pas de décommenter cette cible dans le fichier CMakeLists.txt (lignes 100, 101 et lignes les lignes 142, 143) et d'exécuter Build >Run Cmake lorsque vous êtes prêt à tester.
Vous devriez voir s'afficher ceci pour commencer :
car le fichier app.json est configuré de sorte à avoir un laboratoire ne contenant qu'une seule cage. Le nombre de cages par rangée devrait d'ailleurs s'afficher et valoir la valeur par défaut (1) (voir ce qui indiqué par la flèche rouge).
Contrôle interactif du nombre de cages
Le noyau de simulation fourni prévoit aussi que certains éléments essentiels soient contrôlables par des touches, typiquement:
le nombre de cages par rangée;
le type de statistiques que l'on souhaite voir s'afficher (ce qui viendra plus tard).
Si vous ouvrez le fichier fourni Application.hpp, vous constaterez (ligne 160) qu'un type énuméré modélise ces contrôles. La méthode Application::handleEvent (ligne 520 de Application.cpp) est programmée de sorte à ce que la touche Tab permette de basculer d'un contrôle à l'autre et une fois un contrôle donné choisi, les touches 8 et 9 permettent de faire varier les valeurs du paramètre contrôlé. Ici, si le paramètre contrôlé est le nombre de cages par rangées, les touches 8 et 9 appeleront respectivement vos méthodes Lab::removeCageFromRow et Lab::addCageToRow pour faire varier le contrôle «CAGE» (nombre de cages par lignes).
Vérifiez que:
le recours à la touche Tab vous permet de sélectionner le contrôle "nombre de cages" comme le contrôle actif (celui qui s'affiche en rouge);
que les touches 8 et 9 vous permetttent d'augmenter et diminuer le nombre de cages par rangée (sans incidence sur quoique ce soit pour le moment!) et que les critères plafonnant le nombre de cages par rangées sont bien respectés.
que la touche R ne modifie pas le nombre de cages
que la touche Shift-R permet au nombre de cages de retrouver sa valeur par défaut (telle que donnée dans le fichier app.json)
← Ici le fichier app.json de base est utilisé, les touches 8 et 9 sont chacune appuyées 5 fois (la dernière fois est inopérante car les critères sur le nombre de cages ne sont alors plus respectés)
Vous pouvez aussi jouer avec les valeurs du fichier app.json. Vérifiez alors que:
si la taille minimale de cage souhaitée (prévue à la base à 300 pour une taille de labo à 1800) est trop grande (par exemple > 1000), le nombre de cages par rangée sera plafonné à 1 (on installe la plus grande cage possible sans arriver à satisfaire les desiderata du fichier de configuration);
si le nombre par défaut de cages est trop grand (ne permettant pas d'assurer un espace minimal pour les hamsters), il sera plafonné de sorte à avoir les plus petites cages licites;
si l'on augmente exagérément la taille des futurs hamsters et qu'il n'est alors plus possible de construire une cage compatible avec leur taille, une exception est lancée par le programme.
Notez qu'il est parfois pratique de pouvoir retrouver les valeurs des paramètres tels qu'initialement fournis dans l'archive. Pour vous en assurer vous pouvez en remplacer le contenu de appX.json par celui de même nom fourni dans res/orig/ (ce répertoire contient une sauvegarde de l'état des fichiers de configuration tels que fournis au départ).