Skip to content
/ Imac-Wars Public template
forked from dsmtE/OpenGL-Template

Final project in OpenGL / C++ from my first year of engineering school at IMAC.

Notifications You must be signed in to change notification settings

smallboyc/Imac-Wars

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🎮 TOWER DEFENSE : S2 - IMAC

D'après le Template de Enguerrand Desmet.

🎓 DE SANTIS Léo + DOUBLAL Anass + DUPUIS Maxence.
📚 Librairie / Langage : OpenGL / C++
💻 Développé sur : macOS / Windows

Logo

Tower Defense

Projet de fin d'année d'IMAC 1

➡️ Sujet du projet
➡️ Guide du jeu (PDF)

alt text

Sommaire
  1. Introduction
  2. Mécaniques
  3. Améliorations
  4. Conclusion

1️⃣ Introduction

Dans ce projet de fin d'année, nous avions comme exercice de réaliser un jeu de Tower Defense en OpenGL/C++.

Nous sommes trois à avoir travaillés sur ce projet.

📌 Concept

L'objectif principal était de concevoir un jeu fonctionnel. Cependant, nous avions à cœur de rendre ce projet plus ouvert et accessible, permettant à chacun de personnaliser le jeu et adapter la difficulté.

Nous souhaitons vous offrir la possibilité de jouer facilement vous-même à votre propre version d'Imac Wars Tower Defense. Le jeu a été conçu de manière à ce que, muni d'une carte, d'un fichier d'informations, et de quelques fonctions, vous puissiez créer votre propre carte et adapter les niveaux en jouant sur les paramètres des vagues.

Ainsi, notre projet vise non seulement à fournir une expérience de jeu captivante, mais également à donner aux utilisateurs les outils nécessaires pour personnaliser et enrichir leur propre version du jeu, rendant l'aventure encore plus engageante et personnelle.

🀄 Thème

Avant de nous lancer dans le développement du code, nous avons d'abord réfléchi à un thème qui se prêterait au mieux au style du Tower Defense. Nous avons choisi d'adapter la scène finale de l'épisode IV de Star Wars "Un Nouvel Espoir". Cette scène emblématique montre des escadrons de l'Alliance Rebelle tentant désespérément de détruire l'Étoile de la Mort.

Dans notre adaptation, le joueur incarne les forces de l'Empire. Sa mission sera de défendre l'Étoile de la Mort contre les vagues de rebelles en plaçant des tourelles de différents types pour empêcher les vaisseaux ennemis d'atteindre le réacteur et de le faire exploser.

Ce projet nous semblait extrêmement ambitieux au départ. Heureusement, la Force était avec nous !

🎯 Objectif du rapport

Ce rapport a pour objectif de développer et d'illustrer notre raisonnement dans la conception du Tower Defense. Il ne s'agit pas de décrire dans les moindres détails le code du jeu, mais de comprendre le raisonnement et les principaux éléments implémentés. Si vous souhaitez des détails supplémentaires, n'hésitez pas à regarder le code !

Un guide d'utilisation est à votre disposition pour vous permettre d'accéder aux règles, caractéristiques et commandes du jeu.

2️⃣ Nous expliquerons dans un premier temps l'implémentation des différents éléments essentiels au bon déroulement du jeu.
3️⃣ Ensuite, nous présenterons les diverses améliorations apportées pour rendre le jeu plus immersif et captivant.
4️⃣ Enfin, nous conclurons en portant un regard critique sur le travail accompli et sur les potentielles améliorations à apporter.

2️⃣ Mécaniques

🔍 Généralités

Arborescence simplifiée des principaux fichiers et dépendances.

Game
└── TowerDefense
    ├── Map
    |    ├── Cell
    |    └── Graph
    ├── Base
    ├── Tower
    |    └── Bullet
    ├── Enemy
    ├── Wave
    └── UI

⚠️ Ce rapport a été fait en parallèle de l'implémentation du jeu, il se peut que certains éléments du code n'apparaissent pas dans les captures. N'hésitez pas à vous référez au code si besoin.

Game représente le fichier principal du jeu. Toutes ses fonctions possèdent un TD (structure TowerDefense). Seules les fonctions de Game sont utilisées dans App (Application).

