31 mars 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 cmake de ce document.
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 cmake
de ce document).
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(
googletestURL 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};
(RoundTargetTest, update_noBounce) {
TEST{radius, sf::Color::Cyan, notScreenBoundary, notScreenBoundary, speed, speed};
RoundTarget target
.update(elapsedTime, windowSize);
target(notScreenBoundary + speed * elapsedTime.asSeconds(), target.getShape().getPosition().x);
EXPECT_FLOAT_EQ(notScreenBoundary + speed * elapsedTime.asSeconds(), target.getShape().getPosition().y);
EXPECT_FLOAT_EQ
.update(elapsedTime, windowSize);
target(notScreenBoundary + 2 * speed * elapsedTime.asSeconds(), target.getShape().getPosition().x);
EXPECT_FLOAT_EQ(notScreenBoundary + 2 * speed * elapsedTime.asSeconds(), target.getShape().getPosition().y);
EXPECT_FLOAT_EQ}
(RoundTargetTest, update_bounceLeft) {
TEST{radius, sf::Color::Cyan, 0.f, notScreenBoundary, -speed, -speed};
RoundTarget target
.update(elapsedTime, windowSize);
target(speed * elapsedTime.asSeconds(), target.getShape().getPosition().x);
EXPECT_FLOAT_EQ(notScreenBoundary - speed * elapsedTime.asSeconds(), target.getShape().getPosition().y);
EXPECT_FLOAT_EQ
.update(elapsedTime, windowSize);
target(2 * speed * elapsedTime.asSeconds(), target.getShape().getPosition().x);
EXPECT_FLOAT_EQ(notScreenBoundary - 2 * speed * elapsedTime.asSeconds(), target.getShape().getPosition().y);
EXPECT_FLOAT_EQ}
(RoundTargetTest, update_bounceUp) {
TEST// TODO A vous de completer !
}
(RoundTargetTest, update_bounceRight) {
TEST{radius, sf::Color::Cyan, static_cast<float>(windowSize.x), notScreenBoundary, speed, speed};
RoundTarget target
.update(elapsedTime, windowSize);
target(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);
EXPECT_FLOAT_EQ
.update(elapsedTime, windowSize);
target(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);
EXPECT_FLOAT_EQ}
(RoundTargetTest, update_bounceBottom) {
TEST// 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 cmake
de ce document).
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 cmake
de ce document).
SquareTarget
, un nouveau type de Target
Cette é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 cmake
de ce document).
GroupTarget
, un autre nouveau type de
Target
Cette é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 cmake
de ce document).
Target
Cette é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 cmake
de ce document).