Exercice fil rouge

Michel SIMATIC

6 mai 2024

1 Introduction

L’objectif de cet exercice fil rouge est de développer un mini-jeu qui illustre les différentes étapes du cours CSC4526.

2 Etape de mise en place

Fenêtre SFML Application avant tout codage

3 Etape 0 : Etude du code fourni et premières évolutions

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.

Fenêtre SFML Application à l’étape 0

Corrigé (à exploiter selon la procédure cmake de ce document).

4 Etape 1 : Tests unitaires et vecteur

4.1 Tests unitaires

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 :

  1. Une instance de RoundTarget est en plein milieu de l’écran et avance donc nominalement lors de l’invocation de la méthode Wupdate() sur cette instance.
  2. Une instance de RoundTarget est collée au bord gauche de l’écran avec une vitesse négative selon l’axe des x.
  3. Idem pour une instance de RoundTarget collée au bord supérieur de l’écran avec une vitesse négative selon l’axe des y.
  4. Idem pour une instance de RoundTarget collée au bord droit de l’écran avec une vitesse positive selon l’axe des x.
  5. Idem pour une instance de 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);
        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 !
    }
}

4.2 Vecteur

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 :

Fenêtre SFML Application à l’étape 1

Ajoutez maintenant un aspect ludique : Quand vous cliquez gauche sur l’un des cercles, ce cercle disparaît. NB :

Cliquez pour de l’aide

Voici quelques étapes possibles pour implémenter cette fonctionnalité :

  1. Ajoutez la surveillance des event concernant la souris en vous servant de cette documentation.
  2. Pour connaître le bouton de la souris qui a été appuyé/relâché et la position de la souris, consultez cette documentation.
  3. Quand le bouton gauche de la souris a été appuyé, il faut invoquer une nouvelle méthode dans 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).

5 Etape 2 : Utilisation d’une liste au lieu d’un vecteur

Dans Game.hpp, remplacez std::vector<RoundTarget> par std::list<RoundTarget>, puis vérifiez que votre programme continue à fonctionner correctement.

NB :

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.

6 Etape 3 : Animation lors de la disparition d’un cercle

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 :

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é :

  1. Définissez un enum class RoundTargetStatus {Alive, Dying, Dead}; définissant les différents états par lesquels passe une instance de RoundTarget.
  2. Créez une méthode void RoundTarget::die(); qui est appelée par Game quand cette dernière constate que l’instance a été touchée par la souris.
  3. Modifiez RoundTarget::update() pour gérer les mises-à-jour de l’instance selon son état.
  4. Notez que la suppression de l’instance du vecteur des RoundTarget ne se fait plus au niveau du clic souris, mais au niveau de Game::update().
  5. Pour les tests unitaires, tester l’appel à die() et les conséquences des appels à quelques update() ultérieurs.

Corrigé (à exploiter selon la procédure cmake de ce document).

7 Etape 4 : 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).

8 Etape 5 : 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 :

TODO Corrigé (à exploiter selon la procédure cmake de ce document).

9 Etape 6 : Fission de 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).