alt text

Les 4 premières fonctions sont celles qui permettent de faire fonctionner l'affichage des éléments du jeu.

1. LOAD : Charge toutes les textures une seule fois dans un tableau de texture appelé au tout début de l'application. Charge aussi tous les sons.

alt text

2. SETUP : Appels successifs et ordonnés de méthodes permettant de récupérer et de traiter l'information pour :

  • Afficher la carte.
  • Stocker différentes données telles que les tours, ennemis et vagues.
  • Afficher l'interface utilisateur.

Elle prend le chemin vers le fichier de la map en pixel, ainsi que la taille de la map en pixel (width ou height).

alt text

3. UPDATE : Permet de gérer les déplacements d'ennemis, l'enchainement des différentes vagues et de mettre à jour l'état du jeu. C'est là que le temps devient important !

  • elapsedTime : essentiel pour rendre les déplacements fluides et uniformes.
  • currentTime : c'est le temps qui s'écoule en secondes au lancement du jeu.

alt text

4. RENDER : C'est la fonction qui permet l'affichage des éléments de notre jeu !

alt text

Les 3 fonctions restantes permettent de détecter l'intéraction avec l'utilisateur (Clavier, mouvement, clic de la souris ou du pad).

⚠️ Pour les transformations et affichages d'objets : OpenGL utilise des coordonnées normalisées. C'est à dire que notre fenêtre en pixel, doit être transformée de sorte à ce que ses coordonnées se trouvent sur la plage [-1,1].

Ci-dessous, une partie du code permettant de passer des coordonnées de pixels en coordonnées normalisées, puis en coordonnées de carte.

alt text

Nous ne rentrerons pas dans le détail des fonctions d'interactions. Retenez simplement l'utilitée des 3 fonctions :

  • active_KEY_CALLBACK(...) : Déclenche des évênements grâce aux touches du clavier.
  • active_MOUSE_CLICK_CALLBACK(...) : Déclenche des évênements lorsque le joueur clique sur sa souris (ou pad).
  • active_MOUSE_POSITION_CALLBACK(...) : Permet de garder en mémoire la position de la souris à chaque instant.

📝 Fichier ITD

Pour référencer toutes les données importantes du jeu, nous utilisons des fichiers txt avec l'extension .itd pour (Imac Tower Defense). Un fichier nous était imposé pour représenter les différents éléments de notre carte (expliqué plus loin dans le rapport). Ce fichier est lu et analysé par notre application. Ainsi, les fonctions implémentées sont capables de lire et retranscrire à l'écran une importante quantité de données. Il faut cependant garder en tête que l'utilisateur peut effectuer des erreurs de saisies et donc rendre la lecture impossible ou incorrecte. Des contrôles ont été effectuées pour permettre la bonne lecture du fichier de map. Voilà les contrôles du sujet qui étaient nécessaires d'effectuer :

  1. Toutes les lignes nécessaires sont présentes et dans le bon ordre.
  2. Triplet RGB valide pour les couleurs (compris entre 0 et 255).
  3. Fichier image existant.
  4. Les coordonnées des nœuds sont valides (dans l'image).
  5. Existence d'au moins une zone d'entrée et de sortie (cette vérification pourra se faire implicitement lors de la recherche du chemin des ennemis).
  6. Existence d'au moins un chemin entre la zone d'entrée et de sortie (cette vérification pourra se faire implicitement lors de la recherche du chemin des ennemis).
  • Les vérifications 1, 2, 3 ont été effectuées dans le fichier ITD.cpp.
  • Les vérifications 4, 5, 6 ont été effectuées dans le fichier Map.cpp.

Nous avons réutilisé cette logique sur d'autres éléments de notre jeu tels que : les ennemis, vagues, tours, images animées. La volonté est de permettre à l'utilisateur de pouvoir entrer ses données en gérant simplement les fichiers itd ; notre application se charge dynamiquement de récupérer, traiter et afficher l'information. Voici deux exemples de fichiers itd implémentés.

ITD
#NOMBRE D'ENNEMIS
Enemies 4

#ENEMIES (name, id, health, speed, damage)

type X-Wing 0 40 10 0.5
type Y-Wing 1 70 12 0.75
type A-Wing 2 50 17 1
type Falcon 3 100 20 2
ITD
#NOMBRE DE VAGUES
Waves 2


#VAGUES (niveau, nb d'entrée, nb d'ennemi,
temps btw spawn (s), types ennemis)

