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!
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.
Let's assume we have the following code:
vectorentities({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.
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)
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:
In reality, the update method only takes into account two possible states of the animal:
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!
Start by modeling the fact that an Animal HAS-A state.
{
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:
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.
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:
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.
To test your update method, simply create relevant configurations (for example, a lizard in a scorpion's field of vision):
|
|
||
(restart the video if needed) |
(restart the video if needed) |
|
| |
|
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)
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.
Your displays should then, in debugging mode, look like this:

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