Project: step 3.2
Is it edible?


Disclaimer: the automatically generated English translation is provided only for convenience and it may contain wording flaws. The original French document must be taken as reference!


Make sure our animals correctly identify what serves as their food: lizards will be interested in cacti and scorpions in lizards. Obviously, you shouldn't see cannibal lizards/scorpions or a cactus giving a hungry look at a scorpion (or else it's a "bug").

Who eats whom?

Since our organic entities will not all be equal when facing predation, it seems useful to program a method for them such as:

bool eatable(OrganicEntity const* other)
returning true if this can eat other.

Indeed, during its movements, an animal will encounter entities and will naturally have to ask itself the question: "is it edible?" (hence the need for the eatable method). But actually, why program this method at the level of organic entities rather than at the level of animals? Because, a priori, our program can later evolve in this direction: imagine that we add insects and carnivorous plants, for example!

We clearly cannot define the eatable method at the abstraction level of organic entities.

Let's now imagine how we could concretely redefine the eatable method for lizards, for example.

In the Lizard class, the most obvious idea would be to write:

bool Lizard::eatable(OrganicEntity const* other) {
  // if other is a scorpion or a lizard then return false
  // if other is a "Cactus", then return true
}

hmm... we are doing nasty type tests.

[Question Q3.5] Why is testing the type of objects at runtime potentially harmful? Answer this question in your REPONSES file.

It is possible to avoid them by using the double dispatch technique: we work around the fact that C++ methods are not polymorphic on arguments, but only on this, by using overloading.

The idea is as follows. We would define in OrganicEntity pure virtual methods such as:

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;

The method bool Lizard::eatable(OrganicEntity const* entity) would be written as:

return entity->eatableBy(this);
(the lizard can eat the organic entity if the organic entity can be eaten by it!! this twist allows us to use polymorphism and avoid type tests).

With of course, Lizard::eatableBy(Lizard const*) and Lizard::eatableBy(Cactus const*) always returning false and Lizard::eatableBy(Scorpion const*) always returning true.

We would proceed in a similar way for all subclasses of OrganicEntity.

Let's assume we have the following code:

vector entities({new Lizard(..), new Scorpion(..)});
cout << v[0]->eatable(v[1]) << endl;;

We want to test if the lizard (v[0]) can eat the scorpion (v[1]). This is the Lizard::eatable method that will be invoked and it will execute:

v[1]->eatableBy(this) // this being v[0]: is the scorpion eatable by the lizard?

If you have programmed it properly in the Scorpion class, it should return false.

We can thus test for any pair of organic entities if the first one can eat the second one, without performing a type test.
You will also ensure that the eatable method does not allow autophagy!
Also be careful with circular dependency situations: it will be necessary to predeclare the animal subclasses referenced in OrganicEntity.hpp

Test 14 : edible targets (double dispatch)

The test file Tests/UnitTests/EatableTest.cpp is provided to test these latest developments. This is a textual test (displaying text on the terminal). Open this file and examine the test scenarios that are planned. Uncomment the corresponding target, eatableTest in the file CMakeLists.txt and run it.

You should then get the following execution trace:

Utilisation de ./build/../res/app.json pour la configuration.
===============================================================================
Tous les tests ont réussi (21 assertions dans 3 cas de test)

Animal behavior depends on their state

The update method of Animal had been commented out until now. It's time to look at it again.

Let's recall that this method aims to update the position, direction and speed of an animal after the elapsed time step dt.

Start by uncommenting.

The algorithm implemented so far by update is as follows:

  1. search for environmental targets seen by the animal;
  2. choose one;
  3. if there is at least one, f = calculate_force_exerted_by_target;
  4. otherwise f = randomWalk()
  5. update the movement (position and velocity) taking into account f

In reality, the update method only takes into account two possible states of the animal:

  1. a state "target in sight" where it must head towards the target;
  2. and a state "wandering" (which is its default state in the absence of a target) where it roams randomly.

We can now anticipate that there will be many other possible states. For example, if the animal is in the state "I saw a predator", it will no longer be attracted to food at all but will run for its life!

The animal's behavior will therefore depend on its state. This is an element that needs to be modeled.

Start by modeling the fact that an Animal HAS-A state.

An enumerated type with values for example:
    {
        FOOD_IN_SIGHT, // food in sight
        FEEDING,       // feeding (at this point it stops moving)
        RUNNING_AWAY,  // running away
        MATE_IN_SIGHT, // mate in sight
        MATING,        // private life (encounter with a mate!)
        GIVING_BIRTH,  // giving birth
        WANDERING,     // wandering
    }
	 
seems perfectly suited for the type associated with the animal's state. For a quick reminder about enumerated types, review the material from the first video of week 8 from last semester's MOOC: https://www.coursera.org/learn/initiation-programmation-cpp/lecture/aqXqY/puissance-4-introduction.

