Comme nos entités organiques ne seront pas toutes égales face à la prédation, il semble utile de programmer pour ces dernières une méthode telle que:
bool eatable(OrganicEntity const* other)retournant true si this peut manger other.
En effet, lors de ses déplacements un animal va être amené à voir des entités et il devra naturellement se poser la question : "est-ce que ça se mange?" (d'où le besoin de la méthode eatable). Mais en fait, pourquoi programmer cette méthode au niveau des entités organiques plutôt qu'au niveau des animaux ? Parce qu'à priori notre programme peut plus tard évoluer dans ce sens: imaginez que l'on ajoute des insectes et des plantes carnivores par exemple!
Imaginons maintenant comment on pourrait redéfinir concrètement la méthode eatable pour des lézards par exemple.
Dans la classe Lizard, l'idée la plus évidente serait d'écrire:
bool Lizard::eatable(OrganicEntity const* other) { // si other est un scorpion ou un lézard alors retourner faux // si other est un "Cactus", alors retourner true }
hum... nous faisons là de vilains tests de type.
[Question Q3.5] Pourquoi tester le type des objets à l'exécution est-il potentiellement nocif ? Répondez à cette question dans votre fichier REPONSES.
Il est possible de les éviter en ayant recours à la technique du double dispatch : on contourne le fait que les méthodes en C++ ne soient pas polymorphique sur les arguments, mais uniquement sur this, en utilisant la surcharge.
L'idée est la suivante. On définirait dans OrganicEntity des méthodes virtuelles pures telles que :
virtual bool eatable(OrganicEntity const* entity) const = 0; virtual bool eatableBy(Scorpion const* scorpion) const = 0; virtual bool eatableBy(Lizard const* lizard) const = 0; virtual bool eatableBy(Cactus const* food) const = 0;
La méthode bool Lizard::eatable(OrganicEntity const* entity) s'écrirait comme :
return entity->eatableBy(this);(le lézard peut manger l'entité organique si l'entité organique peut se faire manger par elle!! cette pirouette nous permet d'utiliser le polymorphisme et d'éviter les tests de types).
Avec bien sûr, Lizard::eatableBy(Lizard const*) et Lizard::eatableBy(Cactus const*) retournant toujours faux et Lizard::eatableBy(Scorpion const*) retournant toujours vrai.
Supposons que l'on ait le code suivant :
vector<OrganicEntity*> entities({new Lizard(..), new Scorpion(..)}); cout << v[0]->eatable(v[1]) << endl;;
On veut tester si le lézard (v[0]) peut manger le scorpion (v[1]). C'est la méthode Lizard::eatable qui va être invoquée et cette dernière va exécuter :
v[1]->eatableBy(this) // this étant v[0] : le scorpion est-il mangeable par le lézard?
Si vous l'avez programmé proprement dans le classe Scorpion cela devrait retourner faux.
Le fichier de test Tests/UnitTests/EatableTest.cpp est fourni pour tester ces derniers développements. Il s'agit d'un test textuel (affichage de texte sur le terminal). Ouvrez ce fichier et examinez les scénarios de tests qui y sont prévus. Décommentez la cible correspondante, eatableTest dans le fichier CMakeLists.txtet lancez-la.
Vous devriez alors obtenir la trace d'exécution suivante :
Using ./build/../res/app.json for configuration. =============================================================================== All tests passed (21 assertions in 3 test cases)
La méthode update de Animal avait été mise en commentaire jusqu'ici. Il est temps de nous y intéresser à nouveau.
Rappelons que cette méthode a pour but de mettre à jour la position, direction et vitesse d'un animal après l'écoulement d'un pas de temps dt.
Commencez par la décommenter.
L'algorithme mis en place jusqu'ici par update est le suivant :
En réalité la méthode update ne tient compte jusqu'ici que de deux états possible de l'animal :
On peut maintenant anticiper qu'il y aura bien d'autres états possibles. Par exemple, si l'animal est dans l'état « j'ai vu un prédateur », il ne sera plus du tout attiré par la nourriture mais prendra ses jambes à son cou!
Commencez par modéliser le faite que un Animal A-UN état.
{ FOOD_IN_SIGHT, // nourriture en vue FEEDING, // en train de manger (là en principe il arrête de se déplacer) RUNNING_AWAY, // en fuite MATE_IN_SIGHT, // partenaire en vue MATING, // vie privée (rencontre avec un partenaire!) GIVING_BIRTH, // donne naissance WANDERING, // déambule }semble tout indiqué pour le type associé à l'état de l'animal. Pour un petit rappel sur les types énumérés, revoir le matériel de la première vidéo de la semaine 8 du MOOC du semestre passé : https://www.coursera.org/learn/initiation-programmation-cpp/lecture/aqXqY/puissance-4-introduction.
L'idée est maintenant de reformuler la méthode update de sorte à ce qu'elle tienne compte de l'état de l'animal. Elle devrait pour cela prendre l'allure suivante :
Il vous est demandé maintenant de reformuler update tel que nous venons de la décrire.
Il est judicieux de commencer à modulariser la méthode update. Typiquement, la mise à jour de l'état de l'animal après l'écoulement du pas de temps dt (pas 1 de l'algorithme) est un traitement à part entière et une méthode updateState(sf::Time dt) qui s'en chargerait est certainement une bonne idée.
Le rôle de cette méthode est de déterminer, en fonction de l'environnement, l'état dans lequel va se trouver l'animal (et donc de lui attribuer cet état). Par exemple, s'il y a une source de nourriture en vue, la méthode UpdateState va mettre l'animal dans l'état FOOD_IN_SIGHT.
Pour l'instant d'ailleurs, UpdateState ne peut tenir compte que de l'existence des états FOOD_IN_SIGHT et WANDERING. Elle doit déterminer si notre animal se trouve dans l'un ou l'autre de ces deux états.
Un algorithme possible pour cette méthode, à ce stade, est donc simplement le suivant :
Jusqu'ici le calcul des forces régissant le mouvement (force d'attraction d'une cible ou randomwWalk()) prenaient en compte une vitesse maximale unique pour les animaux (donnée par getStandardMaxSpeed).
On peut maintenant imaginer que cette vitesse maximale dépende aussi de l'état de l'animal (l'animal peut aller au delà de ses limites pour éviter d'être mangé par exemple).
Programmez une méthode getMaxSpeed retournant la vitesse maximale standard:
Autrement, par défaut, la vitesse maximale standard est retournée par getMaxSpeed.
Pour tester votre méthode update, créez simplement des configurations pertinentes (un lézard dans le champ de vision d'un scorpion par exemple) :
|
||
(redémarrez la vidéo au besoin) |
(redémarrez la vidéo au besoin) |
|
|
|
Notez qu'à ce stade, le lézard ne se fait pas dévorer ni le cactus croquer (ces aspects sercont codés à l'étape suivante)
En mode debugging, il sera intéressant de pouvoir visualiser l'état de l'animal, le cercle englobant qui servira au test de collision de l'animal avec le reste des entités, son genre et son niveau d'énergie.
Complétez vos méthodes de dessin de sorte à permettre ces ajouts.
En guise de couleur pour le cercle englobant, vous pouvez utiliser:
auto color(sf::Color(20,150,20,30));
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, convertToGlobalCoord(position_repere_local), getAppFont(), getAppConfig().default_debug_text_size, couleur_du_texte, une_rotation_en_radians / DEG_TO_RAD + 90 ); // si nécessaire target.draw(text);
Vous remarquerez qu'il est souvent plus simple de spécifier une position dans le repère local de l'animal puis de le convertir en coordoonnées globales. 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) qui peut être choisie de façon différenciée en fonction de l'état. La couleur par défaut est paramétrable et accessible via getAppConfig().debug_text_color
Vos affichages devraient alors, en mode debugging, ressembler à ceci :
[Question Q3.6] A quel niveau de la hiérarchie est-il intéressant de placer l'affichage des informations de debugging ? Le fait qu'un Collider soit maintenant dessinable rediscute t-il vos héritages de Drawable ? Répondez à ces questions, en justifiant vos choix, dans votre fichier REPONSES et adaptez votre code en conséquence.