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 En ligne Maître du Puzzle Points: 2414 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


1, 2 Suivante
Tituya Hors ligne Administrateur Points: 2156 Défis: 26 Message

Citer : Posté le 28/01/2024 00:42 | #


Merci pour ce tuto encore une fois très intéressant !

Je pense qu'il faudrait un peu plus d'explication sur la création d'une entité à proprement parler. Actuellement on passe de rien du tout à un début de jeu plutôt convaincant, je trouve la marche un peu grosse pour un débutant
Comprendre les mécanismes que t'as implémenter pour faire bouger les entités, peut être un peu plus d'explication sur le code de la classe Bullet qui est intéressante ?

Ce n'est que mon avis, je ne sais pas s'il est partagé
Bretagne > Reste du globe
(Et de toute façon, vous pouvez pas dire le contraire)
Projet en cours : Adoranda

Mes programmes
Hésite pas à faire un test !


Slyvtt En ligne Maître du Puzzle Points: 2414 Défis: 17 Message

Citer : Posté le 28/01/2024 06:58 | #


M’étant moi même posé la question ta remarque est donc justifiée. Je rajouterai un chapitre avant le chapitre d) pour donner plus de détails.
Merci pour ton retour constructif Tituya.
There are only 10 types of people in the world: Those who understand binary, and those who don't ...
Slyvtt En ligne Maître du Puzzle Points: 2414 Défis: 17 Message

Citer : Posté le 28/01/2024 16:55 | #


Voilà qui est fait, j'ai ajouté un paragraphe qui explique le code dans la classe Bullet et Enemy + détail des fonctions de la boucle de jeu principale.

Bon code et hésitez pas à me faire vos retours, si ça vous plaît ou pas et/ou si je vais trop rapidement ou si c'est OK pour l'assimilation.

Je pense que ce sera plus light niveau périodicité des publications car semaine de déplacements professionnels cette semaine donc je pourrai pas sortir 3 tutos en 1 semaine
There are only 10 types of people in the world: Those who understand binary, and those who don't ...
Tuper4 Hors ligne Membre Points: 889 Défis: 19 Message

Citer : Posté le 28/01/2024 17:17 | #


Bravo Slyvtt! Je vais surement passer un coup d'œil pour mes vidéo youtube
When the doorbell rings at three in the morning, it’s never good news. -Anthony Horowitz


ano Invité

Citer : Posté le 28/01/2024 17:55 | #


La marche est haute pour les débutants. Je me demande comment papy Sly fait pour les monter...
Merci pour ces tutos
Slyvtt En ligne Maître du Puzzle Points: 2414 Défis: 17 Message

Citer : Posté le 28/01/2024 17:58 | #


Merci pour ton retour ano, hésite pas à préciser les zones où c'est compliqué de ton point de vue, le but est que cela soit compréhensible et abordable par le plus grand nombre possible.
There are only 10 types of people in the world: Those who understand binary, and those who don't ...
Tituya Hors ligne Administrateur Points: 2156 Défis: 26 Message

Citer : Posté le 28/01/2024 18:05 | #


Il faut idéalement avoir des connaissances en C++ et en POO pour comprendre correctement

De mon point de vue c'est assez clair comme ça, il s'agit d'une très bonne base commune pour n'importe quel add-in !
Bretagne > Reste du globe
(Et de toute façon, vous pouvez pas dire le contraire)
Projet en cours : Adoranda

Mes programmes
Hésite pas à faire un test !


Farhi Hors ligne Membre Points: 1380 Défis: 0 Message

Citer : Posté le 29/01/2024 00:41 | #


