![]() |
The NEW Pong Game V13.2.1
An interesting implemnettaion of the pong game
|
Puisqu’il n’y a rien de plus amusant pour découvrir un langage que de créer son propre jeu, nous vous présentons The New Pong, un jeu multijoueur développé dans le cadre du module de programmation en langage objet pour la spécialité Robotique à Polytech Sorbonne.
Dans ce projet, il nous était demandé de choisir un jeu à programmer en C++, afin de mettre en pratique les notions étudiées en cours tels que:
Nous avons donc opté pour un grand classique: Pong. Préparez-vous à renvoyer la balle, tout en perfectionnant vos compétences en C++ !
Afin de revisiter l’expérience Pong, l’un des tout premiers jeux vidéo d’arcade et pionnier des jeux de sport, nous avons décidé d’en développer notre propre version. Au-delà d’un simple hommage, nous y avons ajouté de nouvelles fonctionnalités pour rendre ce Pong encore plus captivant que l’original. Pour cela, quatre modes de jeu distincts ont été introduits :
Toutes les instructions relatives à ces modes et leurs spécificités sont détaillées dans la section: Les différents modes. Bonne lecture et bon amusement !
Nous avons intégré un fichier CMakeLists.txt afin de faciliter la compilation du projet sur les principaux systèmes d’exploitation tels que macOS et Linux. Toutefois, l’interface graphique nécessite plusieurs dépendances spécifiques.
Pour simplifier cette étape, nous avons également créé un script Bash qui vérifie automatiquement si toutes les dépendances sont installées. Si ce n’est pas le cas, il se chargera de télécharger et d’installer ce qu'il manque. Vous trouverez la liste exhaustive de ces dépendances dans la section Dépendances .
Sur Linux, le script télécharge et installe automatiquement les bibliothèques SDL nécessaires. Toutefois, si l’une d’entre elles requiert une autre dépendance spécifique, vous devrez l’installer manuellement. Une fois la dépendance installée, relancez simplement le script avec la commande
bash play.sh
.Pour aller plus loin: d'abord, la commande
mkdir -p build
crée un répertoire isolé pour les fichiers générés pendant la compilation si il n'existe déjà, puiscd build
nous positionne dans ce dossier, suivie decmake ..
qui analyse le fichier CMakeLists.txt du projet pour configurer l'environnement et détecter les bibliothèques nécessaires. Enfincmake --build .
lance la compilation effective du code source.
Pour démarrer le programme en mode automatique, suivez les étapes suivantes :
cd ~/Downloads
)git clone https://github.com/vskarleas/The-New-Pong
cd The-New-Pong
chmod 777 play.sh
dans le terminal, puis lancez le script avec bash play.sh
.Pour plus de détails sur la structure du projet et les commentaires (classes, fonctions, etc.), rendez-vous sur : https://pong.madebyvasilis.site
Voici la liste des dépendances indispensables au bon fonctionnement du programme :
(Assurez-vous que ces bibliothèques sont installées ou que le script les télécharge correctement)
Le concept originel de Pong s’apparente à un simulateur de ping-pong minimaliste : une balle se déplace de part et d’autre de l’écran en rebondissant sur les bords supérieur et inférieur. Chaque joueur contrôle une raquette coulissant verticalement le long du bord de l’écran. La balle rebondit différemment selon la partie de la raquette qu’elle touche. Voici les fonctionnalités incluses :
Dans notre version, il n’y a pas de score maximum prédéfini ; les joueurs peuvent simplement s’entendre oralement sur un objectif à atteindre. Lorsqu’ils souhaitent arrêter, il suffit de choisir « End the game ». Ici, la motivation ultime est : qui fera exploser le compteur du high score et revendiquera le titre de meilleur pongiste ?
Ce mode reprend les règles du Classic , à la différence qu’il ne peut être joué que par un seul joueur : la raquette adverse est contrôlée par l’ordinateur. Préparez-vous à affronter une IA tenace. Arriverez-vous à la battre, ou rejoindrez-vous la longue liste de ses victimes ?
Dans ce mode, deux joueurs s’affrontent sur 3 tours. Le vainqueur est celui qui remporte le plus de tours . Chaque tour se compose de 8 points, et c’est le premier joueur à atteindre 8 points qui gagne le tour.
Une nouveauté pimentera votre partie : des lettres tombent depuis le haut de l’écran. En les touchant, vous obtenez un point supplémentaire et vous contribuez à former un mot caché, révélant peu à peu une phrase secrète.
Ce mode s’inspire des règles du Storytime Mode , avec un format de 3 parties où l’objectif est d’atteindre 5 points pour remporter chaque partie. Toutefois, nous y avons glissé plusieurs surprises et easter eggs destinés à dynamiser la compétition.
Puisque nous sommes de futurs roboticiens, nous ne pouvions pas résister à ajouter une petite touche de robotique : vous verrez ainsi de mystérieux robots apparaître au cours de la partie. En les touchant, vous déclencherez des effets inédits :
Saurez-vous exploiter ces bonus (et pièges) pour devenir le champion incontesté du Fun Mode ?
Cette fonctionnalité est disponible uniquement en mode Classic . Le jeu vérifie en permanence si un joueur atteint un score supérieur au record actuel. Lorsque c’est le cas, le record est immédiatement mis à jour.
La sauvegarde est effectuée dans un fichier nommé game_pong-highscore_849216.txt
, dont le contenu est chiffré afin de garantir l'intégrité des données et d'empêcher toute modification non autorisée. Ce fichier contient uniquement le dernier high score ainsi que le nom du joueur correspondant.
Voici l'algorithme qui détermine si quelqu'un a fait un nouveau highscore:
Envie de faire une pause et de retenter de battre le record un peu plus tard ? Avec la fonctionnalité de Game Save , vous pouvez sauvegarder l’état de votre partie et la reprendre quand vous le souhaitez. Là encore, le chiffrement est appliqué pour garantir l’intégrité des données.
La sauvegarde du jeu est réalisée dans un fichier nommé
game_pong-save_849374.txt
. Ce fichier reste disponible jusqu’à ce que le joueur reprenne la partie sauvegardée ou choisisse de démarrer une nouvelle partie, auquel cas il sera automatiquement supprimé. Ainsi, votre progression est préservée même après avoir quitté le jeu.
Par défaut, la balle du Pong est de forme circulaire, mais pourquoi ne pas la personnaliser ? À chaque début de partie, vous pouvez sélectionner l’une des 3 formes proposées :
Ce n’est qu’une preuve de concept : rien ne vous empêche d’imaginer et d’intégrer des formes plus originales dans l’interface graphique.
Grâce à la bibliothèque SDL Mixer, nous pouvons gérer différents effets sonores et musiques avec des fonctions de fade-in et fade-out.
Voici l'implémentation:
La sauvegarde des données utilise un système de chiffrement XOR simple avec une clé rotative:
Les données sont chiffrées avant l'écriture sur le disque et déchiffrées lors de la lecture, assurant une protection basique des sauvegardes.
Inspiré de https://www.101computing.net/xor-encryption-algorithm/ L'utilisation de XOR permet à la même opération de chiffrer et de déchiffrer
Dans ce projet, toutes les fonctionnalités ont été implémentées sous la forme d’objets, garantissant ainsi la modularité, la flexibilité et une organisation claire du code. Chaque élément du jeu Pong est représenté par une classe spécifique, ce qui permet une maintenance aisée et une évolutivité simplifiée du programme.
Voici les différentes classes que nous avons définies :
Classes | Description | Fichier |
---|---|---|
AI | Intelligence artificielle pour contrôler une raquette automatiquement | ai.cpp |
BallBase | Classe de base abstraite pour tous les types de balles dans le jeu car nous proposons différents types de balles à choisir avant de lancer le jeu | ball_base.pp |
ClassicBall | Implémentation classique de balle circulaire héritant de BallBase | classic_ball.cpp |
Game | Contient tous les paramètres principaux, surtout les références de tous les autres objets mentionnés dans cette liste | game.cpp |
GameOver | Gère l'écran de fin de partie lorsqu'une partie est terminée ou si on choisit de terminer manuellement une partie | game_over.cpp |
GUI | Classe utilitaire fournissant des fonctionnalités d'interface utilisateur (donner notre prénom via SDL)) | gui.cpp |
HighScore [structure] | Structure représentant un record de score . Il gère la sauvegarde de ces données spécifiques | game_save.cpp |
InvisiblePower | Rend la balle temporairement invisible. Il hérite de la classe Power | invisible_power.cpp |
Power | Représente les éléments de power-up qui affectent le gameplay comme le changement de la taille de la raquette, ou rendre la balle invisible | power.cpp |
Letter | Représente une lettre dans le mode de jeu Storytime. Contient toute la fonctionnalité pour gérer les mots dans ce mode Storytime | letter.cpp |
Paddle | Représente une raquette (paddle) de joueur | paddle.cpp |
SaveState [structure] | Structure représentant l'état complet du jeu pour la sauvegarde/le chargement | game_save.cpp |
Saving | Classe utilitaire de sauvegarde pour gérer la sauvegarde de la partie et la fonctionnalité de score élevé | game_save.cpp |
SoundEffects | Classe pour gérer les effets sonores et la musique dans le jeu | sound_effects.cpp |
SquareBall | Implémentation de la balle en forme de carré héritant de BallBase | square_ball.cpp |
TriangleBall | Implémentation de la balle en forme de triangle héritant de BallBase | triangle_ball.cpp |
User | Représente un joueur dans le jeu avec son nom et le suivi du score | user.cpp |
page_2b_1t | Écran d'avis avec 2 boutons et 1 titre | page_2b_1t.cpp |
page_3b | Menu de pause avec 3 boutons | page_3b.cpp |
page_3b_0t | Classe de menu principal avec 3 boutons et aucun titre | page_3b_0t.cpp |
page_3b_1t | Classe de menu intermédiaire avec 3 boutons et 1 titre | page_3b_1t.cpp |
page_4b_1t | Définit le menu de sélection de mode avec 4 boutons et 1 titre | page_4b_1t.cpp |
Foncteur | Descriptions | Fichier |
---|---|---|
triangle_renderer | Foncteur pour le rendu des formes triangulaires | renderers.cpp |
square_renderer | Foncteur de rendu de formes carrées | renderers.cpp |
L'utilisation de foncteurs nous permet d'ajouter facilement de nouveaux types de formes et de les tester individuellement.
Cette approche nous a permis d'accélérer le développement en permettant des tests isolés des différents SDL renderers.
Afin de garantir une interface utilisateur claire, fluide et facilement adaptable, nous avons défini plusieurs structures prédéfinies pour l’affichage des différentes pages du jeu. Chaque modèle est conçu pour répondre à des besoins spécifiques et assurer une navigation intuitive.
Voici les spécifications précises de chaque modèle :
page_3b_1t
: Trois boutons centrés verticalement, accompagnés d’un titre en gras en haut de la page (utilisé pour les menus principaux).page_2b_1t
: Deux boutons et une large section dédiée à un texte explicatif (idéal pour l’affichage d’avis ou d’instructions détaillées).page_4b_1t
: Quatre boutons répartis sur la page, avec un titre en gras en haut (utilisé pour la sélection des modes de jeu).page_3b_0t
: Trois boutons répartis de manière spécifique : deux placés en haut et un troisième positionné vers le bas de la page (permettant de mettre en avant une option particulière).page_3b
: Trois boutons alignés verticalement et centrés au milieu de l’écran (structure utilisée pour le menu pause).Ces structures offrent une navigation cohérente, garantissant une meilleure expérience utilisateur tout au long du jeu.
Maintenant que nous avons une vue d’ensemble des différentes pages et des éléments interactifs du jeu, intéressons-nous à la façon dont l’interface graphique est conçue et gérée.
Nous utilisons SDL pour afficher et rendre toutes les formes et objets du jeu dans une fenêtre aux dimensions prédéfinies dans le fichier macros.hpp
(plus de détails dans la section Pourquoi macros.hpp).
Le programme principal repose sur la classe Game
, qui orchestre l’ensemble du jeu à travers trois méthodes clés :
initialise()
– Initialise tous les paramètres et variables nécessaires au jeu.loop()
– Gère la boucle principale du jeu.close()
– Libère les ressources et termine proprement l’exécution.La méthode loop()
constitue le cœur du jeu : il s’agit d’une boucle while qui tourne en continu tant que le jeu est actif. Cette boucle s’arrête uniquement si la variable booléenne mIsRunning
est définie sur false
, soit lorsque le joueur ferme la fenêtre SDL, soit lorsqu'il sélectionne "Exit Game".
Dans cette boucle, trois fonctions essentielles assurent le bon déroulement du jeu :
game_logic()
: Gère la logique principale et décide des transitions entre les pages, menus et événements du jeu.game()
: Met à jour l’état du jeu en fonction des actions du joueur, détermine si une partie est terminée et applique les règles.output()
: Génère et affiche les éléments visuels sur l’interface SDL en fonction des paramètres définis par la logique du jeu.Ces trois fonctions fonctionnent en synergie pour offrir une expérience fluide et dynamique, assurant que le jeu réagit de manière cohérente aux interactions du joueur.
L'héritage est largement utilisé pour étendre la fonctionnalité des classes de base. Les trois types de balles (ClassicBall
, SquareBall
et TriangleBall
) héritent tous de la classe abstraite BallBase
. Par exemple, dans classic_ball.hpp
, nous voyons :
Dans le domaine des power-ups, nous avons également une hiérarchie d'héritage. Les classes InvisiblePower
et InversePower
héritent de la classe Power
, comme on peut le voir dans invisible_power.hpp
et inverse_power.hpp
. Cela permet de partager le comportement commun tout en spécialisant certaines fonctionnalités
Le polymorphisme est implémenté à travers l'utilisation de méthodes virtuelles et leur redéfinition dans les classes dérivées. Un exemple clair se trouve dans la hiérarchie des balles, où la méthode render_object()
est définie différemment dans chaque type de balle :
classic_ball.cpp
, elle dessine un cercle.square_ball.cpp
, elle dessine un carré.triangle_ball.cpp
, elle dessine un triangle.Le jeu peut manipuler n'importe quel objet dérivé de BallBase
de manière uniforme, en appelant mBall->render_object(renderer)
dans game.cpp
, sans se soucier du type spécifique de balle utilisé.
De même, les power-ups démontrent le polymorphisme avec leurs méthodes update()
et render()
qui sont appelées de manière générique mais exécutent un comportement spécifique à chaque type de power-up.
L'abstraction est implémentée principalement à travers les classes abstraites du projet. La classe BallBase
est un excellent exemple d'abstraction. Dans le fichier ball_base.hpp
, nous définissons une interface commune pour tous les types de balles avec des méthodes abstraites comme render_object()
. Cette méthode est déclarée virtuelle pure (= 0
), obligeant toutes les classes dérivées à fournir leur propre implémentation.
De même, la classe Power
dans power.hpp
fournit une abstraction pour les différents types de power-ups du jeu, avec des méthodes virtuelles qui peuvent être redéfinies par les classes dérivées comme InvisiblePower
et InversePower
.
Nous utilisons des fonctions lambda pour contrôler les limites physiques de la raquette (paddle). Cette approche nous permet d'obtenir un code modulaire, facilitant la mise à jour des fonctionnalités liées au déplacement et aux contraintes de position de la raquette.
Les lambdas sont particulièrement adaptées à notre cas, car elles nous permettent de définir des fonctions anonymes tout en bénéficiant d’un typage automatique, simplifiant ainsi l'écriture du code.
Les deux fonctions lambda utilisées sont:
auto move_paddle = [this](float delta, float time)
utiliser pour bouger l'objet paddle (la raquette)auto adjust_boundaries = [this]()
responsable de vérifier et ajuster les limites de la raquetteL'encapsulation est présente dans presque toutes les classes du projet, avec une distinction claire entre les interfaces publiques et les détails d'implémentation privés. Par exemple, la classe User
dans user.hpp
encapsule les données relatives au joueur :
Un autre exemple d'encapsulation se trouve dans le fichier game_save.cpp
, où un namespace anonyme est utilisé. Tous les détails sont disponibles ci-dessous.
Dans le fichier game_save.cpp, nous utilisons un namespace anonyme afin d'encapsuler les constantes sensibles (comme la clé de chiffrement XOR) ainsi que les fonctions utilitaires dédiées au codage et au décodage des données de sauvegarde.
Ce choix présente plusieurs avantages :
L'utilisation d’un namespace anonyme garantit donc une encapsulation stricte et protège les données critiques du jeu contre toute manipulation involontaire ou non autorisée. Cette approche garantit que ces éléments ne sont accessibles que depuis ce fichier, renforçant ainsi la sécurité du mécanisme de sauvegarde.
Cette organisation du code illustre parfaitement le principe d’encapsulation, un pilier fondamental de la programmation orientée objet. En limitant l’accès aux éléments internes du système de sauvegarde, nous renforçons la sécurité, l’isolation et la robustesse globale du jeu.
Afin de réduire la longueur du rapport, nous avons retiré les commentaires détaillés des différentes fonctions. Pour une explication complète et une vue d’ensemble du code, vous pouvez consulter directement game_save.cpp.
Tout au long du projet, nous avons soigneusement choisi les niveaux de visibilité des variables dans nos classes, en décidant de les déclarer private
ou public
en fonction de leur usage. De plus, nous avons veillé à utiliser de manière appropriée les mots-clés static
et virtual
, garantissant ainsi une encapsulation efficace et une meilleure organisation du code.
Lors de nos cours, nous avons étudié l’utilisation de CTest pour organiser et automatiser les tests unitaires. Étant donné que nous avons déjà intégré un fichier CMakeLists.txt dans notre projet, nous avons choisi d’utiliser cette approche pour tester nos fonctionnalités et les méthodes implémentées dans nos différentes classes.
Le répertoire tests
contient son propre fichier CmakeLists.txt
, qui permet de créer les exécutables de nos programmes de tests afin de vérifier le bon fonctionnement de nos méthodes.
Chaque fichier *_test.cpp
est un programme autonome pouvant être exécuté indépendamment. Si le test est réussi, il retourne 0. Cependant en cas d’échec, il renvoie une autre valeur avec des informations détaillées sur le problème rencontré.
La réalisation des tests unitaires s’inscrit dans une démarche d’intégration continue, permettant de valider la non-régression du code tout au long du développement de notre jeu.
Voici les différentes fonctionnalités que nous avons testées :
BallBase
et de ses classes dérivées (SquareBall, TriangleBall, ClassicBall), la vérification des constructeurs, setters et getters de la classe en question, ainsi que les méthodes responsables du rendu graphique sous SDL.Nous utilisons les méthodes statiques de la classe Assert
pour comparer les résultats obtenus avec les résultats attendus. Si la classe testée nécessite une initialisation de l’environnement SDL (pour le rendu graphique), celui-ci est chargé avant l’exécution des tests, même si l’affichage reste invisible.
Chaque appel à assert
évalue une expression booléenne qui représente une condition que le programme doit satisfaire pour être considéré comme correct. Si cette condition est vraie, l'exécution du programme se poursuit normalement, permettant ainsi de vérifier des conditions supplémentaires. En revanche, si l'expression s'avère fausse, le programme s'interrompt immédiatement avec un message d'erreur précisant le fichier source. Au final, si jamais notre main d’un test retourne 0, alors le test est bien terminé sans des erreurs.
Le fichier macros.hpp joue un rôle central dans notre projet en servant de référentiel unique pour toutes les constantes globales du jeu. Il permet de centraliser et de faciliter la gestion des paramètres essentiels, tels que :
Grâce à ce fichier, nous avons assuré une meilleure lisibilité et une maintenance simplifiée, en évitant la dispersion des constantes dans l’ensemble du code.
Initialement, nous avons tenté d'implémenter un mode multijoueur en réseau via TCP avec une architecture client-serveur. Cependant, nous nous sommes rapidement heurtés à la complexité de cette intégration.
En effet, cette fonctionnalité aurait dû être pensée dès le début du projet afin d’être intégrée naturellement dans l’architecture existante. L’ajout tardif d’un mode réseau implique de lourdes modifications sur la structure actuelle du code, ce qui s’avère être un défi technique conséquent.
Malgré ces difficultés, nous avons commencé le développement de cette partie dans les fichiers network.cpp
et network.hpp
, en nous concentrant sur les aspects suivants :
Pour mieux structurer notre projet et assurer une architecture claire et maintenable, nous avons modélisé les principales classes du jeu sous forme de diagrammes UML. Ces diagrammes UML permettent de visualiser l’architecture du projet et les interactions entre les classes. Cette structuration facilite la compréhension du code, son évolutivité et sa maintenance.
Class | UML |
---|---|
AI | ![]() |
Paddle | ![]() |
Pages (all) | ![]() |
Powers (all) | ![]() |
Save | ![]() |
Sound | ![]() |
User | ![]() |
Letter | ![]() |
Balls | ![]() |
Avec cette approche, chaque élément du jeu remplit un rôle bien défini et reste modulaire, ce qui permet d’ajouter de nouvelles fonctionnalités (comme le mode réseau) sans perturber l’ensemble du projet. Nous avons créé un diagramme UML pour les différentes dépendances entre les classes. De plus, les différentes notions de programmation utilisées dans chaque partie y sont indiquées.
Pong, mais en mieux ! Notre projet revisite ce grand classique du jeu vidéo en exploitant pleinement les principes de la programmation orientée objet, nous permettant de créer un code modulaire, extensible et maintenable.
Grâce à l’abstraction et à l’héritage, nous avons structuré notre jeu avec des interfaces claires et des hiérarchies logiques. Le polymorphisme nous a permis de manipuler différents objets de manière uniforme, tandis que l’encapsulation a assuré la protection et l’intégrité des données. Nous avons également tiré parti des foncteurs et des fonctions lambda pour encapsuler des comportements spécifiques, rendant notre implémentation plus souple et efficace.
Mais ce projet ne se limite pas à un simple exercice de programmation ! Nous avons voulu pousser l’expérience plus loin, en intégrant plusieurs modes de jeu inédits, un système de sauvegarde sécurisé, une interface graphique fluide avec SDL, et même une tentative d’implémentation du multijoueur en réseau.
Le résultat ? 🎾 Un jeu fun, dynamique et personnalisable, qui vous permet de revivre l’expérience du Pong… mais avec une touche de modernité !
👉 Prêt à relever le défi et à battre le high score ? Jouez, et montrez-nous qui est le véritable maître du Pong !
Le versioning est un élément clé en programmation, assurant la cohérence des modifications et facilitant la collaboration. Il est aussi primordial pour la récupération de données en cas de perte ou corruption. Au fil du projet, nous avons créé différentes versions de notre code, chacune marquant une étape importante de son évolution. Cela nous a permis de suivre les progrès, d'intégrer de nouvelles fonctionnalités et d'effectuer des corrections de manière structurée.