Skip to content
Snippets Groups Projects
Commit 0b945a5d authored by PierreEVEN's avatar PierreEVEN
Browse files

finished pierre's repport and added missing comments

parent e6d2f513
Branches
No related merge requests found
No preview for this file type
......@@ -124,7 +124,7 @@
en PacMan suite au passage de petits rigolos en ayant profité pour faire\\
une mauvaise blague bien évidente.
~ \\
Notre projet reprends ce noms original en hommage.
Notre projet reprend ce nom original en hommage.
\end{center}
% Page blanche entre la table des matières et le texte
......@@ -134,7 +134,7 @@
% Chapitre 1
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\chapter{Travail réalisé}
\chapter{État du projet}
Le programme rendu reproduit plus ou moins fidèlement le comportement du jeu Pacman de 1980.
Il est à noter que cette fidélité est suggestive, voici un résumé des fonctionnalités et
différences avec le jeu original :
......@@ -144,19 +144,19 @@
rapide de ce genre de fonctionnalités)
\item La gestion des coordonnées est gérée en interne sur des doubles, ce qui peut valoir des petits
écarts de comportement avec le jeu initial. Ce choix permet néanmoins un rendu plus fluide et est plus
adapté aux plétores de taux de rafraichissement différents que l'ont peut trouver sur un écran moderne.
adapté aux pléthores de taux de rafraîchissement différents que l'on peut trouver sur un écran moderne.
\item Les sprites et la map ne sont pas exactement ceux du jeu original (Ils correspondent à la version NES).
Nous nous en sommes rendu compte tardivement ce qui nous a empéché de les corriger.
Nous nous en sommes rendu compte tardivement ce qui nous a empêchés de les corriger.
Nous l'avons tout de même modifiée pour être plus pratique à utiliser.
\item Aucun systeme de rendu de texte n'a encore été implémenté dans le moteur, c'est pourquoi il n'y a aucun
text visible dans le jeu. (les scores sont tout de même gérés et visibles dans la console)
\item Aucun système de rendu de texte n'a encore été implémenté dans le moteur, c'est pourquoi il n'y a aucun
texte visible dans le jeu. (les scores sont tout de même gérés et visibles dans la console)
\item Manger une pac-gomme ou un fantôme ne ralentit / freeze pas le jeu contrairement au jeu original.
Je trouve personnellement que c'est mieux comme ça car ça rend le jeu plus fluide.
\item Le redimensionnement de la fenêtre est "en théorie" traité, mais il doit manquer quelques petits
ajustements pour qu'il fonctionne sans bugs. C'est pourquoi il n'est pas disponible dans le programme fourni.
(c'est un cas qui n'avait pas été prévu dans la conception initiale)
\item Certains bugs du jeu original sont resimulés (ex sur l'IA des fantômes)
\item L'algorithme de déplacement du joueur est le même que celui des fantomes, contrairement au jeu initial
\item L'algorithme de déplacement du joueur est le même que celui des fantômes, contrairement au jeu initial
où le joueur est autorisé à "couper" les virages.
\item Les fantômes ne sont pas ralentis dans le tunnel.
\end{itemize}
......@@ -166,14 +166,106 @@
\chapter{Implémentation et fonctionnement}
Le programme est séparé en deux parties : le moteur et le jeu. Le moteur contient toute la partie utilitaire,
rendu graphique et gestions de sprites. Celui-ci est conçu pour être
rendu graphique et gestion de sprites. Celui-ci est conçu pour être
réutilisable dans le contexte d'un autre jeu 2D utilisant des sprites simples. \\
La partie jeu contient toutes les classes spécifiques au jeu PacMan.
Les commentaires sont non-exhaustifs et concentrés dans les headers.
Ils sont focalisés sur l'essentiel afin de faciliter la compréhension du programme par le relecteur.
\section{Le projet}
L'architecture du projet se veut la plus standard possible selon les habitudes usuelles en C++.
Les exécutables et binaires générés sont dans un dossier bin/, les assets du jeu sont dans resources/.
Le programme doit être exécuté depuis la racine du projet. (le dossier resources doit se trouver à côté
de l'exécutable)
\section{Le moteur}
Le cœur du moteur passe par la classe \textit{Engine}. Celle-ci contient la gestion du delta-time, et du coeur de
la boucle de rendu. (implémentée dans le main)
Le moteur instancie une class \textit{Gamemode} qui contient toutes les informations relatives au jeu courant.
C'est ce gamemode qui se chargera de créer le monde, les entités, gérer les scores etc...
Le moteur inclut un système de gestion des sprites : \textit{sprite\_sheet.hpp}. Il est conçu pour être le plus
intuitif et robuste possible pour l'utilisateur. Son implémentation est un peu plus complexe : les sprites sont
manipulés via des \textit{SpriteHandles} (voir commentaires dans le header correspondant)
Une classe template Vector2 a été implémentée pour représenter les coordonnées flottantes et discrètes, de même
qu'une classe permettant de gérer facilement la notion de "direction".
Aucune librairie externe n'a été utilisée mis à pars SDL, et éventuellement quelques classes utilitaires
comme logger.hpp, format.hpp et event\_manager.hpp. J'avais créé ces classes précédemment pour d'autres projets,
et je les réutilise régulièrement. Elles ont été adaptées pour correspondre aux prérequis de ce projet.
\section{Le jeu}
\begin{itemize}
\item Le niveau est généré à partir d'un fichier texte (resources/level.map).
Dans certain cas, un type de mur spécial "\^" et "v" doit être spécifié pour éviter à l'algorithme
de génération relier des murs proches.
\item La class Entity contient les informations de positionnement sur le terrain.
\item La class Character hérite de Entity gère les déplacements des entitées sur le terrain, ainsi que la vitesse.
\item GhostBase et Player héritent de Character et gèrent respectivement l'IA et le comportement des fantômes, et
la classe du joueur. (Les inputs sont restés dans le main par simplification)
\item Initialement, nous pensions que les fantômes utilisaient un pathfinding avancé, c'est pour cette raison que nous
en avons implémenté un dans la class pathfinding.hpp. Cependant en regardant plus précisément en détail le jeu original,
il s'avère que celui-ci est beaucoup plus basique. Cette classe reste donc dans le projet comme relique mais n'est pas utilisée.
\item Les différentes spécialisations de chaque type de fantôme sont implémentées dans ghosts.hpp (chacun hérite de GhostBase)
\item PacmanGamemode contient tout le reste du gameplay. (le chargement des sprite est séparé dans un fichier à part : sprite\_loader.hpp)
\end{itemize}
\section{Contribution individuelle}
Dans ce projet, j'ai concentré mes efforts sur la base du moteur (Engine.cpp) ainsi qu'une grande partie du gameplay.
J'ai essayé d'implémenter "rapidement" le gameplay le plus fidèlement possible au jeu original, et Thomas a pu repasser derrière moi
pour corriger mes erreurs et oublis. Thomas s'est également chargé de la gestion du terrain. (chargement, cellules etc...)
Note : Je pratique courament le C++ et le développement de moteurs de jeux depuis plusieurs années maintenant.
Avec l'habitude j'arrive à être plutôt efficace : j'ai déjà implémenté de nombreuses fonctionnalités
présentes dans ce projet à plusieurs reprises. Pour ces raisons la quantité de travail semblera peut être déséquilibrée, mais en
temps passé nous sommes à peu près sur le même ordre de grandeur.
\chapter{Limites et problèmes connus}
bugs connus : Fantomes décalés / fantomes bloqués au spawn / fruit raté des fois ?
// todo systeme de texte et
Le moteur créé à l'occasion est très basique. Avec un peu plus de temps nous aurions pu implémenté un système d'affichage
de texte pour les points. De même la gestion des timers est un peu "sale" actuellement et pourrait être déléguée à une
classe spécialisée dans le moteur.
Il n'y a pas non plus de possibilité de charger un autre niveau ou de changer de gamemode.
Pour ces raisons, implémenter les menus et les cinématiques aurait été très fastidieux sans ces outils.
Au vu de la charge de travail et du temps déjà passé sur ce projet nous avons jugé que ce serait superflu et
n'aurait pas servi à démontrer davantage nos compétences en C++ moderne.
\section{Bugs}
A l'heure actuelle (où j'écris ce rapport) il reste quelques bugs mineurs à notre connaissance. En voici une liste exhaustive :
\begin{itemize}
\item Les fantômes se décalent parfois visuellement et ne sont pas tout à fait alignés avec les cases.
\item Parfois (très rarement) un fantôme reste bloqué dans le spawn.
\item Thomas a mentionné un bug où PacMan passait au-dessus du fruit ou d'un pac-gomme sans le manger, mais je n'ai pas pu reproduire le bug.
\end{itemize}
\section{Conclusion}
Ce projet était plutôt intéressant et motivant. Étant habitué travailler sur des moteurs 3D plus ou moins gros, en ayant eu l'occasion d'en
implémenter une petite dizaine au cours de mon cursus, j'ai pu perfectionner mes techniques.
Ce projet est donc plus une routine sans grande surprise pour moi mais toujours très fun.
(peut être un peu long quand même)
\section{Suggestions}
Ayant un emploi du temps déjà très chargé avec les nombreux projets tous très chronophages de ce semestre,
je souhaite faire quelques suggestions qui pourront peut être servir aux prochaines promo afin de leur faire gagner
un peu de temps inutilement perdu, et réduire le facteur de stresse et d'inconnu :
\begin{itemize}
\item Le site "the pacman-dossier \url{https://pacman.holenet.info/} est une mine d'informations sur le jeu original.
j'ai découvert son existence que très tardivement. Le mentionner dans le sujet aurait je pense été très pratique
pour beaucoup d'entre nous.
\item La sprite-map fournie correspond à celle du jeu Nes (et non pas le PacMan originel de 1980)
\item Les attentes ne sont toujours pas très claires.
Peut être que fournir une liste plus détaillée des fonctionnalités voulues aurait été plus simple à gérer pour nous
que seulement demander de "reproduire" le jeu à l'identique. (avec éventuellement un bonus par suplément)
\end{itemize}
En espérant que ce retour vous aura été utile.
\end{document}
......@@ -3,6 +3,9 @@
namespace pm
{
/*
* The character class handles movements with velocity inside terrain
*/
class Character : public Entity
{
public:
......@@ -12,10 +15,16 @@ public:
}
void tick() override;
void set_look_direction(const Direction new_direction) override;
// Set the desired look direction.
// If desired direction is free, the entity will look at the required direction, then move
// until it reaches the center of the intersection before continuing in the desired direction
void set_look_direction(const Direction new_direction) override;
// Change the character velocity (in pixel / second)
void set_velocity(double new_velocity) { velocity = new_velocity; }
protected:
// is this character allowed to pass through doors
bool go_through_doors = false;
private:
double velocity = 75.0;
......
......@@ -8,24 +8,34 @@ class PathFinder;
enum class AiMode
{
// Just spawned, in this state until the first tick
Spawned,
// Should exit spawn (allowed to go through doors)
ExitSpawn,
// Chase player
Chase,
// Go to corresponding corner point
Scatter,
// Randomly move (and may be eaten by player)
Frightened,
// Move back to home (allowed to go through doors)
GoHome,
};
/**
* This is the base class to handle Ghosts behaviors
*/
class GhostBase : public Character
{
public:
explicit GhostBase(const std::shared_ptr<Terrain>& terrain, std::shared_ptr<Character> in_target);
virtual ~GhostBase();
virtual ~GhostBase();
// Implement in children classes to specialize behavior for each different ghost.
[[nodiscard]] virtual Vector2I target_player() const = 0;
[[nodiscard]] virtual Vector2I scatter_target() const = 0;
[[nodiscard]] virtual Vector2I home_location() const = 0;
[[nodiscard]] virtual double compute_speed_percent() const;
[[nodiscard]] virtual double compute_speed_percent() const;
void tick() override;
void draw() override;
......@@ -33,6 +43,7 @@ public:
void reset() override;
protected:
// Call when mode is changed or we reached a new cell (maybe an intersection)
void on_search_new_dir();
// simulate overflow error from original pacman game
......@@ -41,14 +52,22 @@ protected:
return in_dir.is_up() ? Vector2I{-1, -1} : *in_dir;
}
std::shared_ptr<Character> target;
AiMode mode;
Vector2I last_cell = {};
// Player
std::shared_ptr<Character> target;
// Current behavior
AiMode mode;
// Last cell we were, used to call on_search_new_dir once per cell.
Vector2I last_cell = {};
private:
void on_frightened();
void on_scatter();
void on_chase();
// Bound to Gamemode events
void on_frightened();
void on_scatter();
void on_chase();
// Sprites that replace defaults one in some situations
SpriteHandle frightened_sprite;
SpriteHandle frightened_sprite_flash;
SpriteHandle eyes_down_sprite;
......
......@@ -2,6 +2,10 @@
#include "ghost_base.hpp"
/*
* This is the behavior specialization for Blinky, Pinky, Inky and Clyde
*/
namespace pm
{
class Blinky : public GhostBase
......
......@@ -18,14 +18,16 @@ PacmanGamemode::PacmanGamemode()
: sprite_sheet(std::make_shared<SpriteSheet>("./resources/sprite_sheet.bmp"))
{
load_sprites();
INFO("loaded sprites");
// Create terrain
terrain = std::make_shared<pm::Terrain>();
terrain->load_from_file("./resources/level.map");
const auto terrain_unit_length = terrain->get_unit_length();
// Spawn player
player = std::make_shared<pm::Player>(pm::Player(terrain));
// Spawn ghosts
auto blinky = std::make_shared<pm::Blinky>(terrain, player);
auto pinky = std::make_shared<pm::Pinky>(terrain, player);
auto inky = std::make_shared<pm::Inky>(terrain, player, blinky);
......@@ -44,6 +46,7 @@ PacmanGamemode::PacmanGamemode()
frightened_timer = 7;
});
// Reset everything
begin_level();
}
......@@ -109,6 +112,7 @@ void PacmanGamemode::tick(double delta_seconds)
}
}
// Death timer
if (death_timer > 0)
{
const auto last_death_timer = death_timer;
......@@ -134,6 +138,7 @@ void PacmanGamemode::tick(double delta_seconds)
}
}
// Victory timer
if (victory_timer > 0)
{
const auto last_victory_timer = victory_timer;
......@@ -154,6 +159,7 @@ void PacmanGamemode::tick(double delta_seconds)
}
}
// Spawn timer
if (spawn_delay > 0)
{
const auto last_spawn_delay = spawn_delay;
......@@ -172,23 +178,28 @@ void PacmanGamemode::tick(double delta_seconds)
void PacmanGamemode::draw()
{
GamemodeBase::draw();
// Draw level
terrain->draw();
for (const auto& entity : entities)
entity->draw();
// UI sprites
// Render lives
for (int32_t i = 0; i < lives; ++i)
{
SpriteSheet::find_sprite_by_name("pacman_life")->draw({(i + 2) * terrain->get_unit_length(), (static_cast<int32_t>(terrain->get_height())) * terrain->get_unit_length()});
}
// Display fruits in the bottom right corner of the screen
int32_t items_to_display = std::min(level, 7);
for (int32_t i = 0; i < items_to_display; ++i)
terrain->get_item_sprite_handle(terrain->get_level_item(level - items_to_display + i + 1))
.draw({(18 - i) * terrain->get_unit_length(), (static_cast<int32_t>(terrain->get_height())) * terrain->get_unit_length()});
.draw({(18 - i) * terrain->get_unit_length(), (static_cast<int32_t>(terrain->get_height())) * terrain->get_unit_length()});
// Hide tunnel
SDL_FillRect(pm::Engine::get().get_surface_handle(), &tunnel_rect, 0);
// Make level blink on victory
if (victory_timer > 0 && victory_timer < 3)
{
if (static_cast<int32_t>(victory_timer * 5) % 2 == 0)
......@@ -197,6 +208,7 @@ void PacmanGamemode::draw()
terrain->set_wall_color(236, 236, 236);
}
// Render game-over and ready texts
if (lives == 0)
SpriteSheet::find_sprite_by_name("game_over")->draw({8 * terrain->get_unit_length(), 15 * terrain->get_unit_length()});
else if (spawn_delay > 0)
......
......@@ -4,8 +4,13 @@
#include <SDL_rect.h>
#include <engine/event_manager.hpp>
// See engine/event_manager.hpp
DECLARE_DELEGATE_MULTICAST(SimpleEvent)
/*
* Pacman core gameplay (this is our game mode)
*/
namespace pm
{
class Entity;
......@@ -21,8 +26,10 @@ public:
void tick(double delta_seconds) override;
void draw() override;
// Get sprite sheet containing all the sprites used in pacman's game
[[nodiscard]] SpriteSheet& global_spritesheet() const { return *sprite_sheet; }
// Event called when Ai state is globally changed
SimpleEvent on_frightened;
SimpleEvent on_scatter;
SimpleEvent on_chase;
......@@ -33,32 +40,50 @@ public:
[[nodiscard]] double frightened_remaining_time() const { return frightened_timer; }
[[nodiscard]] bool stop_movements() const { return death_timer > 0 || victory_timer > 0 || spawn_delay > 0; }
// When all the pellet have ben eaten. Start victory animation (then go to next level)
void victory();
// Kill the player, start death animation
void death();
// When eat pellets, ghosts or fruits...
void add_points(int32_t added_points);
void victory();
// Reset everything. Does not reset current levels. (on victory)
void begin_level();
// Reset entities (on death). Don't reset levels and pellet states.
void reset_positions();
private:
// Implemented in sprite_loader.cpp
void load_sprites();
std::shared_ptr<pm::Terrain> terrain;
std::shared_ptr<SpriteSheet> sprite_sheet;
int32_t level = 1;
int32_t lives = 3;
std::shared_ptr<Player> player;
std::shared_ptr<pm::Terrain> terrain;
std::shared_ptr<SpriteSheet> sprite_sheet;
int32_t level = 1;
int32_t lives = 3;
std::shared_ptr<Player> player;
// Player score
int32_t points = 0;
// All the entities (like ghosts, including players)
std::vector<std::shared_ptr<pm::Entity>> entities;
SDL_Rect tunnel_rect;
double frightened_timer = 0;
double scatter_chase_timer = 0;
double victory_timer = 0;
bool is_chase = true;
int cycle = 0;
double spawn_delay = 0;
double death_timer = 0.0;
int32_t points = 0;
// Used to render a black rectangle over right tunnel to hide entity when it exit the map.
SDL_Rect tunnel_rect;
// scatter-chase current cycle index.
int cycle = 0;
bool is_chase = true;
// Ideally we should have implemented a custom class to handle timer a proper way.
double spawn_delay = 0;
double death_timer = 0.0;
double frightened_timer = 0;
double scatter_chase_timer = 0;
double victory_timer = 0;
};
}
......@@ -11,12 +11,14 @@ public:
explicit Player(const std::shared_ptr<Terrain>& terrain);
void tick() override;
void draw() override;
// Only visual : play death animation
void play_death();
void reset() override;
// Implemented to prevent inputs to be sent while the game is not started
void set_look_direction(const Direction new_direction) override;
private:
......
#include "pacman_gamemode.hpp"
#include "engine/logger.hpp"
#include "engine/sprite_sheet.hpp"
namespace pm
......@@ -77,5 +78,7 @@ void PacmanGamemode::load_sprites()
sprite_sheet->new_sprite("eyes_up", {96, 96, 16, 16}, 20, {});
sprite_sheet->new_sprite("ready", {0, 112, 48, 16}, 20, {});
sprite_sheet->new_sprite("game_over", {48, 112, 82, 16}, 20, {});
INFO("loaded sprites");
}
}
......@@ -23,7 +23,7 @@ public:
return unit_length;
}
[[nodiscard]] EItemType get_level_item(int level) const;
[[nodiscard]] EItemType get_level_item(int level) const;
[[nodiscard]] SpriteHandle get_item_sprite_handle(EItemType item) const;
void tick(double delta_time);
......@@ -60,18 +60,18 @@ private:
void create_wall_cache_surface();
void free_wall_cache_surface();
static std::array<EItemType, 13> level_items;
static std::array<EItemType, 13> level_items;
static std::unordered_map<EItemType, int> item_values;
std::unordered_map<EItemType, SpriteHandle> item_sprite_handles;
int32_t unit_length = 16;
uint32_t width = 0;
uint32_t height = 0;
std::vector<Cell> grid = {};
std::vector<Cell> initial_grid = {};
SDL_Surface* wall_cache_surface_handle = nullptr;
int32_t gum_count = 0;
int32_t initial_gum_count = 0;
double item_timer = 0;
int32_t unit_length = 16;
uint32_t width = 0;
uint32_t height = 0;
std::vector<Cell> grid = {};
std::vector<Cell> initial_grid = {};
SDL_Surface* wall_cache_surface_handle = nullptr;
int32_t gum_count = 0;
int32_t initial_gum_count = 0;
double item_timer = 0;
};
}
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment