2 mai 2025
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
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 se rapprocher de
la procédure utilisée dans l’UV CSC4526.Include/Book ont été mis dans le répertoire
Include.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)
pour référencer GoogleTest et le dossier contenant les tests
unitaires.
# Lignes à ajouter **avant** la ligne `add_subdirectory(src)`
#
# Include GoogleTest
#
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/releases/download/v1.16.0/googletest-1.16.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()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_include_directories(unitTests
PRIVATE
../Include
)
target_link_libraries(unitTests GTest::gtest_main lib_simpleGame sfml-graphics)
if (WIN32)
target_link_libraries(unitTests wsock32.lib ws2_32.lib)
else() # We are under UNIX
target_link_options(unitTests PRIVATE -pthread)
endif()
# The next line enables CMake’s test runner to discover the tests included in the binary,
# using the GoogleTest CMake module (which was included in root CMakeLists.txt).
include(GoogleTest)
gtest_discover_tests(unitTests)src/CMakeLists.txt doit être modifié pour qu’il
contienne désormais des lignes spécifiant que tous les sources, hormis
le source contenant main(), sont mises dans une
bibliothèque. Ainsi, ils ne sont donc compilés qu’une seule fois, même
s’ils servent pour le programme principal et pour les
tests unitaires. Voici son nouveau contenu :
add_library(lib_simpleGame
Source/Game.cpp
Source/RoundTarget.cpp
Source/RoundTarget.cpp
)
target_include_directories(lib_simpleGame
PRIVATE
Include
)
target_link_libraries(lib_simpleGame PUBLIC sfml-graphics)
add_executable(simpleGame
Source/Main.cpp
)
target_include_directories(simpleGame
PRIVATE
Include
)
target_link_libraries(simpleGame PUBLIC lib_simpleGame sfml-graphics)
add_subdirectory(Test)
# The following command is executed only when cmake is executed
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/../media DESTINATION ${CMAKE_CURRENT_BINARY_DIR})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é :
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 :
TODO 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 d’un groupe, cela détruit, avec animation,
l’ensemble des Target du groupe.TODO 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 à -30° par rapport à la
trajectoire de la cible initiale, l’autre cible partant à
+30° par rapport à la trajectoire de la cible initiale (la
norme de la vitesse restant à 100).
NB : Dans le cas où le Target cliqué est celui d’un
GroupTarget, tous les RoundTarget du groupe
sont subdivisés.
TODO Corrigé (à exploiter selon la procédure Construire un projet C++ avec cmake).