Goal: Implementation of a base class representing circular bodies that can move in a torus world.
Necessary Concepts: object classes, constructors/destructors, operator overloading.
These concepts are explained in courses 15 to 18. These exercises in series 19 contain material close to what you need to program.Useful files: partie1.zip
The instructions below assume that you have created a specific folder for the project (we will assume here that it is a cpp/project directory). Adapt the commands to the location of your project.
Copy the provided archive partie1.zip into your cpp/project directory and unzip it:
===================================================================
All tests passed (60 assertions in 9 test cases)
The meaning of this execution will be explained below.
The provided archive contains:
To increase the modularity of your project, method prototypes and attribute declarations should be in a file with the .hpp extension, while their definitions should be in a file with the .cpp extension.
Regarding compilation, some useful indications:How to avoid multiple inclusions:
#pragma once
Naming convention for files:
This part of the project consists of only one module (component).
The goal here is to apply your initial knowledge of object-oriented programming by implementing a utility class representing circular bodies that can move in a two-dimensional toric space.
The idea is that the program's entities (scorpions, lizards, lizard food, etc.) will evolve in a two-dimensional space.
These entities will "meet" each other, and it will be necessary to test this fact simply: for example, if a scorpion meets a lizard, it will be able to feed on it. It is therefore important to be able to test when a meeting occurs.
Testing the collision/meeting of two objects can be very complex depending on their actual shapes. In this project, we will use a simplification by approximating the shape of each entity with a circular body. Collision tests will then be performed simply based on these bodies, as illustrated in the image below:
|
← The collision test between a lizard and a scorpion is simplified by approximating each of these entities with a circular body (visible in light green). |
Additionally, the two-dimensional space in which the program's entities evolve should be interpreted as a toric environment: meaning that an entity exceeding the environment's boundaries should reappear on the opposite side (as shown in the short video below):
| [Video: movement in a toric world] |
The Collider class that you are required to program will represent the circular bodies between which collision tests will be possible, and which can move in a toric environment.
An object of type Collider is characterized by:
Your Collider class should include the following members:
A constructor initializing the position and radius of the circular object using a Vec2d and a double passed as parameters (in that order). If the radius is negative, throw an exception without catching it (this is a fatal error that should cause the program to terminate).
The constructor must ensure that the provided position values are correctly interpreted in the toric world: once assigned to this, the position must be adjusted using the following "clamping" algorithm:
double worldSize = getAppConfig().simulation_world_size; auto width = worldSize; // width auto height = worldSize; // heightTo use this, include <Application.hpp> in Collider.cpp (but not in .hpp to avoid circular class dependencies).
[Question Q1.1] The "clamping" algorithm for the position will need to be applied in other contexts beyond construction. How can it be implemented so that its reuse in another method of the Collider class does not lead to code duplication? Think about this and answer in your REPONSES file (adapt your code accordingly).
Getters (getPosition and getRadius) for position and radius. The getter for position should return a constant reference to a Vec2d.
A copy constructor (its default definition is fine, but it is better to make it explicit).
The copy assignment operator = (same remark as for the copy constructor).
[Question Q1.2] If the default definitions of copy constructors and the (re)copy operator are sufficient for our needs, why is it still preferable to explicitly code them? Think about it and answer this question in your REPONSES file.
Next, add to your Collider class the methods that allow it to be modeled as an object capable of moving in a toric world:
A method directionTo that takes a Vec2d argument, to. Suppose that from is the position of the current instance. The directionTo method should compute and return the Vec2d between from and to in the toric world:
To determine which vector corresponds to the two endpoints in the toric world, multiple options are possible. The applied algorithm will select the candidate position for to that is the shortest distance from from.
There are nine possible candidates for to in the toric world:
where width is the width of the world and height its height.
[Question Q1.3] How can loops be used to avoid overly repetitive code in the implementation of this algorithm? Think about it and answer this question in your REPONSES file (adapt your code accordingly).
A method directionTo that performs the same task as the previous method but takes a Collider as an argument. In this case, the role of to is played by the position of the provided Collider. Be careful not to duplicate code.
A method distanceTo that takes a Vec2d argument, to, and returns the length of the toric vector computed by directionTo(to).
An overload of the previous method that takes a Collider as a parameter. Here, the role of to is played by the position of the provided Collider.
A method move that adds a given Vec2d, dx, to the current instance's position (see the + operator for Vec2d). The resulting position must undergo a "clamping" algorithm to ensure it remains within the toric world.
The same operation implemented using the += operator.
[Question Q1.4] Which method arguments should be passed by constant reference? Answer in your REPONSES file.
[Question Q1.5] Which methods among those you were asked to implement above would be appropriate to declare as const? Answer this question in your REPONSES file.
Finally, add the following methods to your Collider class to enable it to handle collisions with other objects of the same type:
A method isColliderInside that takes a Collider, other, as an argument and returns true if other is inside the current instance, and false otherwise. Given two Collider objects, a and b, a is inside b if:
A method isColliding that takes a Collider, other, as an argument and returns true if other is colliding with the current instance, and false otherwise. Given two Collider objects, a and b, a is colliding with b if the distance between their centers is less than or equal to the sum of their radii.
A method isPointInside that takes a point (of type Vec2d) as an argument and returns true if the point is inside the current instance, and false otherwise. A point p is inside a Collider body if the distance between p and body is less than or equal to the radius of body.
The > operator, usable as follows:
body1 > body2
Returns true if body2 is inside body1, and false otherwise. The objects body1 and body2 are of type Collider.
The | operator, usable as follows:
body1 | body2
Returns true if body2 is colliding with body1, and false otherwise. The objects body1 and body2 are of type Collider.
The > operator, usable as follows:
body > point
Returns true if point is inside body, and false otherwise. The object body is of type Collider, and point is of type Vec2d.
[Question Q1.6] How can the three previously described operators be implemented in Collider.cpp while avoiding code duplication? Think about it and answer this question in your REPONSES file.
Collider: position = <position> radius = <radius>
where <position> represents the position of the Collider and <radius> represents its radius.
[Question Q1.7] Should these operators be overloaded as member functions or external functions? Justify your answer in REPONSES.
[Question Q1.8] Which method arguments, among those you were asked to code above, do you think should be passed by constant reference? Answer this question in your REPONSES file.
[Question Q1.9] Which methods, among those you were asked to code above, do you think should be declared as const? Answer this question in your REPONSES file.
To test this part of the project, you have:
The colliderTest target defined in the partie1/src/CMakeLists.txt file, which references multiple files.
This target allows you to run the test program src/Tests/UnitTests/ColliderTest.cpp. This program calls the various functionalities you have implemented in the Collider class to verify their correctness.
Take a quick look at the CMakeLists.txt file to see how this target is defined:
# add_executable (colliderTest ${TEST_DIR}/UnitTests/ColliderTest.cpp ${SOURCES} ${CACHE_SOURCES})
# target_link_libraries(...)
===============================================================================
All tests passed (60 assertions in 9 test cases)
(which confirms that the provided Vec2d class successfully passes a number of tests).
To understand how these tests work, open and examine the src/tests/UnitTests/ColliderTest.cpp file.
This test program uses a specific framework called Catch, which allows for what is known in programming as unit testing. This involves writing test scenarios to validate different coded functionalities.
Below, we explain how these tests function.
The provided test program in ColliderTest.cpp defines test scenarios ("test cases") to verify the functionalities of the Collider class by calling the methods you were asked to implement.
Each scenario contains a number of messages (labeled with the keywords GIVEN, THEN, or AND_THEN) that describe the conditions of the test execution and their results. These messages will only be displayed if a test fails.
If all tests in a scenario pass successfully, you will see a message like this:
=============================================================================== All tests passed
The tests verify a number of assertions using functions such as CHECK, CHECK_FALSE, and CHECK_APPROX_EQUAL.
The provided test includes only two scenarios (from line 24 to line 159 and from line 161 to the end), but in general, there could be more. Let's examine part of the first scenario.
Lines 28 and 29 create two identical Collider objects at position {1,1} with a radius of 2. These two objects are expected to collide, as indicated by the THEN statement on line 31. To verify that your code functions correctly in this case, the test checks four assertions:
As you might have guessed, a CHECK succeeds if the assertion it checks returns true. Similarly, a CHECK_FALSE (for example, on line 57) succeeds if the condition it evaluates returns false.
Suppose your isColliding method is incorrectly implemented. Running the test program ColliderTest.cpp will display:
-------------------------------------------------------------------------------
Scenario: Collision/IsColliderInside with Collide
Given: Two identical bodies
Then: they collide
-------------------------------------------------------------------------------
...
src/Tests/UnitTests/ColliderTest.cpp:35: FAILED:
CHECK( o1.isColliding(o2) )
with expansion:
false
...............................................................................
The test output displays the scenario name, the messages associated with GIVEN and THEN to indicate the test conditions (two identical Collider objects) and what should be true in this case ("...they collide"). It then lists the failed assertions and their corresponding lines. The with expansion section shows that the test expected an assertion to be true, but it actually returned false.
Once you fix the program, running the colliderTest target should produce the following final output:
using ./../res/app.json for configuration. =============================================================================== All tests passed (109 assertions in 2 test cases)
Note: In QtCreator, this output is visible in the “Application output” window.
This output indicates that 109 assertions (CHECK, CHECK_FALSE, etc.) were successfully executed across two test scenarios ("2 test cases").
Note that if the tested functionalities are missing from your code, running the test will result in compilation errors.
For example, if your constructor is not implemented correctly (e.g., missing or incorrectly ordered arguments), you will get an error message like:
error: no matching function for call to 'Collider::Collider(const Vec2d&, double)'
If you want to test the methods as you implement them, which is recommended, you can simply comment out the CHECK or CHECK_FALSE statements that call methods you haven't implemented yet.
The tests are globally numbered throughout the project. This is the first test in the project. Each test is graded.
Running the colliderTest target should produce the following final output:
Collider: position = (1, 1), radius = 2 =============================================================================== All tests passed (109 assertions in 2 test cases)