Salut Slyvtt !
Félicitations pour ton excellent travail et cette brillante initiative !
J'ai une proposition à te soumettre en lien avec tes "Ateliers de Papy Sly".
Que dirais-tu de réaliser un hors-série un peu spécial ?
Actuellement, je suis assez débordé, et les semaines à venir s'annoncent encore plus chargées. Cela m'embêterait de ne rien publier pendant trop longtemps, c'est pourquoi je souhaite publier mon moteur 3D (celui de Zelda TOTN) avant la fin de la semaine afin d'en faire profiter toute la communauté.
J'aimerais créer un tutoriel clair et soigné pour qu'il soit facilement utilisable, mais je ne suis pas très doué pour la rédaction. C'est pourquoi j'ai pensé à toi, sachant que tu rédiges très bien les articles, pour une collaboration sur un sujet dédié au fonctionnement du moteur 3D.
Donc, si tu acceptes, cela devrait se faire avant la fin de la semaine, sinon ma prochaine publication ne sera pas avant au moins 2 mois.
Contacte-moi par Discord si tu es d'accord.
"La créativité est contagieuse faites la tourner"
Albert Einstein
Slyvtt En ligne Maître du Puzzle Points: 2414 Défis: 17 Message

Citer : Posté le 29/01/2024 09:26 | #


Salut Farhi, je t'ai répondu sur Discord. @+.
There are only 10 types of people in the world: Those who understand binary, and those who don't ...
Ptitjoz Hors ligne Membre Points: 274 Défis: 10 Message

Citer : Posté le 29/01/2024 11:23 | #


Bonjour Papy Sly

j'aime ton concept narratif d'un papy qui explique à ses petiots ; c'est sympa comme approche ! et mes respects et admiration pour l'investissement et la qualité de tes explications.

Si j'ai bien compris, cet atelier va s'échelonner sur cinq tutos.
Personnellement, je trouve que c'est assez ardu et intense (à mon humble avis). — Il faut préciser que je n'ai lu qu'en diagonale —
J'aurais sans doute préféré avoir beaucoup plus de leçons plus courtes pour mieux les assimiler. Mais si tu as fait ainsi, c'est sûrement pour de bonnes raisons et l'expérience que tu as.

Ce sera un beau défi pour ceux qui iront (ou qui pourront aller) jusqu'au bout de ce défi ces tutos.

Bien à toi
Un peu poète, un peu geek, un peu rêveur, un peu écolo.

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

Citer : Posté le 29/01/2024 11:32 | #


Ptitjoz a écrit :
Bonjour Papy Sly

j'aime ton concept narratif d'un papy qui explique à ses petiots ; c'est sympa comme approche ! et mes respects et admiration pour l'investissement et la qualité de tes explications.

Si j'ai bien compris, cet atelier va s'échelonner sur cinq tutos.
Personnellement, je trouve que c'est assez ardu et intense (à mon humble avis). — Il faut préciser que je n'ai lu qu'en diagonale —
J'aurais sans doute préféré avoir beaucoup plus de leçons plus courtes pour mieux les assimiler. Mais si tu as fait ainsi, c'est sûrement pour de bonnes raisons et l'expérience que tu as.

Ce sera un beau défi pour ceux qui iront (ou qui pourront aller) jusqu'au bout de ce défi ces tutos.

Bien à toi
Je le trouve aussi formidable!

Mais tu n'est pas obligé de les suivre a chaque fois qu'il les postes tu peux commencer par le premier et une fois que tu l'as fini, tu passe au suivant. En gros tu n'es pas obligé de suivre le rythme des tutos postés, enfin c'est mon avis

Et je compte moi aussi me mettre dans les add-ins et je vais faire des vidéos YouTube dessus . Et dont chaque vidéo de la série feront max 5 min. Là, il me reste juste de éditer la première vidéo et la série va commencer! .
Mais le tuto de Slyvtt est beaucoup mieux et je le conseille fortement.
When the doorbell rings at three in the morning, it’s never good news. -Anthony Horowitz
Slyvtt En ligne Maître du Puzzle Points: 2414 Défis: 17 Message

Citer : Posté le 29/01/2024 20:55 | #


Ptitjoz a écrit :
Bonjour Papy Sly

j'aime ton concept narratif d'un papy qui explique à ses petiots ; c'est sympa comme approche ! et mes respects et admiration pour l'investissement et la qualité de tes explications.


