Goals: The aim at this stage is:
Before we delve into coding this step, a brief detour on the question of how to represent the characteristic parameters of a simulation is necessary.
There can indeed be many of them. For example, what dimensions to give to the different graphic components, which textures to use to draw the animals, etc.
So far, we have proceeded in a very ad hoc manner: some hard-coded constants in the file Constants.hpp are used, for example, to configure the texture to associate with an animal or the characteristics of its field of vision.
The downside of proceeding this way is that every time you want to modify these values, you have to recompile and restart the program.
To work around this limitation, we will now take full advantage of the provided Config class. The latter contains the definition of constants useful for the program that it will read from a JSON configuration file (see below). The provided simulation kernel (Application) has an attribute of type Config and a method getAppConfig that allows retrieving values from the configuration file. To change a value during simulation without having to recompile the code, you just need to edit the used JSON file, save it, and then go to the simulation window and reload these changes using the 'C' key ('C' for "Configuration").
This will allow us to vary the executions as desired by simply modifying the configuration file, and thus easily experiment with many different situations without having to recompile and restart the program.
There are several standard formats for data modeling (XML, JSON, YAML etc), which are not only used in the context of programming.
For this project, we chose to use the JSON format to list the characteristic parameters of the simulation.
The project configuration files are the files with the extension .json in the folder res/.
The directory src/JSON contains all the functionalities necessary for retrieving data from such a file. These are the functionalities that the class Config uses to read data from .json files.
In the .json files, the parameters are labeled hierarchically using strings that describe them.
For example:
{
"simulation" : {
...
"animal" : {
...
},
"scorpion":{
"mass":0.7,
"size":100,
"longevity":80000,
"max speed":100,
...
"texture": "scorpion.png"
...
},
...
}
reads as: The provided utility class Config allows access to the value of a given parameter (by associating it with a publicly accessible constant). For example, the constant scorpion_texture will allow access to the value of the parameter labeled ["simulation"]["animal"]["scorpion"]["texture"] in the .json file.
As mentioned above, the Application class has an attribute of type Config, which it initializes using the appropriate .json file, and this is how you can access the parameters using an expression like this:
getAppConfig().scorpion_texture;
Each test you run, whether graphical or not, can be configured using the desired configuration file, for example, via the command line:
./ppsTest app.jsonwill launch the target ppsTest using app.json as the configuration file.
The provided simulation core ensures that certain essential elements can be controlled by keys, typically:
If you open the provided file Application.hpp, you will notice (line 146) that an enumerated type models these controls. The method Application::handleEvent (line 486 of Application.cpp) is programmed so that the Q key allows switching from one control to another, and once a given control is selected, the PgUp and PgDn (or 9 et 8) keys allow varying the values of the controlled parameter. Here, if the controlled parameter is temperature, the PgUp/9 and PgDn/8 keys will respectively call your methods Environment::increaseTemperature and Environment::decreaseTemperature to vary the "temperature" control.
For the code to be compilable, these methods (and a few others) must be completed in your class Environment.
Start by modeling the fact that temperature becomes a characteristic element of the environment. Then equip your environment with public methods to initialize, consult, or modify this temperature:
Until now, an environment consisted of a set of animals and targets, none of the animals could be considered as a target for another.
At this stage, we will rather consider that the environment consists of a set of living beings/organic entities, some of which are "consumable" by others (and can therefore become targets).
In the Environment/ directory, create a new class OrganicEntity that your Animal class will inherit from: from now on, an animal is an organic entity of the environment. The same will apply to the cacti that our lizards will eat.
It must be possible to perform collision tests with any organic entity because encountering one on its path can affect the course of the simulation. Therefore, the inheritance link that has so far connected an Animal with a Collider will need to be moved one level higher. The hierarchy we want to achieve is as follows:

The class OrganicEntity will for now only have an attribute for its energy level (a double). We anticipate here the fact that every organic entity is mortal (it dies notably if it has a too low energy level).
Provide the OrganicEntity class with a constructor taking as parameters a position, a size and an energy level (in this order). The size parameter will be used to initialize the entity's radius as a Collider (the size provided as parameter will be considered as the double of the radius).
Then adapt your constructor of the Animal class to take these modifications into account. It will now take a position, size and energy level as parameters.
In the process, and since the animals will reproduce by the end of this stage, we can also equip the animals with an additional attribute: a boolean indicating whether it is a female or not. This attribute will also be initialized using a value passed as the last parameter of the constructor.
We are now ready to create specific animals: lizards (class Lizard) and scorpions (class Scorpion). For now, these classes have no special attributes. They will be coded in the Animal/ directory.
Our class Animal must, however, undergo some adjustments:
We will tackle the update method later. Comment out its body for now.
Getters, on the other hand, can find concrete definitions in the Lizard and Scorpion classes:
[Question Q3.1] Which methods have you decided to declare as pure virtual in the class Animal? In which places does the use of the keyword override seem relevant to you? Answer these questions, justifying your choices, in your file REPONSES.
You will provide the Lizard and Scorpion classes with two constructors each: one taking as parameters the animal's starting position (a Vec2d), its energy level and a boolean indicating whether it is a female or not. The other one taking only a position as parameter and initializing the energy level with a configurable default value (getAppConfig().lizard_energy_initial and getAppConfig().scorpion_energy_initial). The animal's sex will be randomly determined by this constructor.
[Question Q3.2]: what should be edited in the file .json if one wishes to modify, for example, the default initial energy level of the lizards during the simulation? Answer this question in your file REPONSES.
For the scorpion, the texture file name is configurable in the .json files under the "texture" tag of "scorpion" and is accessible via getAppConfig().scorpion_texture.
For the lizard, you will use getAppConfig().lizard_texture_female if it's a female
and getAppConfig().lizard_texture_male otherwise.
The configuration files .json contain a tag "debug"
{
"debug":true,
...
}
It is possible to use it to have differentiated displays depending on whether you are in "debugging" mode or not.The function isDebugOn(), defined in the files Application.[hpp][cpp], returns true if the value associated with the tag "debug" in the .json file used to launch the program is true and false otherwise.
Make the field of vision and virtual target used for random movement only visible when "debugging" mode is enabled (isDebugOn() returns true).
Here we are, in principle, at this stage with a small hierarchy of concrete animals. We still need to make the appropriate adaptations to the environment to account for the design changes made so far.
Until now, the class Environment listed the animals present and a set of targets, arbitrarily defined, to be reached by these animals. This second attribute no longer really has a reason to exist since the targets will be found among the organic entities of the environment.
To adapt the environment to our recent developments, one way to proceed is to:
[Question Q3.3]: coding the return type of getEntitiesInSightForAnimal similarly to getTargetInSightForAnimal is not a good solution from an encapsulation perspective. Explain why in your REPONSES file and propose an alternative coding solution that ensures better encapsulation.
The clean method must now clear the world of all organic entities (so that you can later repopulate the environment as you wish!). You will need to manage the memory appropriately.
If you have correctly implemented the suggested changes so far, you should be able to test the polymorphic display of the animals in the environment.
The graphical test Tests/GraphicalTests/PPSTest.[hpp][cpp] is provided for this purpose. It will serve as the basis for all graphical tests in step 3 ("PPS" for "PredatorPreySimulation"). To run it, uncomment the target ppsTest in your file CMakeLists.txt (as usual 4 lines to uncomment).
Open the file Tests/GraphicalTests/PPSTest.cpp and examine the method onEvent. You will notice that it is programmed so that pressing the 'S' key adds a scorpion to your heterogeneous collection of organic entities, and the 'L' key adds a lizard to it (the 'R' key, which triggers the invocation of the redoubtable clean, can make them disappear).
Do not forget to uncomment the calls to addEntity in this file once your Scorpion and Lizard classes are ready to be tested.Launch the provided test. You should see a scorpion appear (same for lizards with the 'L' key) (note that you can zoom in/out with the mouse wheel):
![]() |
![]() |
Then activate the "debugging" mode (by pressing the 'D' key, then pressing the 'C' key in the window. You should see distinct characteristics displayed for the field of vision (between the lizard and the scorpion).
Ensure that the 'R' key completely clears the environment of all content.
Change different values (field of view parameter for example) in your app3.json file and verify that it properly reflects in the simulation without needing to recompile the code (this applies to all parameters except those dimensioning the world and the graphic window).
Based on what you have done so far, add to your design a class Cactus (in the directory Environment/) representing the cacti that our lizards can feed on.
You will consider that an object of type Cactus is-a OrganicEntity (but not an animal!). The energy level is equated to the "nutritional value" it provides.
The initial position of a Cactus will be passed to the constructor, and you can use the value of getAppConfig().food_energy to provide an initial starting energy level. For a cactus, the size and energy level are identical.
For the drawing, you will proceed similarly to the animals by using getAppConfig().food_texture as a texture.
You can consider at this stage that a Cactus evolves ... by doing nothing (but it may grow/age/dry out in the future). You can add a comment in its update method.
// TODO: Program the evolution of the cactus
To test your coding, use the test PPSTest.cpp again.
You should first make sure to open the file Tests/GraphicalTests/PPSTest.cpp and uncomment the addition of food using the 'G' key (for "Grove", since the 'C' key is already taken :-/).
Here is the display you should get when running the program and pressing the 'G' key:

Ensure that the 'R' key also correctly makes the Cactus disappear.
You will consider that a cloud is a Collider whose position and size are given at construction. It evolves over time according to the following mode: if the temperature exceeds the value getAppConfig().cloud_evaporation_temperature, it loses getAppConfig().cloud_evaporation_rate of its radius (radius = radius - evaporation_rate * radius). When its radius becomes less than getAppConfig().cloud_evaporation_size it disappears. A cloud is drawn using the texture designated by getAppConfig().cloud_texture.
Clouds must appear randomly in the environment when certain conditions are met (see below).
To enable random cloud generation, complete the class CloudGenerator (in the directory Environment/). It is characterized by a counter of type sf::Time, measuring the time elapsed since the previous cloud generation. The default constructor will initialize this time to sf::Time::Zero.
To evolve this counter over time, the class CloudGenerator must have a method update(sf::Time dt).
The update method will implement the following algorithm:
Finally, add to your class Environment :
You will consider that the environment has drought periods of duration sf::seconds(getAppConfig().environment_drought_duration). The cloud generator should only activate if it has been dry for such duration and the temperature is below the threshold getAppConfig().environment_rain_temperature.
Finally, make sure that the number of clouds cannot exceed the value getAppConfig().environment_max_clouds. Also, the R key must now also remove the clouds.
Open the file PPSTest.cpp and uncomment the call to addGenerator in the method onSimulationStart. This methods, which runs the processes required to start the application, adds a cloud generator to the environment's collection of generators.
Launch PPSTest again, you should spontaneously see clouds appear in the environment when the threshold temperature is reached, and see them stop being generated and evaporate when the temperature rises, as in the video below:
| [Video: Automatic generation of clouds + evaporation with heat] |
You can play with the constants getAppConfig().simulation_time_factor and getAppConfig().cloud_generator_delta to speed up or slow down the automatic cloud generation and with the constant getAppConfig().environment_drought_duration to increase or decrease the duration of drought periods. Ensure that clouds stop being generated when their maximum number is reached or when you raise the temperature.
if (bernoulli(getAppConfig().environment_raining_probability)...)
to test the probability of occurrence of a rainy episode.pointerCollection.erase(std::remove(pointerCollection.begin(), pointerCollection.end(), nullptr), pointerCollection.end());(Refer to last semester’s course on the STL) Be sure to include the header file <algorithm> to use the remove function.
The clouds must now also evaporate during rainy episodes. To visually identify the rainy episodes, you will ensure that while they occur, the position of the clouds increments by Vec2d(uniform
To complete this part, you are asked to model the fact that when a rain episode ends, the ground remains wet for a duration of getAppConfig().environment_humidity_duration. During this humidity period, the energy level is multiplied by getAppConfig().food_growth_rate at each simulation step. However, this level cannot exceed the value getAppConfig().food_max_energy.
Launch PPSTest again. Place some cacti and lower the temperature. You should observe cyclical rainy phenomena that impact the growth of the cacti:
| [Video: Rainy episodes (time-lapse)] |
The provided directory Interface/ provides two abstract class headers Updatable and Drawable.
For greater clarity in design, any drawable object can inherit from Drawable and any object that evolves over time from Updatable.
We will see this in detail later in the semester, but in C++ it is possible for a class to inherit from multiple superclasses. The principle regarding inheritance itself remains fundamentally the same. To specify multiple inheritance (here a class of objects that are drawable and evolving over time), simply list the inheritances separated by commas:
class MyClass : public Drawable, public Updatable(the class MaClasse inherits from Drawable and from Updatable). Of course, it is not essential to systematically inherit from both.
[Question Q3.4] How do you propose to use the classes Updatable and Drawable in your design (one can consider that any organic entity evolves over time, for example, cacti evolve by growing). Answer these questions in your file REPONSES and adapt your code accordingly.
You recently learned that in a polymorphic hierarchy it is advisable to program destructors as virtual... think about it!