level 0 2 5 3 0 1
level 1 4 10 2 0 1 2 3

Ces fichiers représentent les différents types d'ennemis créés et le nombre de vagues que contient le jeu. On remarque que les vagues prennent une clé secondaire liée à la clé primaire des ennemis, permettant ainsi de faire le lien.

Les fichiers itd optionnels implémentés n'ont cependant pas été "sécurisés" dans la saisie de données.

🗾 Carte

Schema : 15x15 px Carte : 240x240 px
Schéma Carte

➡️ Zones :
⚪ = Chemin (path)
🔴 = Entrée (in)
🔵 = Sortie (out)
🟣 = Interdit (forbidden)
⚫ = Libre (void)

⚠️ La carte finale est bien découpée en tiles. Nous avons fait le choix d'un point de vu esthétique, de rendre transparente les 🟣 & ⚫ pour pouvoir afficher une map totalement designé en fonction de ces zones, et donc d'avoir un environnement plus détaillé et varié.

La logique de la carte est implémentée dans une structure Map.

Cette structure est d'une grande importance dans la suite du développement de notre jeu. En effet, le découpage des tiles sur la carte représente une bonne base pour l'implémentation des déplacements des ennemis et le positionnement des tours. En d'autres termes, une structure Map solide et maintenable est indispensable.

Notre carte se base sur une image de référence appelée par la suite schema. Cette image est composée de pixels de couleurs différentes, représentant chacun une information déterminante pour la suite.

Par exemple : si vous voulez construire une Map de 15x15 cases, alors le schema à analyser sera de 15x15 (véritables) pixels.

Ce schema est lu dans un fichier ITD (Image Tower Defense) du type : "mon_schema_15x15.itd". Ce fichier contient :

  • Le chemin menant à l'image du schema.
  • Les couleurs présentes sur l'image.
  • Tous les nœuds des chemins (node) avec leur indice, position et connexions.

L'objectif est maintenant de déterminer comment analyser ce fichier pour obtenir une véritable carte !

Du pixel à la tile !

Une cellule ou case de la carte possède un "squelette" déterminé par Pixel.

On détermine une structure Pixel qui possède :

  • Une position (x, y).
  • Une couleur (structure qui prend le triplet de couleur R, G, B).
  • Un ou plusieurs types ou états (booléens).
  • Des connexions (pointeurs sur les 4 voisins du pixel).

alt text

