Projet : étape 3.2
Est-ce mangeable ?

Faire en sorte que nos animaux identifient correctement ce qui leur sert de nourriture : les lézards s'intéresseront aux cactus et les scorpions aux lézards. Évidemment, vous ne devriez pas voir de lézards/scorpions cannibales ou un cactus jeter un oeil gourmand sur un scorpion (ou alors c'est un « bug »).

Qui mange qui ?

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!

On ne sait clairement pas définir la méthode eatable au niveau d'abstraction des entités organiques.

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.

On procéderait de façon analogue pour toutes les sous-classes de OrganicEntity.

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.

On peut ainsi tester pour n'importe quelle paire d'entités organiques si la première peut manger la seconde, sans faire de test de type.
Vous veillerez aussi à ce que la méthode eatable ne permette pas l'autophagie !
Attention aussi aux situations de dépendances circulaires : il va être nécessaire de predéclarer les sous-classes d'animaux référencées dans OrganicEntity.hpp

Test 14  : cibles à manger (double dispatch)

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)

Le comportement des animaux dépend de leur état

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 :

  1. chercher les cibles de l'environnement vues par l'animal;
  2. en choisir une;
  3. s'il y en a au moins une, f = calculer_force_exercee_par_cible;
  4. sinon f = randomWalk()
  5. mettre à jour le déplacement (position et vitesse) compte tenu de f

En réalité la méthode update ne tient compte jusqu'ici que de deux états possible de l'animal :

  1. un état « cible en vue » où il doit se diriger vers la cible;
  2. et un état « déambulation » (qui est son état par défaut en l'absence de cible) où il se promène au hasard.

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!

Le comportement de l'animal va donc dépendre de son état. C'est un élément qu'il faut modéliser.

Commencez par modéliser le faite que un Animal A-UN état.

Un type énuméré avec comme valeurs par exemple :
    {
        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 :

  1. mise à jour de l'état; l'état de l'animal pourra en effet être conditionné par des changements de l'environnement  par exemple, si un scorpion s'est rapproché, le lézard devra passer en état «fuite»;
  2. Si l'état vaut FOOD_IN_SIGHT alors f = calculer_force_exercee_par_cible;
  3. Si l'état vaut WANDERING alors f = randomWalk();
  4. si autre état: rien pour le moment (f est une force nulle) mais nous sommes animés de sérieuses intentions pour que ça change bientôt;
  5. mettre à jour le déplacement (position, direction et vitesse) compte tenu de f.
L'instruction switch devrait se rappeler à votre bon souvenir ici !

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.

Méthode UpdateState

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 :

  1. trouver les entités de l'environnement vues par l'animal (rappelez-vous de vos récentes retouches à la classe Environment);
  2. parmi cet ensemble d'entités détecter l'entité mangeable la plus proche;
  3. s'il y en a une, stocker ses coordonnées comme cible de l'animal qui prendra alors l'état FOOD_IN_SIGHT;
  4. sinon, l'état de l'animal sera WANDERING;
Les étapes 1 et 2 de l'algorithme permettent à l'animal d'«analyser» ce qui l'entoure pour repérer ce qui peut l'intéresser. Pour le moment, il s'agit simplement de sources de nourriture, mais cela va certainement évoluer. L'animal pourra en effet par la suite être intéressé par la présence d'un conjoint potentiels ou de prédateurs à fuir. Une méthode analyzeEnvironment est donc tout à fait déjà concevable à ce stade.

Méthode getMaxSpeed

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.

Faites en sorte que les appels à getStandardMaxSpeed soient remplacés par ceux à getMaxSpeed là où cela est nécessaire (calcul des forces gérant le déplacement).

Test 15  : je ne mange pas n'importe quoi

Pour tester vos nouveaux développements, vous utiliserez à nouveau la cible ppsTest. du script de compilation fourni.

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) :

le scorpion n'est intéressé ni par le scorpion ni par le cactus
(redémarrez la vidéo au besoin)

le scorpion est intéressé par le lézard la plus proche
(redémarrez la vidéo au besoin)
[Video : prédation (scorpion + congénère + cactus)]
[Video : prédation (scorpion + lézard)]

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)

Vous créerez des situations analogues pour les lézards et vérifierez au passage que seule la nourriture qui est dans le champ de vision exerce un pouvoir d'attraction. Notez que dans l'exemple ci-dessus, les champs de vision ont été modifiés dans le fichier de configuration (pour avoir un scorpion très vindicatif ;-)). N'hésitez pas à tester différentes valeurs pour les paramètres dans votre simulation.

Test 16 : Affichages en mode debbuging (état, genre et niveau d'énergie)

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

Pour convertir un double en string, vous pouvez utiliser la fonction utilitaire fournie to_nice_string (définie dans Utility/Utility.[hpp][cpp]).

Vos affichages devraient alors, en mode debugging, ressembler à ceci :

Affichages en mode debug

[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.

Vos méthodes de dessin font maintenant beaucoup de choses. Pensez à les modulariser!

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