Les membres ayant 30 points peuvent parler sur les canaux annonces, projets et hs du chat.
La shoutbox n'est pas chargée par défaut pour des raisons de performances. Cliquez pour charger.

Forum Casio - Actualités


Index du Forum » Actualités » L'Atelier de Papy Sly - Part #2 - Présentation en détail du jeu et premier prototype
Slyvtt Hors ligne Maître du Puzzle Points: 2389 Défis: 17 Message

L'Atelier de Papy Sly - Part #2 - Présentation en détail du jeu et premier prototype

Posté le 27/01/2024 23:23

Bonjour mes Petits

Nous nous retrouvons donc réunis une nouvelle fois dans mon atelier pour mener à bien ce deuxième épisode de notre série de tutos. On va pousser un peu le bazar sur le côté, souffler un peu la poussière, déplacer la camomille sur le coin du bureau pour ne pas la renverser sur le clavier, là juste à côté du verre à dents et reprendre notre petit projet là où nous l'avions laissé.

Liens vers l'ensemble des articles de la série
Cliquer pour enrouler
Pour rappel, les liens qui vont bien pour la série [c'est du WIP, donc ce sera complété au fur et à mesure] :

- Introduction de la série
- Tuto #1 - Présentation du fxSDK et du wording associé
- Tuto #2 - Présentation en détail du jeu et premier prototype
- Tuto #3 - On entre dans le vif du sujet, un premier truc qui ressemble vraiment à un jeu
- Tuto #4 - Quand ça va mal, qu'est ce qu'on fait ? Car ça arrive toujours à un moment ou à un autre
- Tuto #5 - On ajoute quelques features avancées


Donc après un longuissime premier tuto qui nous a permis de mettre en place notre base de projet avec le bon paramétrage pour travailler sereinement en C++ et après avoir longuement explicité le rôle des divers outils présents dans fxSDK, nous allons donc pouvoir désormais entamer les choses sérieuses au cours de ce second volet.
Nous allons donc commencer par une rapide première partie destinée à montrer notre cible en termes de gameplay puis poser les bases de la structure de notre jeu en identifiant les différents éléments à mettre en place ainsi que leurs possibles interactions.
Ensuite, nous donnerons une structure à notre projet en créant les fichiers qui contiendront le code source puis nous mettrons en place la boucle principale de jeu avec un début de rendu.
Enfin, nous ferons notre première compilation afin de vérifier que tout fonctionne bien.

Donc aujourd'hui le mot d'ordre est "Réflexion" pour bien architecturer notre projet et ensuite pouvoir avancer à un bon rythme. Cela peut paraître rébarbatif, mais un bon projet commence souvent par cette étape, qui fait gagner énormément de temps par la suite et évite de s'embarquer dans de mauvaises pistes.

Allez, on est partis ?!? ...

Présentation en détail du jeu et premier prototype


Donc comme explicité dans l'article d'introduction, notre objectif est de programmer un jeu de type Tower Defense en C++. Voici une image représentant approximativement la cible que je vous propose de copier

(Crédit image - The Tower - Idle Tower Defense par Tech Tree Games LLC )


Afin de montrer le fonctionnement du jeu, je vous invite à regarder cette très courte vidéo qui vous montre le principe de jeu (attention c'est très très rapide).



Nous allons donc commencer à décortiquer ce que nous devrons mettre en place pour coder ce jeu (en version simplifiée bien entendu, le but n'étant pas de faire un jeu complet dans le cadre de cette série tuto).

a. On liste les différents items du jeu (joueur, ennemis, attaques, niveau de vie, score, ... )

En analysant la vidéo précédente, on peut noter la présence d'un certain nombre d'entités :
La tour et son périmètre à défendre (entité que nous appellerons Tower)
Les ennemis qui foncent vers la tour (entités que nous appellerons Enemy)
Les tirs de la tour vers les ennemis (entités que nous appellerons Bullet)

Donc la base du jeu se fait sur ces 3 entités qui faudra faire interagir. Il y a bien entendu tout un lot de propriétés associées à chacune de ces entités que nous listerons et que nous pourrons faire évoluer au fur et à mesure du développement, mais dans un premier temps, focalisons nous sur ces trois éléments. Si on résume de manière graphique, avec ces 3 entités, on devrait arriver à un truc du genre :



Donnons-nous donc comme objectif du jour de parvenir à obtenir ce résultat sur fxCG et sur fx9860G.

b. On pose la structure du projet avec des fichiers aux bons endroits.

Pour commencer, nous allons créer une structure de fichiers/répertoires propre dans le sous-répertoire source. Créons donc un sous-répertoire entities dans lequel nous déposerons les fichiers nécessaires à la définition des entités vues précédemment. Ensuite, pour chacune de ces entités, nous créons dans ce sous-répertoire un fichier header (format .hpp) et un fichier source (format .cpp) tel que présenté par la capture suivante :



Il nous faut, pour que tout cela soit pris en compte lors de la compilation, ajouter dans le fichier CMakeLists.txt les noms des fichiers source .cpp afin de les compiler. Ceci est réalisé en modifiant les lignes dans la section set(SOURCES) de la manière suivante :
set(SOURCES
src/main.cpp
src/entities/tower.cpp
src/entities/enemy.cpp
src/entities/bullet.cpp

# ...

)

On ajoute donc simplement le lien relatif des fichiers .cpp. Il est à noter que les headers (.hpp) n'apparaissent pas ici mais seront liés au projet directement dans le code que nous écrirons.

Donc désormais, nous avons positionné dans notre arborescence l'ensemble des fichiers nécessaires pour construire notre premier prototype de jeu.

Bien entendu, on va expliciter un peu le rôle de chacun. Voici ce que nous mettrons en place dans chacun des fichiers:
fichier main.cpp : il s'agit du fichier qui contient le point d'entrée de notre programme. Il contient notamment la fonction principale qui gèrera la boucle de jeu principale (ou GameLoop()) que nous allons détailler dans la partie suivante,
fichiers entities/tower.hpp et entities/tower.cpp : il s'agit respectivement des fichiers qui décriront l'entité Tower sous forme d'une classe avec ses propriétés et ses méthodes pour le fichier header et de l'implémentation du code de ses méthodes pour le fichier source,
fichiers entities/enemy.hpp et entities/enemy.cpp : fichiers de description de la classe Enemy et d'implémentation de ses méthodes,
fichiers entities/bullet.hpp et entities/bullet.cpp : vous l'aurez deviné, idem pour la classe Bullet.

Donc d'une manière globale, on aura beaucoup de similitudes dans la structure de ces divers fichiers. Détailllons un peu en prenant l'exemple de l'entité Bullet :
Les fichiers headers .hpp: ils contiennent la description des classes relatives à chacune des entités. Celles-ci contiennent à la fois des propriétés (par exemple les points de vie, le score, la puissance, la position et la vitesse, ...), mais aussi des méthodes (Constructeur et Destructeur, fonctions de dessin et de mise à jour, divers tests de collisions, ...). Pour la classe Bullet le fichier header ressemble à ceci :
#ifndef BULLET_HPP
#define BULLET_HPP

#include <cstdint>

class Bullet {
public:
Bullet(float x, float y); //Constructor
~Bullet(void); //Destructor

void Render(void); // Rendering method
void Update(void); // Update method
void HitDone(void); // Method to mark when an enemy has been hit

// The Strength of the Bullet - to know how much Life it takes to Enemies
uint16_t Power;
// Size of the Bullet for the rendering method
uint16_t Size;
// Coordinates of the Bullet and its speed
float BulletX;
float BulletY;
float Speed;
float DX, DY;
// Marker to know when this bullet can be deleted
bool ToBeDeleted;
// The Color of the Bullet
uint16_t Color;
};

#endif

Les fichiers source .cpp: ils contiennent le code relatif à chacune des méthodes décrites dans le fichier header de l'entité. Il est usuel (et plus propre de ne jamais mettre de code dans le fichier header, mais de toujours déporter le code dans le fichier source correspondant). C'est donc dans ce fichier que l'on va implémenter les lignes de code qui vont permettre de rentrer dans le concrêt. Pour la classe Bullet le fichier source ressemble à ceci :
#include "bullet.hpp"

#include <cmath>
#include <cstdlib>
#include <gint/display.h>

// Constructor of the Bullet object (need a target (X,Y) )
Bullet::Bullet(float TargetX, float TargetY) {
Power = 2;
Speed = 1.0f;

ToBeDeleted = false;

float len = 0.0f;

BulletX = (float)(DWIDTH / 2);
BulletY = (float)(DHEIGHT / 2);

Size = Power;

DX = (TargetX - BulletX);
DY = (TargetY - BulletY);

len = sqrt(DX _ DX + DY _ DY);

DX /= len / Speed;
DY /= len / Speed;

// the color must be chosen depending on the machine (C_BLACK for monochrome
// or any RGB color for Prizm)
#if (FX9860G)
Color = C_BLACK;
#else
Color = C_GREEN;
#endif
}

// Destructor (empty cause neither dynamic allocation nor sub-classes)
Bullet::~Bullet(void) {

}

// Implementation of the rendering method
void Bullet::Render(void) {
dcircle((int)BulletX, (int)BulletY, Size, Color, Color);
}

// Implementation of the update method
void Bullet::Update(void) {
if (ToBeDeleted)
return;

BulletX += DX;
BulletY += DY;

if (BulletX < 0 || BulletX > DWIDTH || BulletY < 0 || BulletY > DHEIGHT)
ToBeDeleted = true;
}

// Implementation of the method called when Enemy is hit by this bullet
void Bullet::HitDone(void) {
ToBeDeleted = true;
}


Avec tous ces éléments, nous avons donc en tête la structure de notre projet ainsi qu'une vision claire du contenu de chacun des fichiers. Nous allons donc passer à la description du contenu de la boucle principale de jeu et du fichier main.cpp.

c. On met en place la "Game Loop" la plus simple possible mais fonctionnelle.

Comme explicité très rapidement dans le précédent chapitre, le but du fichier main.cppest de mettre en place la boucle de jeu principale, ou Game Loop. Il s'agit de la boucle qui sera répétée de manière continue jusqu'à l'ordre de sortie et qui aura pour but d'organiser le bon séquencement des diverses actions à exécuter dans le jeu.

Vu d'un niveau très macroscopique, la boucle de jeu peut se traduire par une boucle tant que (ou boucle while):

Tant que ExitToOs est différent de vrai :
Vérifier si le joueur a demandé une action via le clavier

    Lancer l'Update des Entités

    Afficher les entités sur l'écran

Fin Tant que


Nous allons donc créer diverses méthodes qui seront appelées en boucle par la fonction main et dont le rôle individuel sera de réaliser chacune des trois opérations de la Game Loop:
intercepter les interactions du joueur avec le clavier et lancer les sous-actions correspondantes. Nous appellerons cette fonction GetInputs.
lancer les mises à jour de l'ensemble des entités (changement de position, vérification des collisions, ...). Cette fonction appellera donc entre autres les méthodes Update() de chacune des entités.
lancer les mises à jour de l'ensemble des entités (changement de position, vérification des collisions, ...). Cette fonction appellera donc entre autres les méthodes Render() de chacune des entités.

Il existe deux techniques principales pour effectuer cette boucle de jeu, soit on fait une simple boucle while sans se poser de question, et dans ce cas le processeur va tourner à fond et on aura des FPS qui peuvent être très élevés dans le cas de jeux simples, soit on peut limiter le FPS et mettre le processeur au repos via une commande sleep() lorsque l'on a du temps non consommé entre deux frames. Cette solution est nettement meilleure d'un point du vue consommation des piles, mais il faut néanmoins faire attention au temps alloué pour les opérations entre 2 frames.
Prenons par exemple une cible de fonctionnement à 30FPS, cela signifie qu'entre 2 frames on a 33ms. Toutes les 33ms, on va demander un rendu de la nouvelle frame. Autant dire que si les opérations intermédiaires à exécuter prennent 40ms par exemple, on passera notre temps à être aux fraises et on va faire du grand n'importe quoi. Donc, pour résumer, il faut bien pondérer le plus et le moins de chacune des 2 options.

Boucle sans limitation de FPSBoucle avec limitation de FPS
#include <gint/cpu.h>
#include <gint/display.h>
#include <gint/keyboard.h>

bool ExitToOS = false;

// Function which will call render methods of entities
void Render(void) {
dclear(C_WHITE);

// TODO : add rendering stuff here !!!

dupdate();
}

// Function which will call update methods of entities
void Update(void) {

// TODO : add update stuff here !!!

}

// Function which will check user inputs
void GetInputs(void) {

cleareventflips();
clearevents();

// EXIT KEY is pressed so we change the flag to quit
if (keypressed(KEY_EXIT))
ExitToOS = true;

// TODO : add more inputs
}

// Main function
int main(void) {

// While loop : begining of the Game Loop
// we stop when the flag ExitToOs becomes true
while (!ExitToOS) {

    GetInputs();    // Call to Inputs management function

    Update();       // Call to Update management function

    Render();       // Call to Render management function

}

return 1;
}
#include <gint/cpu.h>
#include <gint/display.h>
#include <gint/keyboard.h>
#include <gint/timer.h>

bool ExitToOS = false;

#define FRAME_RATE 30

// The variable that will be triggered when next frame is necessary
volatile int frame_tick = 1;
int timer_id; // ID of the timer (to be saved to close it later)

// We define the parameter of the timer
// here we will change the value of frame_tick every 33ms (1 000 000 µs / FRAME_RATE)
void SetTimer(void) {

timer_id = timer_configure(TIMER_ANY, 1000000 / FRAME_RATE,
GINT_CALL_SET(&frame_tick));
if (timer_id >= 0)
timer_start(timer_id);
}

// Function to close the timer
void CloseTimer(void) {
if (timer_id >= 0)
timer_stop(timer_id);
}

// Similar to other option
void Render(void) {
dclear(C_WHITE);

dupdate();
}

// Similar to other option
void Update(void) {

}

// Similar to other option
void GetInputs(void) {

cleareventflips();
clearevents();

if (keypressed(KEY_EXIT))
ExitToOS = true;
}

// Main function
int main(void) {

// We initiate the timer
SetTimer();

// While loop : begining of the Game Loop
// we stop when the flag ExitToOs becomes true
while (!ExitToOS) {

    // We have now an internal loop that will put the CPU in sleep mode as long as the value of frame_tick is 0
    // every 33ms, frame_tick will be set at 1 and then the following actions (GetInput(), Update() and Render())
    // will be performed
    while (!frame_tick)
      sleep();

    GetInputs();    // Call to Inputs management function

    Update();       // Call to Update management function

    Render();       // Call to Render management function

}

// We close the timer
CloseTimer();

return 1;
}


On peut donc voir que les modifications sont assez limitées pour limiter (ou caper) le framerate d'un jeu. Il suffit de passer par un timer que l'on configure de manière adéquate pour changer le contenu d'une variable. Ensuite dans la boucle principale, il reste à mettre le CPU au repos tant que cette variable n'est pas à la valeur demandant le rendu de la prochaine frame.

La boucle de jeu principale est donc désormais terminée. On va rajouter un peu de code dans les méthodes des diverses entités (je vous laisse le lire et me poser des questions si besoin) et complèter les fonctions du fichier main.cpp. Ce dernier devient :
#include "entities/bullet.hpp"
#include "entities/enemy.hpp"
#include "entities/tower.hpp"
#include <algorithm>
#include <cstdlib>
#include <gint/cpu.h>
#include <gint/display.h>
#include <gint/keyboard.h>
#include <gint/timer.h>
#include <vector>

#define FRAME_RATE 30

bool ExitToOS = false;

Tower _MyTower;
std::vector<Enemy _> MyEnemies;
std::vector<Bullet \*> MyBullets;

volatile int frame_tick = 1;
int timer_id;

void SetTimer(void) {

timer_id = timer_configure(TIMER_ANY, 1000000 / FRAME_RATE,
GINT_CALL_SET(&frame_tick));
if (timer_id >= 0)
timer_start(timer_id);
}

void CloseTimer(void) {
if (timer_id >= 0)
timer_stop(timer_id);
}

// Function which will call render methods of entities
void Render(void) {
dclear(C_WHITE);

// We render the Tower
MyTower->Render();

// We render all the Enemies
for (auto &e : MyEnemies)
e->Render();

// We render all the Bullets
for (auto &b : MyBullets)
b->Render();

// Some debug information (TO BE REMOVED)
dprint(1, 1, C_BLACK, "Nb Enemies = %i", MyEnemies.size());
dprint(1, 11, C_BLACK, "Nb Bullets = %i", MyBullets.size());

dupdate();
}

// Function which will call update methods of entities
void Update(void) {

// Update player's tower
MyTower->Update();

// sort vector of Enemies by distance from the tower
// It uses a lambda function that is a bit complex : consider this as a black
// box for now
sort(MyEnemies.begin(), MyEnemies.end(),
[](const Enemy *lhs, const Enemy *rhs) {
return lhs->DistanceToTower < rhs->DistanceToTower;
});

// Update Bullets
for (auto &b : MyBullets) {
b->Update();
// Check for the current Bullet if hits an Enemy
for (auto &e : MyEnemies)
if (e->TestImpact(b)) {
b->HitDone(); // if so HitDone will mark it as "ToBeDeleted"
}
}

// Update Enemies
for (auto &e : MyEnemies)
e->Update();

// Remove Enemies killed
// It uses a lambda function that is a bit complex : consider this as a black
// box for now
MyEnemies.erase(
std::remove_if(MyEnemies.begin(), MyEnemies.end(),
[](const Enemy \*lhs) { return lhs->ToBeDeleted; }),
MyEnemies.end());

// remove Bullets that hit an Enemy
// It uses a lambda function that is a bit complex : consider this as a black
// box for now
MyBullets.erase(
std::remove_if(MyBullets.begin(), MyBullets.end(),
[](const Bullet \*lhs) { return lhs->ToBeDeleted; }),
MyBullets.end());
}

// Function which will check user inputs
void GetInputs(void) {

cleareventflips();
clearevents();

// EXIT KEY is pressed so we change the flag to quit
if (keypressed(KEY_EXIT))
ExitToOS = true;

// if F1 is pressed, we remove the closest Enemy (this is a test function)
if (keypressed(KEY_F1)) {
MyEnemies[0]->ToBeDeleted = true;
}

// if F2 is pressed we shoot a Bullet to the Closest Enemy
if (keypressed(KEY_F2)) {
Bullet \*b = new Bullet(MyEnemies[0]->EnemyX, MyEnemies[0]->EnemyY);
MyBullets.push_back(b);
}
}

// We create the entities at start
void InitEverything() {
srand(0);

MyTower = new Tower();

for (int i = 0; i < 25; i++) {
Enemy \*E = new Enemy();
MyEnemies.push_back(E);
}
}

// We clear Everything before leaving
void CloseEverything() {
delete MyTower;

for (auto &e : MyEnemies)
delete (e);
MyEnemies.clear();
}

// Main function
int main(void) {

InitEverything();

// We initiate the timer
SetTimer();

// While loop : begining of the Game Loop
// we stop when the flag ExitToOs becomes true
while (!ExitToOS) {

    // We have now an internal loop that will put the CPU in sleep mode as long
    // as the value of frame_tick is 0 every 33ms, frame_tick will be set at 1
    // and then the following actions (GetInput(), Update() and Render()) will
    // be performed
    while (!frame_tick)
      sleep();

    GetInputs(); // Call to Inputs management function

    Update(); // Call to Update management function

    Render(); // Call to Render management function

}

// We close the timer
CloseTimer();

CloseEverything();

return 1;
}


Nous allons désormais peupler le corps de nos diverses méthodes et fonctions. Je ne reviendrai pas sur l'intégralité du code mais me focaliserait sur l'entité Bullet. Les entités Tower fonctionnant globalement de la même manière, vous vous y retrouverez très facilement.

d. On ajoute du code dans les méthodes des entités et dans la boucle principale

Commençons donc à examiner de plus près les méthodes de la classe Bullet. Nous avons les deux méthodes Render() et Update() que nous retrouvons sur l'ensemble des entités ainsi qu'une méthode HitDone() spécifique à cette classe. Les attributs (ou propriétés) de la classe on été vus au tout debut de cet article, je ne les reprendrai donc pas ici et vous invite à relire le paragraphe b.

Allez, nous sommes partis ...

Commençons par les constructeurs et destructeurs de classe.

// Constructor of the Bullet object (need a target (X,Y) )
Bullet::Bullet(float TargetX, float TargetY) {
  Power = 2;
  Speed = 1.0f;

  ToBeDeleted = false;

  float len = 0.0f;

  BulletX = (float)(DWIDTH / 2);
  BulletY = (float)(DHEIGHT / 2);

  Size = Power;

  DX = (TargetX - BulletX);
  DY = (TargetY - BulletY);

  len = sqrt(DX * DX + DY * DY);

  DX /= len / Speed;
  DY /= len / Speed;

  // the color must be chosen depending on the machine (C_BLACK for monochrome
  // or anyRGB color for Prizm)
#if (FX9860G)
  Color = C_BLACK;
#else
  Color = C_GREEN;
#endif
}


Le constructeur doit être appelé avec 2 paramètres de type float correspondants aux coordonnées de la cible (TargetX, TargetY). Sachant que le point d'émission est le centre de la Tour situé au centre de l'écran, on va donc pouvoir affecter ou calculer l'intégralité des valeurs des atttributs avec seulement ces 2 paramètres supplémentaires précisés lors de l'instanciation d'un objet de type Bullet. Les coordonnés de départ (BulletX, BulletY) sont calculées avec les valeurs de taille d'écran issues de gint (DWIDTH et DHEIGHT), la vitesse est donnée à une valeur de 1.0f et la puissance vaut 2. On choisit la taille Size identique à Power dans ce cas, mais on pourrait aussi ajouter des paramètres au constructeur pour paramétriser cela plus finement. Enfin, on calcul les valeurs de DX et DY qui représentent la modification des coordonnées BulletX et BulletY respectivement à chaque appel de la méthode Update().

On remarque une section un peu particulière que vous retrouverez souvent dans vos add-ins fxSDK si vous visez un support multiplateforme :
#if (FX9860G)
  Color = C_BLACK;
#else
  Color = C_GREEN;
#endif

Les directives préprocesseurs #if ( ) .. #else .. #endif permettent de modififier des pans de code avant la compilation au niveau de l'étape de préprocessing. Le préprocesseur a connaissance de la machine cible (FX9860G si monochrome et FXCG50 si couleur) et saura donc censurer les morceaux de code ne se rapportant pas à telle ou telle architecture. Dans notre cas, on se sert de cela pour affecter une couleur verte aux balles si on est sur Prizm ou sinon, on dessine en noir. Malin, n'est il pas ?

Pour créer un objet Bullet, il nous suffera donc de faire par exemple dans notre boucle principale
Bullet *Balle = new Bullet( EnemyX, EnemyY );


Pour le destructeur, c'est vraiment très simple ici, car il n'y a que des membres "statique" (pas de pointeur, pas de "containers", ...) donc pas de variables à libérer manuellement. On peut donc laisser cette fonction vide :
// Destructor (empty cause neither dynamic allocation nor sub-classes/containers)
Bullet::~Bullet(void) {}

Mais attention, ce n'est pas toujours le cas.

Pour appeler le destructeur, il suffit de faire appel dans la boucle principale à delete:
delete Balle;


Nous arrivons désormais aux deux méthodes "génériques" appelées par la boucle principale à chaque rotation :
la méthode Render():
// Implementation of the rendering method
void Bullet::Render(void) {
  dcircle((int)BulletX, (int)BulletY, Size, Color, Color);
}

Il s'agit juste d'un appel à la fonction de dessin d'un cercle plein fournie par gint, cercle centré en (BulletX, BulletY), de rayon Size et de couleur Color (bordure + remplissage).

la méthode Update():
// Implementation of the update method
void Bullet::Update(void) {
  if (ToBeDeleted)
    return;

  BulletX += DX;
  BulletY += DY;

  if (BulletX < 0 || BulletX > DWIDTH || BulletY < 0 || BulletY > DHEIGHT)
    ToBeDeleted = true;
}

Pas beaucoup plus complexe, cette méthode sert à mettre à jour la position de la balle à chaque tour de boucle de jeu. Son rôle est donc de mettre à jour les coordonnées (BulletX, BulletY) avec les incréments calculés dans le constructeur. On a juste 2 tests suplémentaires, le premier qui vérifie si la balle a été marquée précédemment pour destruction et si oui shunte le calcul et enfin, un test de vision pour savoir si la balle est en dehors de l'écran visible et si oui la marque pour destruction au prochain tour.

La dernière méthode HitDone() est appelée si la balle touche un ennemi. Il s'agit en cas de contact de marquer la balle pour destruction tout simplement :
// Implementation of the method called when Enemy is hit by this bullet
void Bullet::HitDone(void) { ToBeDeleted = true; }


Voici pour la description exhaustive des méthodes de la classe Bullet telles que l'on peut les trouver dans le fichier bullet.cpp. Je vais juste détailler une méthode intéressante de la classe Enemy, à savoir la méthode TestImpact :
bool Enemy::TestImpact(Bullet *Shot) {
  if (Shot->BulletX <= EnemyX - Size / 2 ||
      Shot->BulletX >= EnemyX + Size / 2 ||
      Shot->BulletY <= EnemyY - Size / 2 || Shot->BulletY >= EnemyY + Size / 2)
    return false;

  if (Life >= Shot->Power)
    Life -= Shot->Power;
  else
    Life = 0;

  return true;
}

Tout d'abord, on doit tester l'impact entre l'ennemi courant et une balle, il faut donc préciser l'objet Bullet en paramètre de cette fonction (Ne pas oublier de mettre un #include "bullet.hpp" dansle fichier enemy.hpp sinon ça va hurler à la compilation). On cherche ensuite à vérifier si les coordonnées de la balle (qui s'appelle "Shot" dans ce contexte) se superposent à celle de l'ennemi. On fait donc un simple test AABB. Si celui-ci échoue on renvoie false, à savoir pas de contact, sinon on retire de la vie à l'ennemi en fonction de la puissance de la balle et on retourne true à savoir il y a contact.

Si on regarde du côté de la boucle principale, il s'agit désormais de bien piloter ces entités avec les bons calls aux bonnes méthodes et aux bons moments.

On commence par initialiser le jeu via une fonction dédiées :
// We create the entities at start
void InitEverything() {
  srand(0);

  MyTower = new Tower();

  for (int i = 0; i < 25; i++) {
    Enemy *E = new Enemy();
    MyEnemies.push_back(E);
  }
}

On crée un objet de type Tower puis 25 objets de type Enemy que l'on place dans un containers de type std::vector<Enermy *>. Il s'agit d'une feature offerte par le C++ et la librairie standard du C++. Ceci nous permet d'avoir un tableau dynamique donc la taille est automatiquement gérée.

En toute fin de programme, on a le pendant de cette fonction:
// We clear Everything before leaving
void CloseEverything() {
  delete MyTower;

  for (auto &e : MyEnemies)
    delete (e);
  MyEnemies.clear();
}

où on appelle le destructeur de l'objet MyTower et pour le tableau d'Enemy. Attention dans ce cas, il y a une petite subtilité, on commence par détruire tous les objets un par un avec un delete, et ensuite on fait un appel à la méthode clear() du container std::vector<Enermy *>.

Pour la fonction de rendu, rien de compliqué, mais regardons la fonction Update():

void Update(void) {

  // Update player's tower
  MyTower->Update();

  // sort vector of Enemies by distance from the tower
  // It uses a lambda function that is a bit complex : consider this as a black
  // box for now
  sort(MyEnemies.begin(), MyEnemies.end(),
       [](const Enemy *lhs, const Enemy *rhs) {
         return lhs->DistanceToTower < rhs->DistanceToTower;
       });

  // Update Bullets
  for (auto &b : MyBullets) {
    b->Update();
    // Check for the current Bullet if hits an Enemy
    for (auto &e : MyEnemies)
      if (e->TestImpact(b)) {
        b->HitDone(); // if so HitDone will mark it as "ToBeDeleted"
      }
  }

  // Update Enemies
  for (auto &e : MyEnemies)
    e->Update();

  // Remove Enemies killed
  // It uses a lambda function that is a bit complex : consider this as a black
  // box for now
  MyEnemies.erase(
      std::remove_if(MyEnemies.begin(), MyEnemies.end(),
                     [](const Enemy *lhs) { return lhs->ToBeDeleted; }),
      MyEnemies.end());

  // remove Bullets that hit an Enemy
  // It uses a lambda function that is a bit complex : consider this as a black
  // box for now
  MyBullets.erase(
      std::remove_if(MyBullets.begin(), MyBullets.end(),
                     [](const Bullet *lhs) { return lhs->ToBeDeleted; }),
      MyBullets.end());
}


Il y a quelques petites portions de code qui sont un peu plus velues, notamment pour retirer les éléments qui ont été marqués pour destruction qui utilisent des fonctions lambda. Ne vous posez pas trop de questions, et considérez ces quelques portions comme des boîtes noires. Il s'agit de morceaux de code destinés à retirer les projectiles et les enemis marqués pour destruction des containeurs de type std::vector< >. La synthaxe du C++ pouvant être parfois un peu rustre, ne vous tracassez donc pas avec cela.

Pour le reste, il s'agit de calls aux méthodes Update() de chaque entité.

On regarde aussi si il y a collision entre les balles et les ennemis dans cette partie :
// Update Bullets
  for (auto &b : MyBullets) {
    b->Update();
    // Check for the current Bullet if hits an Enemy
    for (auto &e : MyEnemies)
      if (e->TestImpact(b)) {
        b->HitDone(); // if so HitDone will mark it as "ToBeDeleted"
      }
  }

et si contact il y a, on marque la balle pour destruction.

Enfin, les balles et ennemis marqués pour destruction sont retirés des containers C++ afin de ne plus avoir à les traiter au tour suivant.

Nous avons donc désormais vu l'ensemble du code utile et on va vérifier que tout fonctionne correctement.


e. On fait notre toute première compilation et quelques premiers tests

La compilation se fait comme explicitée lors du premier volet via un fxsdk build-cg ou fxsdk build-fx selon si vous viser de faire une version fxCG ou fx9860G. Logiquement tout devrait se passer sans encombre et vous pourrez obtenir le résultat de cette vidéo (test du fichier add-in en g3a sur l'émulateur de fxCG50 de Heath123) :



Voici le résultat détaillé de la compilation pour fxCG en mode verbeux. Même pas un petit warning, on est bons là les amis



Voilà qui en termine donc avec ce deuxième épisode de notre série de tutos. On commence d'avoir une base qui ressemble à quelque chose de propre. On poursuivra dans les prochains volets en mettant un peu plus d'automatisme, en ajoutant quelques menus et en affinant un peu le gameplay.

Vous trouverez comme d'habitude dans le fichier ZIP attaché une copie du projet dûment configuré et fonctionnel. N'hésitez pas à poster vos questions et/ou vos commentaires. Je tâcherai de répondre à tous vos posts.

On se retrouve donc très rapidement pour le prochain épisode de cette série de tutos, gardez un œil bien ouvert sur la page d'accueil et guettez bien le retour de Papy Sly.

C'est pas tout ça, je vais retourner me faire ma camomille du soir et mettre mes charentaises avant de m'installer dans mon rocking chair ...



A très bientôt mes Petiots

Papy Sly


Edit du 28/01/2023 : Ajout d'un paragraphe détaillant le contenu des méthodes + boucle principale.

Fichier joint


Ptitjoz Hors ligne Membre Points: 264 Défis: 10 Message

Citer : Posté le 08/02/2024 12:47 | #


Merci
Les 2 versions fonctionnent.
Juste dû faire un chmod +x sur chacun.
Ça affiche bien les 2 colonnes comme dans ton exemple atelier #1
Mais si je clique sur une touche ça crash mais cest peut être normal ?
Un peu poète, un peu geek, un peu rêveur, un peu écolo.
https://joz.alwaysdata.net/info/

Tuper4 Hors ligne Membre Points: 889 Défis: 19 Message

Citer : Posté le 08/02/2024 13:04 | #


Oui car a la fin du code il y a un getkey() qui attend que tu appuie sur une touche pour continuer. Et comme il n'y a rien après, ça quitte . Tu peux l'essayer sur ta calculatrice physique si tu veux et ça ferra la même chose
When the doorbell rings at three in the morning, it’s never good news. -Anthony Horowitz
Ptitjoz Hors ligne Membre Points: 264 Défis: 10 Message

Citer : Posté le 08/02/2024 14:09 | #


certes ; mais je pensais que ça allait fermer proprement l'émulateur et ne pas me renvoyer une Erreur de segmentation.
Un peu poète, un peu geek, un peu rêveur, un peu écolo.
https://joz.alwaysdata.net/info/

Tuper4 Hors ligne Membre Points: 889 Défis: 19 Message

Citer : Posté le 08/02/2024 14:10 | #


Une Erreur de ségmentation? Ça ce n'est pas normal. Tu peux faire une capture d'écran ou pas?
When the doorbell rings at three in the morning, it’s never good news. -Anthony Horowitz
Slyvtt Hors ligne Maître du Puzzle Points: 2389 Défis: 17 Message

Citer : Posté le 08/02/2024 14:20 | #


J'ai ça à chaque fois moi aussi, tous mes essais avec l'émulateur se terminent par une SEGFAULT aussi.
J'avoue que j'ai pas investigué plus que ça.
There are only 10 types of people in the world: Those who understand binary, and those who don't ...
Ptitjoz Hors ligne Membre Points: 264 Défis: 10 Message

Citer : Posté le 08/02/2024 14:25 | #



et dès que je clique sur un bouton :

joz@emachines:~/Developpement/Casio$ ./calcemuQT TutosPapySly/TowerDefense/Tdefense.g3a
Erreur de segmentation
joz@emachines:~/Developpement/Casio$

Un peu poète, un peu geek, un peu rêveur, un peu écolo.
https://joz.alwaysdata.net/info/

Slyvtt Hors ligne Maître du Puzzle Points: 2389 Défis: 17 Message

Citer : Posté le 08/02/2024 14:29 | #


oui c'est à priori "normal", j'ai exactement la même chose.
Ca ne semble pas provenir du code de ton addin, il doit y avoir dans l'émulateur des trucs louches en fin de run de celui-ci.

Pour info, une erreur de segmentation correspond à un accès dans un mauvais segment de mémoire. Autrement dit, on accède à une adresse que le process ne connaît pas. C'est typique des débordements de tableaux, pointeurs non ou mal initialisés, etc.
il faudrait regarder le code de CalcEmu pour voir si en sortie de programme il y a pas un truc louche qui traine.

Mais tu peux globalement te servir de l'émulateur sans problème.
There are only 10 types of people in the world: Those who understand binary, and those who don't ...
Le_masque Hors ligne Membre Points: 87 Défis: 0 Message

Citer : Posté le 20/02/2024 06:37 | #


Salut ! A quand la suite ? je suis en manque
Slyvtt Hors ligne Maître du Puzzle Points: 2389 Défis: 17 Message

Citer : Posté le 20/02/2024 08:42 | #


Dans qq temps car là j'ai à peu près 0 temps libre vu les heures que je fais au taf.
Faut être un peu patient
There are only 10 types of people in the world: Those who understand binary, and those who don't ...
Ptitjoz Hors ligne Membre Points: 264 Défis: 10 Message

Citer : Posté le 20/02/2024 12:01 | #


Pas de soucis prends ton temps.
Déjà que je ne comprends pas tout , voire pas grand chose, de marcher lentement me va très bien.
Un peu poète, un peu geek, un peu rêveur, un peu écolo.
https://joz.alwaysdata.net/info/

Tuper4 Hors ligne Membre Points: 889 Défis: 19 Message

Citer : Posté le 20/02/2024 12:04 | #


Ptitjoz a écrit :
Pas de soucis prends ton temps.
Déjà que je ne comprends pas tout , voire pas grand chose, de marcher lentement me va très bien.
Qu'est ce que tu ne comprends pas bien? Le C++?
When the doorbell rings at three in the morning, it’s never good news. -Anthony Horowitz

LienAjouter une imageAjouter une vidéoAjouter un lien vers un profilAjouter du codeCiterAjouter un spoiler(texte affichable/masquable par un clic)Ajouter une barre de progressionItaliqueGrasSoulignéAfficher du texte barréCentréJustifiéPlus petitPlus grandPlus de smileys !
Cliquez pour épingler Cliquez pour détacher Cliquez pour fermer
Alignement de l'image: Redimensionnement de l'image (en pixel):
Afficher la liste des membres
:bow: :cool: :good: :love: ^^
:omg: :fusil: :aie: :argh: :mdr:
:boulet2: :thx: :champ: :whistle: :bounce:
valider
 :)  ;)  :D  :p
 :lol:  8)  :(  :@
 0_0  :oops:  :grr:  :E
 :O  :sry:  :mmm:  :waza:
 :'(  :here:  ^^  >:)

Σ π θ ± α β γ δ Δ σ λ
Veuillez donner la réponse en chiffre
Vous devez activer le Javascript dans votre navigateur pour pouvoir valider ce formulaire.

Si vous n'avez pas volontairement désactivé cette fonctionnalité de votre navigateur, il s'agit probablement d'un bug : contactez l'équipe de Planète Casio.

Planète Casio v4.3 © créé par Neuronix et Muelsaco 2004 - 2024 | Il y a 137 connectés | Nous contacter | Qui sommes-nous ? | Licences et remerciements

Planète Casio est un site communautaire non affilié à Casio. Toute reproduction de Planète Casio, même partielle, est interdite.
Les programmes et autres publications présentes sur Planète Casio restent la propriété de leurs auteurs et peuvent être soumis à des licences ou copyrights.
CASIO est une marque déposée par CASIO Computer Co., Ltd