The idea now is to reformulate the update method so that it takes into account the state of the animal. It should therefore take the following form:

  1. state update; the animal's state may indeed be conditioned by changes in the environment for example, if a scorpion has come closer, the lizard will need to switch to "flight" state;
  2. If the state is FOOD_IN_SIGHT then f = calculate_force_exerted_by_target;
  3. If the state equals WANDERING then f = randomWalk();
  4. if other state: nothing for now (f is a zero force) but we have serious intentions to change this soon;
  5. update the movement (position, direction, and speed) considering f.
The switch statement should remind you of something here!

You are now asked to rephrase update as we have just described it.

It is wise to start modularizing the update method. Typically, updating the state of the animal after the passage of the time step dt (step 1 of the algorithm) is a process in itself, and a method updateState(sf::Time dt) to handle it is certainly a good idea.

Method UpdateState

The role of this method is to determine, based on the environment, the state in which the animal will be (and therefore assign it this state). For example, if there is a food source in sight, the method UpdateState will put the animal in the FOOD_IN_SIGHT state.

For now, UpdateState can only take into account the existence of the states FOOD_IN_SIGHT and WANDERING. It must determine if our animal is in one or the other of these two states.

A possible algorithm for this method, at this stage, is therefore simply the following:

  1. find the entities in the environment seen by the animal (remember your recent edits to the Environment class);
  2. among this set of entities detect the closest edible entity;
  3. if there is one, store its coordinates as the target of the animal which will then take the state FOOD_IN_SIGHT;
  4. otherwise, the animal's state will be WANDERING;
Steps 1 and 2 of the algorithm allow the animal to "analyze" its surroundings to identify what might interest it. For now, this simply involves food sources, but this will certainly evolve. The animal may indeed later be interested in the presence of potential mates or predators to flee from. A method analyzeEnvironment is therefore already quite conceivable at this stage.

Method getMaxSpeed

Until now, the calculation of the forces governing the movement (attraction force of a target or randomwWalk()) took into account a unique maximum speed for the animals (provided by getStandardMaxSpeed).

We can now imagine that this maximum speed also depends on the state of the animal (the animal can go beyond its limits to avoid being eaten, for example).

Program a method getMaxSpeed returning the standard maximum speed:

Otherwise, by default, the standard maximum speed is returned by getMaxSpeed.

Ensure that calls to getStandardMaxSpeed are replaced by those to getMaxSpeed where necessary (calculation of forces managing movement).

Test 15: I don't eat just anything

To test your new developments, you will again use the ppsTest target of the provided compilation script.

To test your update method, simply create relevant configurations (for example, a lizard in a scorpion's field of vision):


the scorpion is interested neither in the scorpion nor in the cactus
(restart the video if needed)

the scorpion is interested in the nearest lizard
(restart the video if needed)
[Video: predation (scorpion + congener + cactus)]
[Video: predation (scorpion + lizard)]

Note that at this stage, the lizard does not get eaten nor the cactus get bitten (these aspects will be coded in the next step)

You will create similar situations for lizards and verify in the process that only food within the field of vision exerts an attractive force. Note that in the example above, the fields of vision were modified in the configuration file (to have a very vindictive scorpion ;-)). Feel free to test different parameter values in your simulation.

Test 16: Displays in debugging mode (status, type, and energy level)

In debugging mode, it will be interesting to visualize the state of the animal, the encompassing circle that will be used for testing the animal's collision with the rest of the entities, its species, and its energy level.

Complete your drawing methods to allow these additions.

As a color for the encompassing circle, you can use:

      auto color(sf::Color(20,150,20,30));
      

To create textual displays, you will use the utility function provided buildText (defined in Utility/Utility.[hpp][cpp]). Here is an example of its use:


auto text = buildText(a_text,
                      convertToGlobalCoord(local_reference_position),
                      getAppFont(),
                      getAppConfig().default_debug_text_size,
                      text_color,
                      rotation_in_radians / DEG_TO_RAD + 90
                     ); // if needed
target.draw(text);

You will notice that it is often simpler to specify a position in the animal's local frame and then convert it to global coordinates. un_texte is the text to display (a string) and couleur_du_texte is a sf::Color (for example sf::Color::Yellow, sf::Color::Blue or sf::Color::Magenta) which can be chosen differently depending on the state. The default color is configurable and accessible via getAppConfig().debug_text_color.

To convert a double to a string, you can use the provided utility function to_nice_string (defined in Utility/Utility.[hpp][cpp]).

Your displays should then, in debugging mode, look like this:

Affichages en mode debug

[Question Q3.6] At what level of the hierarchy is it interesting to place the display of debugging information? Does the fact that a Collider is now drawable reconsider your Drawable inheritances? Answer these questions, justifying your choices, in your REPONSES file and adapt your code accordingly.

Your drawing methods are now doing a lot of things. Consider modularizing them!

Back to the project statement (part 3) Next module (part 3.3)