Salut Ptitjoz. Merci pour ton commentaire. En effet je trouve sympa de prendre ce rôle de Papy Sly et de faire les tutos un peu dans l'optique de Père Castor qui raconte des histoires. Le but est bien entendu de ne pas se prendre au sérieux.

Ptitjoz a écrit :
Si j'ai bien compris, cet atelier va s'échelonner sur cinq tutos.
Personnellement, je trouve que c'est assez ardu et intense (à mon humble avis). — Il faut préciser que je n'ai lu qu'en diagonale —
J'aurais sans doute préféré avoir beaucoup plus de leçons plus courtes pour mieux les assimiler. Mais si tu as fait ainsi, c'est sûrement pour de bonnes raisons et l'expérience que tu as.


Oui le tutoriel dans son idée de base devrait se dérouler sur 5 épisodes. L'idée première, et c'est pour cela que j'ai un peu rushé sur les 2 premiers épisodes (je ne compte pas le tout premier de la série, l'introduction ou le teaser comme vous voulez), est de pouvoir rapidement amener à un premier prototype qui fonctionne. Ensuite, on verra des améliorations. Il faut trouver un compromis entre quantité de contenu limité et avoir du concret assez rapidement. Qui n'a pas déjà été frustré d'attendre des années pour avoir enfin le squelette entier avec les morceaux dispatchés dans 273 magazines

Mais par contre, en faisant ce choix, j'ai clairement annoncé qu'il faut surtout pas hésiter à me dire quand ça va trop vite, auquel cas je peux ajuster, ou répondre à des questions dans les commentaires, ou si le besoin s'en fait sentir, faire un petit détour par un épisode "hors-série" sur tel ou tel point.

