22 avril 2026
L’objectif de cet exercice fil rouge est de développer un mini-jeu qui illustre les différentes étapes du cours CSC4526.
Décompressez l’archive SimpleGame.zip dans le répertoire de votre choix.
Exploitez le canevas de projet obtenu selon la procédure Construire un projet C++ avec cmake.
Exécutez SimpleGameOriginal. Vous devriez obtenir la
figure suivante :
Z, Q, S et D.Setting vertical sync not supported. Nous étudierons ce
message dans la section suivante.Le code du répertoire src_original provient du
répertoire 01_Intro du dépôt Git
(cette
archive contient ce code en version exploitable selon la procédure
cmake proposée en CSC 4526) hébergeant les exemples du livre “SFML game
development : learn how to use SFML 2.0 to develop your own
feature-packed game” de Jan Haller, Henrik Vogelius Hansson et Artur
Moreira, disponible sous format
électronique ici. Pour y accéder :
Lisez Chapter 1: Making a Game Tick en analysant le code
dans src_original de manière à être sûr·e que vous
comprenez bien ce code.
Pour information, le code de src_original a été
adapté du répertoire 01_Intro original de la manière
suivante :
CMakeLists.txt a été modifié pour correspondre à la
procédure utilisée dans l’UV CSC4526 :
Include/Book ont été transférés dans le répertoire
core.Main.cpp a été transféré dans
main/main.cpp, la majorité de son code étant transférée
dans core/my_main.cpp.Game.cpp a été transféré dans le répertoire
core.media a été renommé
res.Eagle.png (cf. section Adapting the code du
chapitre).Étudiez maintenant le code dans le répertoire src
qui sera désormais votre répertoire de travail. Ce code est une
adaptation du code dans src_original destinée, nous
l’espérons, à vous simplifier la vie pour la suite de cet exercice.
Vérifiez que vous comprenez les changements qui ont été apportés :
RoundTarget.Game::PlayerSpeed est devenue la
constexpr RoundTarget::TargetSpeed.std::format dans
Game::updateStatistics(), nous nous sommes affranchi·es de
l’utilisation des fichiers StringHelpers.hpp et
StringHelpers.ini. **NB : Cela pourrait vous causer des
soucis de compilation si vous êtes sur macOS et que vous utilisez le
compilateur Apple clang fourni avec un XCode de
version inférieure à 15.0 (cf. ligne “Text formatting (FTM)” de la page
Compiler
support de C++ 20). Si vous avez ces soucis, remplacez
l’instructionstd::format("Frames / Second = {}\nTime / Update = {} us", mStatisticsNumFrames, mStatisticsUpdateTime.asMicroseconds() / mStatisticsNumFrames )
"Frames / Second = " + std::to_string(mStatisticsNumFrames) + "\nTime / Update = "+ std::to_string(mStatisticsUpdateTime.asMicroseconds() / mStatisticsNumFrames) + " us"Game::mPlayer est devenue
Game::mTarget.Passons maintenant (enfin !) à des premières évolutions de ce
code. NB : Ces évolutions sont à faire dans le
répertoire src, pas le répertoire src_original
!
Commençons par améliorer la consommation CPU du programme.
Actuellement, la Game Loop tourne à pleine vitesse. Certes,
cela permet d’avoir un frame rate d’environ 5000 FPS
(Frames Per Second). Mais cela est non seulement inutile (vu
que notre œil n’a pas besoin de plus de 60 FPS), mais c’est surtout
dommageable pour la consommation électrique de votre machine et donc
pour la planète (vu que vous consommez inutilement de la CPU ;
vérifiez-le en utilisant votre “Gestionnaire des Tâches” sous
Windows et la commande top sous
Linux).
Game::run(), insérez l’instruction
mWindow.setVerticalSyncEnabled(true); juste avant
l’instruction while (mWindow.isOpen()). Exécutez votre
programme et vérifiez que votre framerate n’est désormais que de 60 FPS
et que donc votre consommation CPU est réduite.Setting vertical sync not supported à
l’exécution). Dans ce cas, l’instruction
mWindow.setVerticalSyncEnabled(true); n’a aucun effet sur
votre frame rate. Remplacez-la par l’instruction
mWindow.setFramerateLimit(60); et vérifiez qu’elle a
l’effet escompté.Faites évoluer le code pour que 1) le cercle cyan ait une vitesse initiale v=(100,100) et 2) se déplace donc sans clavier (NB : Pour l’instant, ne vous préoccupez pas des rebonds sur les bords de l’écran) : Vous devriez obtenir un cercle qui se déplace en diagonale et qui sort de l’écran au bout de quelques secondes.
Supprimez
RoundTarget::handlePlayerInput et toutes les
variables d’instance qu’elle utilisait,RoundTarget::TargetSpeed désormais
inutile.,Game::processEvents(), les
if (const auto* keyPressed... et
if (const auto* keyReleased désormais inutilesAjoutez une variable d’instance RoundTarget::mSpeed de
type sf::Vector2f.
Ensuite, modifiez le constructeur de RoundTarget pour initialiser ce
vecteur. Modifiez également l’appel à ce constructeur dans
Game::Game().
Enfin, modifiez la méthode RoundTarget::update() pour
exploiter mSpeed.
Corrigé (à exploiter selon la procédure Construire un projet C++ avec cmake).
Rappel : Travaillez dans le répertoire
src, pas le répertoire src_original !
Mettez en place le rebond d’un cercle sur les bords selon une démarche TDD (Test-Driven Development). De ce fait, commencez par mettre en place les tests que vous souhaitez effectuer.
NB : Le rebond nécessite de changer la signature de la méthode
RoundTarget::update(const sf::Time &elapsedTime) en
void RoundTarget::update(const sf::Time &elapsedTime, const sf::Vector2u &windowSize)
de sorte que vous puissiez connaître la taille de la fenêtre.
Dans le fichier CMakeLists.txt racine, ajoutez les
lignes suivantes derrière la ligne
add_subdirectory(src/main) pour référencer
GoogleTest et le dossier contenant les tests unitaires.
# Lignes à ajouter **apres** la ligne `add_subdirectory(src/main)`
#
# Include GoogleTest
#
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/refs/tags/v1.17.0.tar.gz
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
# enable_testing() must be in the source directory root (see cmake documentation at https://cmake.org/cmake/help/latest/command/enable_testing.html)
# Otherwise, Visual Studio test explorer does not see unit tests (See ticket https://developercommunity.visualstudio.com/t/No-tests-discovered-for-googletest-and-C/1148799#T-ND1150621)
include(GoogleTest)
enable_testing()
add_subdirectory(src/test)Votre fichier src/test/CMakeLists.txt doit contenir les
lignes suivantes:
#
# Lines inspired by https://google.github.io/googletest/quickstart-cmake.html
#
# Note: include(GoogleTest)and enable_testing() already done in top CMakeLists.txt
add_executable(unitTests
RoundTargetTest.cpp
)
target_link_libraries(unitTests GTest::gtest_main lib_core sfml-graphics)
# The next line enables CMake’s test runner to discover the tests included in the binary,
# using the GoogleTest CMake module.
include(GoogleTest)
gtest_discover_tests(unitTests)Il y a 5 cas à tester :
RoundTarget est en plein milieu de
l’écran et avance donc nominalement lors de l’invocation de la méthode
update() sur cette instance.RoundTarget est collée au bord gauche
de l’écran avec une vitesse négative selon l’axe des x.RoundTarget collée au bord
supérieur de l’écran avec une vitesse négative selon l’axe des y.RoundTarget collée au bord
droit de l’écran avec une vitesse positive selon l’axe des x.RoundTarget collée au bord
inférieur de l’écran avec une vitesse positive selon l’axe des y.Partez des lignes suivantes pour votre propre fichier
src/test/RoundTargetTest.cpp :
#include <gtest/gtest.h>
#include "RoundTarget.hpp"
namespace RoundTarget_test {
static const sf::Time elapsedTime = sf::seconds(1.f/60.f);
constexpr auto notScreenBoundary = 200.f;
constexpr auto radius = 50.f;
constexpr auto speed = 100.f;
const sf::Vector2u windowSize{640, 480};
TEST(RoundTargetTest, update_noBounce) {
RoundTarget target{radius, sf::Color::Cyan, notScreenBoundary, notScreenBoundary, speed, speed};
target.update(elapsedTime, windowSize);
EXPECT_FLOAT_EQ(notScreenBoundary + speed * elapsedTime.asSeconds(), target.getShape().getPosition().x);
EXPECT_FLOAT_EQ(notScreenBoundary + speed * elapsedTime.asSeconds(), target.getShape().getPosition().y);
target.update(elapsedTime, windowSize);
EXPECT_FLOAT_EQ(notScreenBoundary + 2 * speed * elapsedTime.asSeconds(), target.getShape().getPosition().x);
EXPECT_FLOAT_EQ(notScreenBoundary + 2 * speed * elapsedTime.asSeconds(), target.getShape().getPosition().y);
}
TEST(RoundTargetTest, update_bounceLeft) {
RoundTarget target{radius, sf::Color::Cyan, 0.f, notScreenBoundary, -speed, -speed};
target.update(elapsedTime, windowSize);
EXPECT_FLOAT_EQ(speed * elapsedTime.asSeconds(), target.getShape().getPosition().x);
EXPECT_FLOAT_EQ(notScreenBoundary - speed * elapsedTime.asSeconds(), target.getShape().getPosition().y);
target.update(elapsedTime, windowSize);
EXPECT_FLOAT_EQ(2 * speed * elapsedTime.asSeconds(), target.getShape().getPosition().x);
EXPECT_FLOAT_EQ(notScreenBoundary - 2 * speed * elapsedTime.asSeconds(), target.getShape().getPosition().y);
}
TEST(RoundTargetTest, update_bounceUp) {
// TODO A vous de completer !
}
TEST(RoundTargetTest, update_bounceRight) {
RoundTarget target{radius, sf::Color::Cyan, static_cast<float>(windowSize.x), notScreenBoundary, speed, speed};
target.update(elapsedTime, windowSize);
EXPECT_FLOAT_EQ(static_cast<float>(windowSize.x) - 4 * target.getShape().getRadius() - speed * elapsedTime.asSeconds(), target.getShape().getPosition().x);
EXPECT_FLOAT_EQ(notScreenBoundary + speed * elapsedTime.asSeconds(), target.getShape().getPosition().y);
target.update(elapsedTime, windowSize);
EXPECT_FLOAT_EQ(static_cast<float>(windowSize.x) - 4 * target.getShape().getRadius() - 2 * speed * elapsedTime.asSeconds(), target.getShape().getPosition().x);
EXPECT_FLOAT_EQ(notScreenBoundary + 2 * speed * elapsedTime.asSeconds(), target.getShape().getPosition().y);
}
TEST(RoundTargetTest, update_bounceBottom) {
// TODO A vous de completer !
}
}Modifiez Game.cpp de sorte qu’il affiche 100
RoundTarget de taille aléatoire comprise entre 10 et 50, de
couleur aléatoire entre Green, Blue, Yellow ou Magenta, de position
aléatoire à l’écran et de vitesse aléatoire, la vitesse selon l’axe des
x étant comprise entre -100 et 100, la vitesse selon l’axe des y étant
une valeur positive ou négative telle que la norme du vecteur vitesse
est égale à 100. NB : Les RoundTarget doivent se
déplacer.
Vous devriez obtenir un affichage semblable à celui-ci :
Ajoutez maintenant un aspect ludique : Quand vous cliquez gauche sur l’un des cercles, ce cercle disparaît. NB :
RoundTarget. Pensez à la tester
unitairement.Voici quelques étapes possibles pour implémenter cette fonctionnalité :
event concernant la souris
en vous servant de cette
documentation.Game qui balaye le vecteur des
RoundTarget, dans l’ordre inverse (de manière à considérer
d’abord les RoundTarget affichés en dernier, donc au dessus
des autres), pour trouver une instance de RoundTarget pour
laquelle la méthode
bool RoundTarget::isHitByMouse(const sf::Vector2i &mousePosition) const
(que vous devez écrire et tester unitairement avec, par exemple, deux
tests TEST(RoundTargetTest, isHitByMouse_true) et
TEST(RoundTargetTest, isHitByMouse_false)) renvoie
true. Il faut ensuite supprimer cette instance du vecteur.
Pour ce dernier point, cet
article pourrait vous aider.Corrigé (à exploiter selon la procédure Construire un projet C++ avec cmake).
Dans Game.hpp, remplacez
std::vector<RoundTarget> par
std::list<RoundTarget>, puis vérifiez que votre
programme continue à fonctionner correctement.
NB :
vector ou d’une list.vector.vector et list. Lequel vous semble préférable
?vector semble préférable à list. En effet,
tous les 60ème de seconde, le programme parcourt deux fois le
conteneur, la première dans Game::update() et la deuxième
dans Game::render(). Pour tous ces parcours, l’efficacité
de vector est très supérieure à celle de list.
Certes, à chaque clic gauche, le programme fait potentiellement une
suppression dans le conteneur, opération où list est bien
plus efficace que vector. Mais, ces suppressions sont
beaucoup moins fréquentes que le parcours de conteneurs.
Pour l’instant, lorsque l’utilisateur clique gauche sur une instance
de RoundTarget, celle-ci disparaît simplement. Ajoutons une
animation pour mieux visualiser la disparition de ce cercle avec les
étapes suivantes quand une instance se fait cliquer dessus :
sf::Color::Red.RoundTarget.Comme d’habitude, pensez à faire des tests unitaires sur les
éventuelles nouvelles méthodes créées dans RoundTarget.
Voici quelques étapes possibles pour implémenter cette fonctionnalité :
src/core/RoundTarget.hpp, définissez un
enum class RoundTargetStatus {Alive, Dying, Dead};
définissant les différents états par lesquels passe une instance de
RoundTarget.void RoundTarget::die(); qui est
appelée par Game quand cette dernière constate que
l’instance a été touchée par la souris.RoundTarget::update() pour gérer les
mises-à-jour de l’instance selon son état.RoundTarget ne se fait plus au niveau du clic souris, mais
au niveau de Game::update().die() et les
conséquences des appels à quelques update()
ultérieurs.Corrigé (à exploiter selon la procédure Construire un projet C++ avec cmake).
SquareTarget, un nouveau type de TargetCette étape peut être menée quand vous avez fini l’étape 2 de l’exercice Outil (simplifié) de visualisation.
Modifiez votre code pour que le vecteur de Game
contienne non seulement des RoundTarget, mais aussi des
SquareTarget (ces derniers étant représentés graphiquement
avec des sf::RectangleShape et non avec des regular
polygons générés à l’aide de sf::CircleShape, cf. cette
documentation).
NB :
Voici quelques étapes possibles pour implémenter cette fonctionnalité :
ShapeTarget qui est mère de
RoundTarget et d’une nouvelle classe
SquareTarget.RoundTarget ne contient plus qu’une variable d’instance
sf::CircleShape mShape tandis que SquareTarget
ne contient plus qu’une variable
d’instancesf::RectangleShape mShape.ShapeTarget) :
void drawCurrent(sf::RenderWindow &window)bool isHitByMouse(const sf::Vector2i &mousePosition) const.ShapeTarget, il ne faut pas oublier de
définir la méthode virtual ~ShapeTarget() = default;. Sinon
il y a fuite mémoire.TestSquareTarget.cpp dans lequel il n’y a besoin que de
faire des tests concernant la méthode
SquareTarget::isHitByMouse(). En effet, le fonctionnement
de cette méthode est différent de celui de
RoundTarget::isHitByMouse() et
SquareTarget::drawCurrent() ne peut pas être testé
unitairement.Vous devriez obtenir un affichage semblable à celui-ci :
Corrigé (à exploiter selon la procédure Construire un projet C++ avec cmake).
GroupTarget, un autre nouveau type de
TargetCette étape peut être menée quand vous avez fini l’étape 2 de l’exercice Outil (simplifié) de visualisation.
Modifiez votre code pour que :
Game contienne aussi des
GroupTarget, chaque groupe pouvant contenir au moins 2 et
au plus 5 Target qui peuvent être une
RoundTarget, SquareTarget ou
GroupTarget.GroupTarget imbriqués.RoundTarget et SquareTarget faisant
partie d’un GroupTarget sont systématiquement de couleur
cyan.RoundTarget/SquareTarget et le centre du
deuxième premier RoundTarget/SquareTarget sont
reliés par une ligne cyan d’un pixel de large. De même, le centre du
deuxième RoundTarget/SquareTarget et le centre
du troisième etc. Idem pour le centre du dernier et le centre du
premier.RoundTarget/SquareTarget d’un groupe, cela
détruit, avec animation, l’ensemble des Target du groupe et
de l’éventuel groupe dont ce groupe fait partie (et de l’éventuel groupe
etc.). Par ailleurs, les centres des
RoundTarget/SquareTarget sont reliés par des
lignes rouges.Pour le tracé des lignes entre les centres, une piste est d’effectuer
les opérations suivantes dans
GroupTarget::drawCurrent():
std::vector<sf::Vertex> vertices;
comme présenté dans cette
documentation,sf::Vertex en leur donnant la couleur cyan ou
rouge selon le status du groupe,vertices à la fin (pour
tracer une ligne du dernier élément vers le premier),window.draw(&vertices[0], vertices.size(), sf::PrimitiveType::LineStrip);
(La constante LineStrip est explicitée ici).En ce qui concerne les tests unitaires, pour simplifier leur
programmation, il est intéressant d’utiliser un test
fixture, dont vous avez un
exemple ici. Ainsi, avant chacun des tests de
GroupTarget du corrigé, grâce la méthode
SetUp() (similaire à la méthode JUnit vue en
CSC4102), nous créons systématiquement un GroupTarget
contenant un SquareTarget et un GroupTarget
contenant un RoundTarget. Une fois le test passé, la
méthode TearDown() (similaire à la méthode JUnit
vue en CSC4102) supprime ces objets.
Vous devriez obtenir un affichage semblable à celui-ci (notez la
couleur rouge des Target et des lignes du groupe en train
de mourir en haut à gauche) :
Corrigé (à exploiter selon la procédure Construire un projet C++ avec cmake).
TargetCette étape peut être menée quand vous avez fini l’étape 7 de l’exercice Outil (simplifié) de visualisation.
Modifiez votre code pour que, quand l’utilisateur clique
droit sur l’une des Target
(RoundTarget ou SquareTarget), qu’elle fasse
partie ou non d’un GroupTarget, celle-ci se subdivise en 2
cibles de même couleur, mais de surface divisée par 2 par rapport à la
surface initiale, une cible partant à -60° par rapport à la
trajectoire de la cible initiale, l’autre cible partant à
+60° par rapport à la trajectoire de la cible initiale (la
norme de la vitesse de chaque cible restant à 100).
NB : Dans le cas où le Target cliqué est celui d’un
GroupTarget, tous les Target (hormis les
GroupTarget) de ce groupe sont subdivisés.
Corrigé (à exploiter selon la procédure Construire un projet C++ avec cmake).