Après avoir déterminé nos structures de base, on va analyser et attribuer à chaque pixel sur le schema (présent dans l'ITD) une structure Pixel. Pour ce faire, on va récupérer les données concernant les couleurs et les nodes de l'ITD, pour déterminer le type du pixel !

Toute cette analyse se fait dans le Game::SETUP(...) et plus précisemment dans le setup_MAP(...) qui prend en paramètre le nom du fichier ITD et le nombre de pixels sur la largeur ou hauteur (peu importe car notre map est carré).

alt text

Nous ne rentrerons pas dans le détail de ces fonctions, retenez juste que toutes les données sont stockées dans les vector ci-dessous grâce aux appels ordonnés et successifs des méthodes.

  struct Map {
  std::vector<Tile> TILES;
  std::vector<Pixel> PIXELS;
  std::vector<Node> NODES;
  Graph::WeightedGraph GRAPH;
  std::vector<std::vector<Node>> SHORTER_PATH_LIST;
  ...
  }

A partir de là, nous possédons toutes les Tile de notre carte. Le prochain objectif est de déterminer comment afficher correctement les tiles pour obtenir la carte.

Afficher la carte !

Si on essaye de dessiner un quad en (0,0), on obtient une tile dessinée au centre de notre fenêtre. Or, dans la logique de notre carte, on veut que la case (0,0) soit celle tout en bas à gauche. Ainsi, comment passer d'un repère à un autre ? Comment convertir les 4 coins de notre quad ?

Méthode & illustration :

alt text

Une fois les formules de conversions obtenues, on peut utiliser glVertex et dessiner la tile en bouclant sur le tableau TILES contenu par notre structure Map.

On obtient finalement notre carte !

🚀 Ennemis

Il existe dans notre jeu, 4 types d'ennemis différents :

Ennemis

Un ennemi est caractérisé par une structure : Enemy

alt text

Celui-ci a pour objectif d'infliger des dégâts à la base impériale défendue par le joueur. Les ennemis ont des caractéristiques différentes en fonction de leur type (vitesse, dégâts, points de vie, récompense).

Chaque ennemi parcourt le plus court chemin entre son point de spawn et la base à attaquer. Le spawn est déterminé aléatoirement.

Déplacement

Nous allons nous intéresser à l'implémentation du déplacement de l'ennemi.

alt text

L'idée, c'est de définir une current position qui représente le point de départ de l'ennemi (au début). Ce point de départ représente le premier noeud du chemin. On définit également une target position qui sera la position ciblée par l'ennemi. Au départ, la position cible est le noeud qui suit le current.

On définit également un step selon x et y d'une valeur de +1 ou -1. Ce step détermine si on se déplace vers la droite (step_x = 1) ou vers la gauche (step_x = -1), mais aussi si on se déplace vers le bas (step_y = -1) ou vers le haut (step_y = 1). Ces valeurs permettent également de déclencher la texture correspondant à l'orientation de l'ennemi.

On doit garder à l'esprit que la fonction ci-dessus est appelée en boucle dans App. Ainsi, nous n'avons pas besoin d'utiliser de boucle, les conditions suffisent car le rappel successif de notre fonction nous donne un effet de récursion. Si on arrive à mettre à jour nos positions current et target, c'est gagné.

En effet, quand la position relative de l'ennemi n'est pas égale à la target, on incrémente la position en fonction du step, de sa vitesse et du temps.

⚠️ this->TIME = elapsedTime, mesure le temps écoulé entre chaque frame ➡️ permet la fluidité de l'animation et fixe l'indépendance du framerate.

Si l'ennemi atteint la target, alors il est au noeud suivant, current devient target et on incrémente l'id de la target pour aller chercher le prochain noeud cible.

Ainsi, notre ennemi arrive à se déplacer correctement !

L'ennemi inflige des dégats à la Base s'il arrive à l'atteindre. Il suffit simplement de comparer si les positions respectives de l'ennemi et de la base coincident. On décrémente alors la vie de la base en fonction des dégats causés par l'ennemi et on fait disparaitre ce dernier.

🌊 Vagues

Une vague possède un certain nombre d'ennemis. Plus les vagues s'enchaînent et plus la difficulté doit augmenter. Ainsi, le nombre d'ennemi augmente, le temps écoulé entre chaque spawn ennemi diminue. Ces paramètres sont évidemment ajustables dans le fichier ITD.

On utilise une structure : Wave

alt text

C'est une petite structure. Elle prend :

  • un nombre de spawn
  • un nombre d'ennemi
  • un temps de spawn entre chaque ennemi
  • un tableau contenant le type des ennemis contenu dans la vague

Comme vous pouvez le constater, ce n'est pas dans la structure Wave que nous récupérons les Enemy dans un tableau. Ce processus est effectué plus haut, dans la structure TowerDefense.

alt text

C'est ici que sont stockés les différentes vagues et les différents ennemis dépendants eux-mêmes d'une vague.

alt text

  1. setup_WAVE() permet de récupérer la current Wave grâce au current_WAVE_id initialisé à 0. Cet id est envoyé dans un tableau WAVES_checked permettant de stocker les vagues qui ont été chargées.
  2. get_ENEMIES_into_WAVE() permet de récupérer les ennemis de la vague en court grâce au tableau number_of_ENEMIES. On récupère aléatoirement l'id des ennemis. L'id permet de récupérer la structure de l'ennemi correspondante grâce au ENEMIES_ITD qui détient toutes les propriétés de chaque type d'ennemi. On insert ensuite dans le current_ENEMIES_in_WAVE la structure de l'ennemi.
  3. setup_ENEMIES_in_WAVE() permet d'initialiser toutes les caractéristiques propres aux ennemis comme leur texture, vitesse, chemin suivi (déterminé aléatoirement).

alt text

update_ENEMIES_in_WAVE() permet comme son nom l'indique de mettre à jour l'état des ennemis dans la vague (spawn, inflammabilité), cette partie du code n'est pas développé dans le rapport. On se concentre ici sur le passage d'une vague à la suivante. Les 3 fonctions développées précédemment sont utilisées dans cette fonction de mise à jour. On vérifie si l'id de la Wave est présent dans le WAVES_Checked, si ce n'est pas le cas, on appelle les fonctions 1,2,3. Si current_ENEMIES_in_WAVE est vide, alors on donne la possibilité au joueur de passer à la vague suivante en appuyant sur Entrée.

🗼 Tours

Il y a 3 types de tours que le joueur peut placer dans les endroits alloués :

  • Tour de base : pas chère, peu puissante mais avec une bonne cadence.
  • Tour de ralentissement : moyennement chère, ralentit les ennemis.
  • Tour destructrice : chère, puissante mais avec une cadence inférieure.

Dans la structure Tower, on retrouve une autre structure membre Bullet. En effet, chaque tour a une balle qui reçoit les propriétés de la tour pour adopter un certain comportement.

Mais à quel moment tirer les balles ?

On boucle une fois sur les ennemis (grâce au booléen lockedEnemy) pour calculer la distance entre la tour et chaque ennemi (en Distance de Chebyshev). La variable closest_enemy_dist stockera la plus petite distance et closest_enemy l'ennemi associé (donc le plus proche) sous la forme d'un pointeur.

if(!lockedEnemy)
{
  lockedEnemy = true;

  for (auto &enemy : TD->current_ENEMIES_in_WAVE)
  {
      if (enemy.second.isTarget)
      {
          // Distance de Chebyshev
          float dist_to_enemy = std::max(std::abs(pos.x - enemy.second.pos.x), std::abs(pos.y - enemy.second.pos.y));

          if (dist_to_enemy < this->portee)
          {
              if(dist_to_enemy < closest_enemy_dist)
              {
                  closest_enemy_dist = dist_to_enemy;
                  closest_enemy = &enemy.second;
              }
          }
      }
  }
}

Plus bas, on update la balle en lui donnant en paramètre la valeur déréférencée de closest_enemy, alias l'ennemi le plus proche.

if(closest_enemy_dist < this->portee)
{
    if(closest_enemy->isMoving && closest_enemy != nullptr)
    {
        this->bullet.update(*closest_enemy, elapsedTime, currentTime, this);
        this->bullet.isBeingShot = true;
    }
    else
        this->bullet.isBeingShot = false;
}

À la fin des n secondes relatives à la cadence de tir, on réinitialise toutes les variables à l'état initial pour pouvoir tirer une nouvelle balle sur l'ennemi le plus proche recalculé.

if (cadence < 0)
{
    this->bullet.pos = this->pos;
    cadence = 3;
    this->bullet.fixedDirection = false;
    this->bullet.hitEnemy = false;
    this->bullet.isBeingShot = false;
    lockedEnemy = false;
    closest_enemy_dist = 100;
}

🔴 Bullets

Le comportement des balles est principalement régi par les vec2 direction et pos.

Expliquons leur utilisation :

  • direction : calcule les coordonnées du vecteur entre la tour mère et l'ennemi le plus proche. Pour cela, on fait simplement {x2-x1, y2-y1}. On a aussi un booléen fixedDirection qui régule le fait de ne calculer la direction que toutes les n secondes relatives à la cadence de tir de la tour.
if (!fixedDirection)
{
    direction = {enemy.pos.x - pos.x, enemy.pos.y - pos.y};
    fixedDirection = true;
}
  • pos : une fois la direction calculée, on update la position de la balle en lui ajoutant les coordonnées du vec2 direction et en lui multipliant une constante qui représente la vitesse.
this->pos.x += direction.x * elapsedTime * 4;
this->pos.y += direction.y * elapsedTime * 4;

Par ailleurs, quand la balle touche un ennemi, sa valeur pos devient quelque part hors écran, en attendant d'être réinitialisée dans la position de la tour et tirée à nouveau.

pos = {1000, 1000};

PS : parce qu'on avait besoin d'inclure les structures Tower et Bullet les unes dans les autres, nous avons utilisé la méthode de la déclaration anticipée pour pallier les problèmes de link.

struct Tower;

struct Bullet
{
    // GLuint texture;
    SpriteSheet sprite;
    glm::vec2 pos;
    glm::vec2 direction;
    bool fixedDirection{false};
    bool isBeingShot{false};
    bool hitEnemy{false};
    void setup(std::unordered_map<std::string, SpriteSheet> &SPRITE_SHEETS_ITD, Tower*);
    void update(Enemy&, const double &elapsedTime, const double &currentTime, Tower*);
    void render(Map &);
};

Tower.hpp est ensuite inclus dans Bullet.cpp (implicitement derrière TowerDefense.hpp).

#include "Bullet.hpp"
#include "TowerDefense.hpp"

📺 UI

Le joueur a la possibilité d'intéragir avec l'application, grâce notamment aux 3 fonctions de callback mentionnées au début du rapport. Il a aussi de nombreuses informations à sa disposition lui permettant de comprendre le fonctionnement et déroulement du jeu.

alt text

Elements visibles à l'écran :

  • Argent disponible.
  • Vague en court.
  • Point de vie de la base.
  • Curseurs intuitifs.

Exemples d'intéractions :

  • Possibilité de lancer le jeu, de mettre en pause et de quitter.
  • Accès lors de la mise en pause aux caractéristiques des ennemis et tours.
  • Possibilité de sélectionner une tour spécifique en fonction de l'argent disponible et de placer celle-ci sur la carte si la case est valide.
  • Cliquer sur une tour ou un ennemi pour voir les caractéristiques.

🎶 Son

Afin d'avoir la meilleure expérience utilisateur, il est nécessaire de manipuler l'ambiance sonore du jeu. Pour cela, voici comment on a procédé :

CMakeLists.txt

# ---miniaudio---
FetchContent_Declare(
    miniaudio
    GIT_REPOSITORY https://github.com/mackron/miniaudio
)
FetchContent_MakeAvailable(miniaudio)
add_library(miniaudio INTERFACE)
target_include_directories(miniaudio SYSTEM INTERFACE ${miniaudio_SOURCE_DIR})
target_link_libraries(${PROJECT_NAME} PRIVATE miniaudio)
  • Pour gérer l'intégration de la bibliothèque Mini-Audio dans notre projet, nous avons décidé de passer par CMake en téléchargeant la librairie depuis le dépôt GitHub et en configurant les directives d'inclusion et de liaison nécessaires.

SoundEngine.hpp

#pragma once

#include "miniaudio.h"
#include <vector>

class SoundEngine {
    public:
        static SoundEngine& GetInstance();
        static ma_engine& GetEngine();

        SoundEngine(SoundEngine const& other) = delete;
        SoundEngine& operator=(const SoundEngine &) = delete;

    private:
        SoundEngine();
        ~SoundEngine();

        ma_engine _sound_engine;
};
  • Utilisation d'un fichier SoundEngine.hpp qui définit la classe SoudEngine sous forme de singleton. Grâce à l'aide de notre professeur, nous avons pu utiliser ce singleton comme un outil. En effet, un singleton est un design pattern qui garantit qu'une classe n'aura qu'une seule instance à tout moment, offrant ainsi un point d'accès global à cette instance. Cela a été particulièrement utile pour les composants du moteur sonore qui ne nécessitent qu'une seule instance partagée dans tout le programme.

SoundEngine.cpp

#define MINIAUDIO_IMPLEMENTATION
#include "SoundEngine.hpp"
#include <iostream>

SoundEngine::SoundEngine() {
    ma_result const result { ma_engine_init(NULL, &_sound_engine) };
    if (result != MA_SUCCESS) {
        std::cerr << "Unable to init sound engine" << std::endl;
    }
}

SoundEngine::~SoundEngine() {
    ma_engine_uninit(&_sound_engine);
}

SoundEngine& SoundEngine::GetInstance() {
    static SoundEngine instance;
    return instance;
}

ma_engine& SoundEngine::GetEngine() {
    return GetInstance()._sound_engine;
}
  • Utilisation d'un fichier SoundEngine.cpp qui implémente les méthodes de la classe. C'est lui qui permet l'instance du moteur sonore ma_engine de Mini-Audio lors de l'initialisation. De plus, il définit le rôle du destructeur du singleton qui se charge de désinitialiser correctement le moteur sonore.

Exemple d'application

ma_sound mainThemeSound;

// Chargement et stockage des sons dans un vecteur
std::vector<std::pair<std::string, ma_sound *>> sounds = {
    {"../../sound/Main_Theme.mp3", &mainThemeSound}
};

// Initialisation du volume sonore
ma_engine_set_volume(&SoundEngine::GetEngine(), 0.1f);

// Démarrage de la lecture du son
ma_sound_start(&mainThemeSound);
  • Voici un exemple concret d'utilisation présent dans le fichier Game.cpp. Ici, on remarque que les sons sont lancés grâce aux méthodes déjà présentes dans la librairie, en manipulant particulièrement : ma_sound qui permet de gérer directement les sons sans toucher le moteur sonore qui s'initialise directement à l'appel du fichier.

3️⃣ Améliorations

Cette partie a pour objectif de mentionner les améliorations effectuées par rapport aux règles imposées par le sujet du projet.

✅ Zones constructibles pour les tours.
✅ Placement intelligent des sprites de chemin.
✅ Sprites animées.
✅ Différents types d'ennemis avec des caractéristiques différentes.
✅ Différents types de tours avec des caractéristiques différentes.
✅ Visualisation des tirs des tours sur les ennemis.
✅ Créer une zone de sortie ayant des points de vie, encaissant les dégâts des ennemis avant de perdre la partie.
➕ Créations de toutes les textures du jeu.
➕ Créations d'ITD supplémentaires (enemy, wave, tower, sprite_sheets).
➕ Utilisations de musiques et effets sonores pour renforcer l'ambiance.

4️⃣ Conclusion

Nous sommes tout d'abord fiers de ce projet.

Ce jeu a été réalisé avec passion et nous avons tous les trois énormément appris. Nous ne pensions pas aller aussi loin dans l'implémentation, mais nous avions réellement envie de développer davantage ce projet. Nous tenons à remercier notre professeur de programmation Enguerrand Desmet, qui a été là quand nous avions besoin d'aide, notamment pour l'utilisation de la librairie miniaudio, ainsi que pour les problèmes d'affichage liés à la librairie de texte (le jeu ayant principalement été développé sur macOS avec un écran Retina).

Évidemment, beaucoup de choses peuvent encore être améliorées. On peut notamment noter l'absence de vérifications sur les ITD des vagues, des ennemis, des tours et des spritesheets, bien que ces derniers soient une amélioration de notre part. Certaines fonctions mériteraient d'être optimisées, ou certains choix, comme le fait de ne pas intégrer directement un tableau d'ennemis dans la structure Wave, pourraient être repensés.

Les principales difficultés rencontrées ont été liées à l'organisation des différents fichiers. Comment s'organiser pour que tout soit bien interconnecté et que la structure du projet reste cohérente ? Gérer l'affichage n'a pas été simple, car entre macOS et Windows, ce dernier était différent (lié à la densité de pixels).

Notre objectif a été de produire le code le plus propre possible dans le temps qui nous était imparti, mais surtout de créer un jeu agréable visuellement auquel on prend plaisir à jouer !

Tout n'a pas été expliqué dans ce rapport car cela aurait pris une éternité. N'hésitez pas à explorer par vous-même ! ;)

Que la force soit avec vous !

Léo DE SANTIS
Anass DOUBLAL
Maxence DUPUIS

About

Final project in OpenGL / C++ from my first year of engineering school at IMAC.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C++ 96.4%
  • CMake 3.6%