Du coup, n’hésites pas (enfin, n'hésitez pas à me dire ce que vous ne comprenez pas), j'en tiendrais compte au maximum.

Ptitjoz a écrit :
Ce sera un beau défi pour ceux qui iront (ou qui pourront aller) jusqu'au bout de ce défi ces tutos.

Bien à toi


Mon défi à moi est justement d'essayer de ne laisser personne sur le bout de la route et que tous ceux qui veulent s'essayer à la création d'add-ins arrivent à bon port. Je vous promets pas de faire de vous tous des Lephenixnoir, mais je vous promets d'essayer

(Je réfléchis aussi à la possibilité de transcrire les présents tutos en vidéos, qui sont peut-être pour certains un peu plus facile à suivre. L'idée fait son chemin dans ma vieille caboche de Papy )
There are only 10 types of people in the world: Those who understand binary, and those who don't ...
Ptitjoz Hors ligne Membre Points: 274 Défis: 10 Message

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


Bonjour PapySly

tu utilises calcemu pour tester



comment peut-on l'obtenir ? merci
Un peu poète, un peu geek, un peu rêveur, un peu écolo.

Slyvtt En ligne Maître du Puzzle Points: 2414 Défis: 17 Message

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


Salut Ptitjoz (tu me diras si il faut 1 ou 2 "t" dans ton pseudo car du coup je pense qu'il y a un bug)

Tu peux l'obtenir "officiellement" via le github de Heath123 ici https://github.com/Heath123/casio-emu

j'ai fait qq modifs perso pour avoir un mapping plus complet des touches, si besoin ou si problème à la compilation des sources, dis le moi.
Auquel cas je compilerai une version que que mettrai en download sur Planete Casio.

Tiens moi au courant.
There are only 10 types of people in the world: Those who understand binary, and those who don't ...
Tituya Hors ligne Administrateur Points: 2156 Défis: 26 Message

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


Disponible ici : https://github.com/Heath123/casio-emu
Ou réhebergé sur notre gitea https://gitea.planet-casio.com/Slyvtt/casio-emu

Tu as juste à suivre le readme pour l'installer
Bretagne > Reste du globe
(Et de toute façon, vous pouvez pas dire le contraire)
Projet en cours : Adoranda

Mes programmes
Hésite pas à faire un test !


Slyvtt En ligne Maître du Puzzle Points: 2414 Défis: 17 Message

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


Ah oui dis donc j'ai oublié que j'avais forké
Papy Sly commence à gatouiller sévère

La version giteapc intègre le mapping étendu des touches.
There are only 10 types of people in the world: Those who understand binary, and those who don't ...
Ptitjoz Hors ligne Membre Points: 274 Défis: 10 Message

Citer : Posté le 08/02/2024 11:02 | #


Slyvtt a écrit :
Salut Ptitjoz (tu me diras si il faut 1 ou 2 "t" dans ton pseudo car du coup je pense qu'il y a un bug)

au départ, je me suis inscrit avec ptitjoz.
à un moment, j'ai voulu supprimer mon compte et tout compte fait je suis resté... (ça, c'était pour la petite histoire)
et j'ai demandé à ce que mon pseudo soit changé en ptijoz mais apparemment ce n'était pas possible ou compliqué.
Cependant, mon compte gitea est Ptijoz. https://gitea.planet-casio.com/Ptijoz


Slyvtt a écrit :
Tu peux l'obtenir "officiellement" via le github de Heath123 ici https://github.com/Heath123/casio-emu

j'ai fait qq modifs perso pour avoir un mapping plus complet des touches, si besoin ou si problème à la compilation des sources, dis le moi.
Auquel cas je compilerai une version que que mettrai en download sur Planete Casio.

Tiens moi au courant.

Je pense que ce serait mieux d'avoir une version à télécharger qui serait plus facile pour tous ceux qui découvrent ce logiciel (mais tout le monde n'a pas le même matériel...)
Sinon je peux compiler de mon côté.

Tituya a écrit :
Disponible ici : https://github.com/Heath123/casio-emu
Ou réhebergé sur notre gitea https://gitea.planet-casio.com/Slyvtt/casio-emu

Tu as juste à suivre le readme pour l'installer

Merci je vais attendre la réponse de PapySly
Un peu poète, un peu geek, un peu rêveur, un peu écolo.

Slyvtt En ligne Maître du Puzzle Points: 2414 Défis: 17 Message

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


Les touches sont reprises dans la description README.md du projet (page de garde en bas dans le tableau pour la version QT)

Pour changer, c'est là que ça se passe pour la version QT5 :
https://gitea.planet-casio.com/Slyvtt/casio-emu/src/branch/master/src/gui/qt/gui.cpp#L42-L138

Et ici pour la version SDL 2 :
https://gitea.planet-casio.com/Slyvtt/casio-emu/src/branch/master/src/gui/sdl/gui.c#L88-L235
There are only 10 types of people in the world: Those who understand binary, and those who don't ...
Slyvtt En ligne Maître du Puzzle Points: 2414 Défis: 17 Message

Citer : Posté le 08/02/2024 11:27 | # | Fichier joint


Je mets en PJ à ce post un zip qui contient les deux versions de calcemu compilées sur la distribution Ubuntu 22.04.

calcemuQT peut être lancée sans paramètre, auquel cas une fenêtre d'ouverture de fichier apparaîtra pour charger un fichier g3a. Il est possible de tout faire en ligne de commande : calcemuQT chemin_vers_votre_fichier_g3a

calcemuSDL ne peut être lancé que via la ligne de commande en spécifiant le fichier g3a à lancer. la seule commande possible est donc : calcemuSDL chemin_vers_votre_fichier_g3a

Je vous conseille de mettre dans votre home dans .local/bin et de faire un lien symbolique calcemu qui pointe sur la version que vous voulez utiliser préférentiellement (mon choix perso va pour la version QT).

A noter que l'émulateur est en développement, et donc il y a des trucs qui ne fonctionnent pas (encore) :
- pas de support de l'USB
- pas de support du Filesystem (donc pas de fopen/BFile et compagnie)

Je précise que j'ai compilé sur ma machine avec QT5 installé et SDL2 installé, vous aurez certainement besoin des librairies adhoc pour que cela fonctionne correctement (se lance en fait)
There are only 10 types of people in the world: Those who understand binary, and those who don't ...
Ptitjoz Hors ligne Membre Points: 274 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.

1, 2 Suivante

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 90 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