6 mai 2024
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 correspondre à 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
.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 ceil 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 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.
Cliquez pour de l’aide
Supprimez 1) la méthode RoundTarget::handlePlayerInput
et toutes les variables d’instance qu’elle utilisait, 2) la constante RoundTarget::TargetSpeed
désormais inutile., 3) dans Game::processEvents()
, les case sf::Event::KeyPressed
et sf::Event::KeyReleased
désormais inutiles
Cliquez pour plus d’aide
Ajoutez 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()
.
Cliquez pour encore plus d’aide
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 Developement). 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.
Cliquez pour de l’aide sur le
CMakeLists.txt
racine
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 après la ligne `add_subdirectory(src)`
#
# Include GoogleTest
#
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip
)
# 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(tests)
Cliquez pour de l’aide sur
tests/CMakeLists.txt
Votre fichier tests/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(tests
RoundTargetTest.cpp
)target_include_directories(tests
PRIVATE
../src/Include
)target_link_libraries(tests GTest::gtest_main lib_simpleGame sfml-graphics)
# 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).
gtest_discover_tests(tests)
Cliquez pour de l’aide sur
src/CMakeLists.txt
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)
# The following command is executed only when cmake is executed
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/../media DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
Cliquez pour de l’aide sur les tests unitaires à implémenter
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 Wupdate()
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.Cliquez pour de l’aide supplémentaire si vous n’êtes vraiment pas inspiré·e pour l’écriture des tests
Partez des lignes suivantes pour votre propre fichier tests/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);2 * speed * elapsedTime.asSeconds(), target.getShape().getPosition().x);
EXPECT_FLOAT_EQ(notScreenBoundary + 2 * speed * elapsedTime.asSeconds(), target.getShape().getPosition().y);
EXPECT_FLOAT_EQ(notScreenBoundary +
}
TEST(RoundTargetTest, update_bounceLeft) {0.f, notScreenBoundary, -speed, -speed};
RoundTarget target{radius, sf::Color::Cyan,
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);2 * speed * elapsedTime.asSeconds(), target.getShape().getPosition().x);
EXPECT_FLOAT_EQ(2 * speed * elapsedTime.asSeconds(), target.getShape().getPosition().y);
EXPECT_FLOAT_EQ(notScreenBoundary -
}
TEST(RoundTargetTest, update_bounceUp) {// TODO A vous de completer !
}
TEST(RoundTargetTest, update_bounceRight) {static_cast<float>(windowSize.x), notScreenBoundary, speed, speed};
RoundTarget target{radius, sf::Color::Cyan,
target.update(elapsedTime, windowSize);static_cast<float>(windowSize.x) - 4 * target.getShape().getRadius() - speed * elapsedTime.asSeconds(), target.getShape().getPosition().x);
EXPECT_FLOAT_EQ(
EXPECT_FLOAT_EQ(notScreenBoundary + speed * elapsedTime.asSeconds(), target.getShape().getPosition().y);
target.update(elapsedTime, windowSize);static_cast<float>(windowSize.x) - 4 * target.getShape().getRadius() - 2 * speed * elapsedTime.asSeconds(), target.getShape().getPosition().x);
EXPECT_FLOAT_EQ(2 * speed * elapsedTime.asSeconds(), target.getShape().getPosition().y);
EXPECT_FLOAT_EQ(notScreenBoundary +
}
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 White, Red, Green, Blue, Yellow, Magenta ou Cyan, 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.Cliquez pour de l’aide
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) 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 ?Cliquez pour des éléments de réponse à cette dernière question
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
(de ce fait, la couleur sf::Color::Red
ne peut plus être donnée initialement à une instance de RoundTarget
).RoundTarget
.Comme d’habitude, pensez à faire des tests unitaires sur les éventuelles nouvelles méthodes créées dans RoundTarget
.
Cliquez pour de l’aide
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 ==> Les RoundTarget
et les SquareTarget
qui ne font pas partie d’un GroupTarget
ne peuvent plus avoir le cyan comme couleur initiale.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).