Project: step 3.1
Fauna, Flora and Weather


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!


Goals: The aim at this stage is:

  1. to ensure that the environment has fauna and flora;
  2. to introduce rainy episodes conditioning the evolution of the flora;
  3. and to review different elements of the design established so far.

Simulation parameters

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.

Configuration file format

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:
a scorpion ("scorpion") is an animal ("animal") in the simulation ("simulation") whose configurable parameters are the movement speed ("max speed"), the texture used for graphics ("texture"), etc.

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;
Reminder: expressions using getAppConfig() must not be used to initialize global constants.

Using configuration files

Each test you run, whether graphical or not, can be configured using the desired configuration file, for example, via the command line:

./ppsTest app.json
will launch the target ppsTest using app.json as the configuration file.
In QTCreator, you can pass arguments to your program (app.json in the example above) by following the instructions here: https://iccsv.epfl.ch/series-prog/install/installation-tutorial.php#commandLineArgs_in_qtcreator.
If no argument is provided, the .json file used will be the one whose name corresponds to the DEFAULT_CFG constant defined in Utility/Constants.hpp. For this step, this default value is app3.json but you are free to change it.

A brief word about "controls"

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:

Hierarchy of "organic entities"

We will now take advantage of inheritance to revisit the design of our program a little.

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:

Modele

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

At this stage, it is considered that only animals move according to the modalities programmed in the previous step (random walks). All attributes related to these movements therefore naturally remain in the Animal class.

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.

Lizards and scorpions

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:

  1. the methods getStandardMaxSpeed, getMass, getRandomWalkRadius, getRandomWalkDistance, getRandomWalkJitter, getViewRange and getViewDistance were drawing, in an ad hoc manner from the file Constants.hpp, values related to some animal. This doesn't really make sense: we don't really know how to define these methods at this level of abstraction;
  2. the method update no longer knows very well at the moment how to identify the targets of the environment.

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:

The size and initial energy level are also configurable parameters specific to each animal category: respectively, getAppConfig().lizard_energy_initial and getAppConfig().lizard_size for lizards and getAppConfig().scorpion_energy_initial and getAppConfig().scorpion_size for scorpions.

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

To generate a random boolean you can use the following expression:
uniform(0, 1) == 0
which gives you a fifty-fifty chance of having a female.
This constructor will typically be used to set up reproduction (an animal that is born will have a one in two chance of being female).

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

Polymorphic drawing

You were previously explained how to draw an animal using a texture. You are asked to adapt your code so that the drawing code remains in the Animal class but the choice of texture is done polymorphically.

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.

Display in "debugging" mode

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

Note that in the provided test programs, it is possible to enable/disable the "debugging" mode using the D key.

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.

Changes to the Environment class

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:

  1. transform the list of animals into a list of organic entities;
  2. replace the method addAnimal with a method addEntity (the bodies are almost identical);
  3. make the draw and update methods now work on organic entities without any type checking (here too, the adaptations should be minor).
    Your heterogeneous collection of organic entities must be handled polymorphically.
  4. and replace the method getTargetInSightForAnimal with a method getEntitiesInSightForAnimal.

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

Modification to the clean method

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.

Test 10: polymorphic displays of animals

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.
You can comment out momentarily the inclusion of the Cactus.hpp file, which will be introduced a little later.

We provide in the res/ folder several configuration files including app3.json, which is the default file for step 3 of the project. A copy of these files, appX.orig.json is also provided allowing you to restore the initially provided values if you wish.

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

Affichage du Scorpion
Affichage de la lézard
Display example with the 'S' key
Display example with the key 'L'

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

Lizard's animation (bonus)

A simple way of animating the lizards' graphical representation is to make their textures alternate randomly between two possible images. Additional textures are available to manage this aspect (getAppConfig().lizard_texture_male_down et getAppConfig().lizard_texture_female_down).

Cactus

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

Test 11: polymorphic display (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:

Affichage des cactus

Ensure that the 'R' key also correctly makes the Cactus disappear.

If you try to create animals and cacti in the environment, you may notice that animals sometimes appear under the cacti. This is because objects are drawn in the order they are inserted into the collection of simulated entities. We will address this later. For now, you should create the cacti before the animals to avoid this inconvenience.

Weather

It is now about simulating the fact that, depending on the temperature, clouds may appear and cause "rainfall" which will affect the growth of cacti.
Note that the next steps of the project can be tackled before this aspect is fully functional.

Automatic cloud generation

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:

  1. increment the dt counter;
  2. if its value exceeds the threshold sf::seconds(getAppConfig().cloud_generator_delta)
    1. reset it;
    2. add to the environment a cloud randomly placed according to a normal distribution law with a center environment_size/2 and a variance environment_size/4 * environment_size/4. The size of the cloud will be a random value uniformly distributed between the bounds getAppConfig().cloud_min_size and getAppConfig().cloud_max_size.

Finally, add to your class Environment :

Condition for cloud formation

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.

Use the implemented time counter for automatic cloud generation as a reference to implement the one related to drought time.

Test 12: automatic cloud generation

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.

Rain episodes and impact on cacti

You are now asked to model the occurrence of rainy episodes according to the following algorithm:
Use the instruction:
      if (bernoulli(getAppConfig().environment_raining_probability)...)
    
to test the probability of occurrence of a rainy episode.
Warning: Deleting an element from a collection while iterating over it using a "for auto" loop can invalidate the iteration (segmentation fault ahead!).
In a collection of pointers, pointerCollection, it is possible to remove all elements equal to nullptr using the following statement:
  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(-1.0, 1.0),uniform(-1.0, 1.0)); which allows them to tremble to mimic storm activity.

Impact of rain on cacti

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.

Test 13: Rains

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

Improvement of the design

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.

Virtual destructors

You recently learned that in a polymorphic hierarchy it is advisable to program destructors as virtual... think about it!

You now have the essentials of your architecture! All the following modules are dedicated to developing the update method of animals so they can move around again.

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