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 - Vos tutoriels et astuces


Index du Forum » Vos tutoriels et astuces » Tutoriel de création d'un jeu de pong avec le fxsdk/gint
Mb88 Hors ligne Rédacteur Points: 1334 Défis: 3 Message

Tutoriel de création d'un jeu de pong avec le fxsdk/gint

Posté le 19/07/2026 20:47

NOTE : Le tutoriel est aussi disponible ici : https://git.planet-casio.com/mibi88/pong_fxsdk/src/branch/master/TUTORIAL_fr.md.

Tutoriel de développement d'un jeu de pong avec le fxsdk

Dans ce tutoriel nous allons coder un jeu de pong en C avec gint et le fxsdk.

Préréquis

Des connaissances solides de C sont requises.

Pour pouvoir suivre ce tutoriel, vous avez besoin du fxsdk et de gint. Vous avez donc besoin d'un système d'exploitation UNIX-like compatible avec celui-ci. Vous avez bien sûr besoin d'une calculatrice CASIO monochrome ou couleur pour pouvoir tester l'add-in.

Pour tester l'add-in régulièrement, vous pouvez utiliser mq (https://www.planet-casio.com/Fr/forums/topic18782-4-mq-emulateur-add-ins-universel.html).

Une version en ligne de mq existe aussi : https://www.planet-casio.com/mq/index.html

Je trouve cependant qu'il est plus pratique d'utiliser la build Linux pour pouvoir tester l'add-in en saisissant la commande suivante dans un shell :

$ <chemin vers le binaire de mq> <chemin vers l'add-in>

NOTE : Dans ce tutoriel je marquerais tout le texte à replacer entre chevrons. Ci-dessus, <chemin vers le binaire de mq> doit être replacé par le chemin vers ce binaire, et <chemin vers l'add-in> doit être replacé par le chemin vers l'add-in à exécuter.

Je vous conseille de quand même de tester régulièrement vos add-ins on-calc, mq est encore assez nouveau, et ne reproduit pas à 100 % le comportement de la calculatrice. Par exemple, le jeu ne s'exécutera probablement pas à la même vitesse dans mq que sur la vraie calculatrice.

Quelques infos utiles

Le coin en haut à gauche est l'origine du repère. Le pixel en haut à gauche est aux coordonnées (0; 0) et le pixel en bas à droite aux coordonnées (127; 63) ou (395; 223) pour les calculatrices monochromes ou couleur respectivement.

L'écran des calculatrices monochromes a une résolution de 128 × 64 et celui des calculatrices couleur une résolution de 396 × 224 (sans le bandeau).

Création du projet

Pour créer un nouveau projet, il suffit d'exécuter la commande suivante dans le dossier où vous souhaitez créer le projet :

$ fxsdk new <nom du projet>

Par exemple :

$ fxsdk new pong

Entrez le répertoire crée avec ce nom. Ensuite vous pouvez compiler votre projet.

Pour le compiler pour des calculatrices monochromes :

$ fxsdk build-fx

Pour des calculatrices couleur :

$ fxsdk build-cg

Pour compiler la version monochrome pour une calculatrice couleur :

$ fxsdk build-fxg3a

Pour tester l'add-in avec Addin push (https://www.planet-casio.com/Fr/programmes/programme4384-2-addin-push-heath-utilitaires-add-ins.html#65954) :

$ fxsdk build-cg-push

et pour l'envoyer (nécessite d'avoir lancé Addin push sur la calculatrice) :

$ fxlink -p build-cg-push build-cg-push/<nom de l'add-in>.bin

Premier test

Vérifier que le projet compile et que l'add-in produit affiche bien « Sample fxSDK add-in. » en haut à gauche de l'écran.

Structure du projet

Nous allons structurer le projet comme ceci:

  • main.c Lancement du jeu et boucle du jeu.
  • game.c et game.h logique du jeu et rendu. game.h contiendra toutes les structures.
  • ball.c et ball.h mouvement, collisions et rendu de la balle.
  • paddle.c et paddle.h gestion des collisions entre la balle et la raquette, mouvement et rendu de la raquette.
  • fixed.h Calculs à virgule fixe (j'explique ce que c'est dans la suite du tutoriel).

Créez ces fichiers dans src.

Dans assets-fx et assets-cg, nous stockerons l'écran de titre, ayant une dimension de 128 × 64 ou 396 × 224 pour calculatrices monochromes ou couleur respectivement.

Votre projet devrait avoir la structure suivante :

$ tree
.
├── assets-cg
│   ├── fxconv-metadata.txt
│   ├── icon-cg.xcf
│   ├── icon-sel.png
│   ├── icon-uns.png
│   └── title.png
├── assets-fx
│   ├── fxconv-metadata.txt
│   ├── icon.png
│   └── title.png
├── CMakeLists.txt
└── src
    ├── ball.c
    ├── ball.h
    ├── fixed.h
    ├── game.c
    ├── game.h
    ├── main.c
    ├── paddle.c
    └── paddle.h

4 directories, 17 files

Ajout des fichiers de code source et des assets à CMakeLists.txt

CMake est utilisé pour compiler les projets utilisant le fxsdk.

Ajout des fichiers de code source

Pour que nos fichiers .c soient bien compilés, il faut les ajouter ici :

set(SOURCES
  src/main.c
  # ...
)

On a donc :

set(SOURCES
  src/main.c
  src/ball.c
  src/game.c
  src/paddle.c
  # ...
)

Ajout des assets

Pour ajouter nos assets au projet, il faut les ajouter à la propriété ASSETS, ASSETS_fx ou ASSETS_cg, pour qu'ils soient inclus lorsque l'on compile l'add-in pour n'importe quel modèle de calculatrice, une calculatrice avec un écran monochrome ou une calculatrice avec un écran couleur respectivement.

Pour nous, les lignes

set(ASSETS
  # ...
)
set(ASSETS_fx
  assets-fx/example.png
  # ...
)
set(ASSETS_cg
  assets-cg/example.png
  # ...
)

deviennent donc :

set(ASSETS
  # ...
)
set(ASSETS_fx
  assets-fx/title.png
  # ...
)
set(ASSETS_cg
  assets-cg/title.png
  # ...
)

Ensuite, il faut changer les fichiers fxconv_metadata.txt dans assets-fx et assets-cg.

Ils contiendront :

title.png:
  type: bopti-image
  name: title_img

Ces fichiers permettent de configurer comment les images sont converties vers le format utilisé par gint par l'utilitaire du fxsdk appelé fxconv.

Nous allons utiliser la même configuration que celle utilisée par l'image d'exemple. Nous changeons juste les noms. C'est aussi à l'aide de ce fichier qu'il est possible d'ajouter des polices d'écriture (sous forme de bitmap, d'image) à un projet, mais nous ne le ferons pas dans ce projet.

Code

Nous sommes enfin prêts à rentrer dans le vif du sujet, le code !

main.c, la boucle du jeu

Nous allons commencer par faire la boucle du jeu. Pour que notre jeu soit agréable à jouer, nous allons limiter le nombre d'images par seconde auquel il peut tourner, car la calculatrice est en mesure de l'exécuter très très vite.

Nous allons quitter la boucle si EXIT est appuyé, ce que nous pouvons vérifier avec keydown(KEY_EXIT) définie dans <gint/keyboard.h>. L'ensemble des codes associés à des touches peuvent être lus dans le fichier d'en-tête <gint/keycodes.h> (https://git.planet-casio.com/Lephenixnoir/gint/src/branch/dev/include/gint/keycodes.h). Ce fichier est inclus par <gint/keyboard.h>.

J'ai donc choisi de limiter le nombre d'images par seconde à 50 (cela vous rappelle peut-être les jeux vidéos PAL des années 80 en Europe).

NOTE : Sur PC, on calcule le temps qu'une image prend pour être produite pour adapter la vitesse des éléments qui se déplacent, pour que le jeu se comporte pareil indépendamment du nombre d'images par seconde.

Le jeu est assez simple, on ne limitera donc que le nombre d'images par seconde, il est très probable qu'une image soit produite assez rapidement, et étant donné le peu de différences entre les différents modèles de calculatrices, en testant sur une calculatrice couleur et une calculatrice monochrome, la vitesse à laquelle s'exécutera le jeu est assez prévisible.

Utilisation de timer

Pour compter le temps qui s'est écoulé entre deux images, nous allons créer un timer. Le processeur, à un intervalle de temps régulier, arrêtera ce qu'il est en train d'exécuter, et exécutera une fonction que nous passons à timer_configure, puis retournera à ce qu'il était en train d'exécuter.

Les fonctions liées à la gestion de timer sont définies dans <gint/timer.h>.

D'abord il faut le configurer avec timer_configure, puis le lancer avec timer_start. Ensuite, lorsque l'on n'en a plus besoin, on l'arrête avec timer_stop. Cela libère aussi les ressources associées à ce timer.

La macro GINT_CALL permet d'obtenir un gint_call_t à passer à timer_configure pour passer la fonction à appeler en argument. Cette fonction doit renvoyer l'entier (int) TIMER_CONTINUE ou TIMER_STOP, pour que le timer continue d'interrompre le processeur à intervalles réguliers, ou pour qu'il s'arrête.

Pour qu'une fonction ayant pour prototype int timer_call(void) soit appelée toute les millisecondes, on peut écrire le code suivant :

int timer;

timer = timer_configure(TIMER_ANY, 1000, GINT_CALL(timer_call));
if(timer < 0){
    /* Afficher un message d'erreur. */
    dclear(C_WHITE);

    dtext(1, 1, C_BLACK, "Failed to create timer!");

    dupdate();
    getkey();

    return 0;
}
timer_start(timer);

/* Du code..., par exemple la boucle d'un jeu. */

timer_stop(timer);

NOTE : getkey permet d'attendre l'appui d'une touche, et renvoie un objet key_event_t décrivant la touche qui a été appuyée (plus d'informations dans <gint/keyboard.h>.

NOTE : Au lieu de TIMER_ANY on peut aussi passer TIMER_TMU pour un timer très précis, ou TIMER_ETMU, pour un timer un peu moins précis.

Pour plus d'informations sur les timer, regardez le fichier d'en-tête <gint/timer.h>.

Quelques fonctions de dessin

Dans la boucle de jeu, nous allons effacer l'écran avec dclear(C_WHITE); et afficher ce que nous avons dessiné avec dupdate();.

On peut afficher du texte avec dtext, par exemple :

dtext(1, 1, C_BLACK, "Sample fxSDK add-in.");

Boucle de jeu

Au début de chaque itération de cette boucle, nous allons appeler clearevents, pour que gint traite tout les événements en attente (pour pouvoir vérifier si une touche est enfoncée avec keydown), et cleareventflips, pour que gint sache que l'on commence une nouvelle image du jeu, et qu'il mette à jour les états des touches pour que l'on puisse utiliser keypressed et keyreleased, qui permet de savoir si une touche a été relâchée ou enfoncée depuis le dernier appel à cleareventflips, donc depuis la dernière image.

NOTE : Sur la calculatrice, main doit renvoyer 1 en cas de succès et pas zéro comme c'est le cas habituellement.

Essayez d'écrire la boucle de jeu, puis regardez mon implémentation.

#include <gint/keyboard.h>
#include <gint/display.h>
#include <gint/timer.h>
#include <gint/cpu.h>

static volatile unsigned long int ms = 0;

static int timer_call(void) {
    ms++;

    return TIMER_CONTINUE;
}

struct game game;

int main(void) {
    int timer;

    timer = timer_configure(TIMER_TMU, 1000, GINT_CALL(timer_call));
    if(timer < 0){
        dclear(C_WHITE);

        dtext(1, 1, C_BLACK, "Failed to create timer!");

        dupdate();
        getkey();

        return 0;
    }
    timer_start(timer);

    /* Initialisation du jeu. */
    game_init(&game);

    /* Boucle de jeu. */
    do{
        cleareventflips();
        clearevents();

        /* Generation d'une image du jeu. */
        dclear(C_WHITE);

        game_logic(&game);
        game_draw(&game);

        dupdate();

        /* Attendre que 20ms se soient ecoulees. */
        while(ms < 20) sleep();
        ms = 0;
    }while(!keydown(KEY_EXIT));

    timer_stop(timer);

    /* On retourne 1, car c'est ce que des add-ins doivent renvoyer en cas de
     * succes. */
    return 1;
}

Testez. Si vous arrivez bien à quitter le jeu en appuyant sur EXIT, continuez.

game.c et game.h : initialisation, rendu et logique du jeu

J'aime bien structurer mes jeux en séparant l'initialisation, la logique et le dessin, en stockant l'état dans lequel le jeu se trouve et en traitant chaque état dans des grands switch dans les fonctions s'occupant de la logique et du dessin.

Dans game.h

Nous allons commencer par créer une structure contenant toutes les données du jeu, contenant pour l'instant seulement l'état dans lequel le jeu se trouve :

struct game {
    unsigned char state;
};

Nous allons définir ces différents états avec une enum :

enum {
    S_INGAME, /* Etat dans lequel on est lorsque le joueur est en train de
               * jouer. */

    S_AMOUNT
};

Nous ajouterons d'autres états au fur et à mesure ou nous développerons le jeu.

Dans game.c

Ensuite, nous allons créer trois fonctions:

  • game_init pour initialiser le jeu.
  • game_logic pour la logique du jeu.
  • game_draw pour le rendu.

game.c contient donc :

void game_init(struct game *game) {
    game->state = S_INGAME;
}

void game_logic(struct game *game) {
    switch(game->state){
        case S_INGAME:
            /* TODO */

            break;

        default:
            /* Cela ne devrait jamais arriver. */
            game->state = S_INGAME;
    }
}

void game_draw(struct game *game) {
    switch(game->state){
        case S_INGAME:
            /* TODO */

            break;

        default:
            /* Cela ne devrait jamais arriver. */
            game->state = S_INGAME;
    }
}

On peut ensuite appeler game_logic, puis game_draw dans la boucle du jeu, et game_init avant.

paddle.c : mouvement et dessin de la raquette

Nous allons maintenant commencer à écrire le code exécuté lorsque l'on se trouve dans l'état S_INGAME. Nous allons commencer par coder le code pour le dessin et le mouvement de la raquette du joueur.

Nous allons en premier lieu créer une structure contenant des données décrivant la raquette dans game.h :

struct paddle {
    /* La position de la raquette. */
    int x;
    int y;
};

Les membres x et y de struct paddle contiennent la position du coin en haut à gauche de la raquette.

Comme dans game.c, nous allons créer une fonction pour initialiser une structure paddle : paddle_init.

/* Initialise la raquette. */
void paddle_init(struct paddle *paddle, int x);

Ensuite, nous allons écrire plusieurs fonctions utilisant cette structure, en commençant par les fonctions suivantes :

/* Deplace la raquette avec les touches du clavier. */
void paddle_move(struct paddle *paddle);

/* Dessine la raquette. */
void paddle_draw(struct paddle *paddle);

Initialisation de la raquette.

Dans paddle_init nous allons centrer la raquette sur l'axe des ordonnées, et mettre son abscisse à x (un paramètre de paddle_init).

Pour calculer le centre de l'écran, nous pouvons utiliser les object-like macros DWIDTH et DHEIGHT définies dans <gint/display-fx.h> ou <gint/display-cg.h>. Ces en-têtes sont inclues par <gint/display.h>, la bonne en-tête est choisie en fonction de la plateforme visée.

Dans paddle.h nous allons inclure <gint/config.h> pour identifier pour quelle plateforme nous compilons le jeu, afin de définir des macros avec des valeurs différentes pour chaque plateforme, permettant d'avoir une expérience similaire sur les calculatrices couleur ou monochrome, malgré les différences de taille d'écran.

  • Le jeu est compilé pour une calculatrice monochrome (ou couleur avec fxsdk build-fxg3a), si GINT_RENDER_MONO est non nul.
  • Le jeu est compilé pour une calculatrice couleur si GINT_RENDER_RGB est défini. Ce tutoriel n'est pas adapté aux calculatrices ClassPad, car je n'ai pas une telle machine. Cependant, il y a sûrement peu de choses à ajuster, entre autres ces macros, pour le suivre pour le développement d'un add-in pour ClassPad. L'add-in est compilé pour ClassPad si GINT_HW_CP est défini, et pour Graph 90+e/CG50 ou Prizm si GINT_HW_CG est défini.

Nous allons définir la largeur de la raquette sous le nom PADDLE_WIDTH, sa hauteur avec PADDLE_HEIGHT et sa vitesse de déplacement avec PADDLE_SPEED.

Voici comment je les ai définis :

#if GINT_RENDER_MONO

/* La vitesse de la raquette. */
#define PADDLE_SPEED 1

/* La taille de la raquette. */
#define PADDLE_WIDTH 3
#define PADDLE_HEIGHT 16

#elif GINT_RENDER_RGB && GINT_HW_CG

/* La vitesse de la raquette. */
#define PADDLE_SPEED 3

/* La taille de la raquette. */
#define PADDLE_WIDTH 9
#define PADDLE_HEIGHT 48

#else

#error "Unsupported!"

#endif

Essayez maintenant de coder paddle_init.

Ensuite, vous pouvez regarder comment je l'ai implémenté :

void paddle_init(struct paddle *paddle, int x) {
    /* Initialisation de la position de la raquette. */
    paddle->x = x;
    paddle->y = DHEIGHT/2-PADDLE_HEIGHT/2;
}

Ajoutez deux struct paddle à struct game : une pour le joueur, et une pour l'ordinateur.

NOTE : Lorsque j'écris « l'ordinateur », je désigne le joueur joué par la calculatrice.

Ma struct game contient les membres suivants maintenant :

struct game {
    /* La raquette du joueur. */
    struct paddle paddle1;
    /* La raquette de l'ordinateur. */
    struct paddle paddle2;

    /* L'etat dans lequel se trouve le jeu. */
    unsigned char state;
};

Appelez ensuite paddle_init dans game_init pour initialiser les deux raquettes. J'ai placé mes raquettes à 16 px du bord de l'écran.

NOTE : On aurait aussi pu définir une object-like macro pour changer cette position d'une plateforme à l'autre.

Essayez déjà de le faire par vous-même.

Voici mon implémentation :

void game_init(struct game *game) {
    /* Initialisation des raquettes aux deux cotes de l'ecran. */
    paddle_init(&game->paddle1, 16);
    paddle_init(&game->paddle2, DWIDTH-16-PADDLE_WIDTH);

    game->state = S_INGAME;
}

Dessin de la raquette

Pour vérifier que nos calculs sont corrects, nous allons dessiner la raquette.

Nous allons dessiner la raquette dans paddle_draw, défini dans paddle.c.

Nous allons dessiner un rectangle blanc avec un bord noir d'un pixel pour la raquette.

Il est donc le bon moment de s'intéresser à quelques fonctions de dessin de gint (dont les prototypes sont dans <gint/display.h>) :

  • drect : dessin d'un rectangle rempli
  • drect_border : dessin de la bordure d'un rectangle
  • dpixel : dessin d'un seul pixel
  • dline : dessin d'une ligne
  • dcircle : dessin d'un cercle
  • dellipse : dessin d'un ellipse
  • dpoly : dessin d'un polygone
  • dtext, dtext_opt et dprint : affichage de texte
  • dimage : dessin d'une image
  • dsubimage : dessin d'une partie d'une image

NOTE : Ces fonctions sont très bien documentées dans l'en-tête <gint/display.h>.

Pour le dessin de la raquette, nous allons utiliser drect_border. Le prototype de drect_border dans <gint/display.h> est :

void drect_border(int x1, int y1, int x2, int y2,
    int fill_color, int border_width, int border_color);

Implémentez paddle_draw.

Voici mon implémentation de paddle_draw :

void paddle_draw(struct paddle *paddle) {
    /* Dessin d'un rectangle representant la raquette. */
    drect_border(paddle->x, paddle->y, paddle->x+PADDLE_WIDTH,
                 paddle->y+PADDLE_HEIGHT, C_NONE, 1, C_BLACK);
}

Pour voir les raquettes, nous allons appeler paddle_draw dans game_draw. Ma fonction game_draw ressemble maintenant à :

void game_draw(struct game *game) {
    switch(game->state){
        case S_INGAME:
            /* Dessin des raquettes. */
            paddle_draw(&game->paddle1);
            paddle_draw(&game->paddle2);

            break;

        default:
            /* Cela ne devrait jamais arriver. */
            game->state = S_INGAME;
    }
}

Testez votre code, et continuez si vous voyez bien les deux rectangles représentant les raquettes aux deux côtés de l'écran.

Mouvement de la raquette

Maintenant que le joueur voit les raquettes, nous allons lui permettre de déplacer la sienne.

Nous allons utiliser keydown pour vérifier si les touche KEY_UP ou KEY_DOWN sont pressées pour déplacer la raquette vers le haut ou vers le bas respectivement.

Nous allons aussi vérifier, que la raquette ne sort pas de l'écran (que paddle->y >= 0 && paddle->y < DHEIGHT-PADDLE_HEIGHT).

Je vous laisse donc implémenter la fonction paddle_move qui a le prototype suivant :

void paddle_move(struct paddle *paddle);

Voici mon implémentation :

void paddle_move(struct paddle *paddle) {
    /* Deplacement de la raquette vers le haut si la fleche "haut" est pressee.
     */
    if(keydown(KEY_UP)){
        if(paddle->y-PADDLE_SPEED >= 0){
            paddle->y -= PADDLE_SPEED;
        }else{
            paddle->y = 0;
        }
    }
    /* Deplacement de la raquette vers le bas si la fleche "bas" est pressee.
     */
    if(keydown(KEY_DOWN)){
        if(paddle->y+PADDLE_SPEED < DHEIGHT-PADDLE_HEIGHT){
            paddle->y += PADDLE_SPEED;
        }else{
            paddle->y = DHEIGHT-PADDLE_HEIGHT-1;
        }
    }
}

Nous allons appeler cette fonction dans game_logic, pour pouvoir voir la raquette se déplacer en fonction des actions de l'utilisateur !

Essayez par vous-même.

Voici à quoi ressemble ma fonction game_logic maintenant :

void game_logic(struct game *game) {
    switch(game->state){
        case S_INGAME:
            /* Permettre au joueur de deplacer sa raquette. */
            paddle_move(&game->paddle1);

            break;

        default:
            /* Cela ne devrait jamais arriver. */
            game->state = S_INGAME;
    }
}

fixed.h : calculs à virgule fixe

Les écrans des calculatrices sont petits : 128 × 64 pour les calculatrices monochromes, et 396 × 224 pour les calculatrices couleur (hors ClassPad).

Il est donc difficile de contrôler précisément la vitesse d'objets dans un jeu en les stockant sous forme d'entiers. Une vitesse de deux pixels par image est beaucoup plus rapide qu'une vitesse d'un seul pixel par image.

Nous aurons besoin de précision supplémentaire dans le déplacement de la balle.

On pourrait donc utiliser des floats. Cependant, les calculatrices CASIO SH-3 ou SH-4 n'ont pas de FPU, une unité de calcul avec des nombres à virgule flottante dans le MPU. Ils sont donc effectués en software, et sont donc assez lents. Notre jeu est assez simple, donc cela ne pose pas de gros problème, en dehors de la taille d'add-in un peu plus grande. Dans des jeux plus complexes, cela peut poser problème.

Nous allons donc faire des calculs à virgule fixe, bien moins coûteux : nous allons réserver quelque bits dans un entier pour les chiffres après la virgule.

Exemple : 19,2 comme entier 16 bit non signé avec une précision de 8 bits après la virgule :



Avant la virgule Après la virgule
00010011 00110011

Si nous reconvertissons ce chiffre en nombre à virgule, nous obtenons environ 19,199. Nous avons perdu en précision, mais ce n'est pas grave dans un jeu vidéo (19,199 c'est quand même très proche de 19,2 !).

Codons quelque macros pour convertir des nombres à virgule en nombres à virgule fixe, et vice-versa, et d'autres pour la multiplication et la division (nous pouvons faire les additions et les soustractions comme d'habitude).

La conversion est très facile, il suffit d'effectuer des décalages de bits.

/* La precision des nombres a virgule fixe. */
#define PRECISION 8

/* Conversion d'un nombre vers un nombre a virgule fixe. */
#define TO_FIXED(num) (int)((num)*(1<<PRECISION))
/* Conversion d'un nombre a virgule fixe vers un entier. */
#define TO_INT(num) ((num)>>PRECISION)

NOTE : Je multiplie num par (1*PRECISION) pour pouvoir passer des float à la macro, on ne peut pas faire des décalages de bits sur un float.

Les macros pour la multiplication et la division sont très simples, elles aussi :

/* Multiplication de deux nombres a virgule fixe. */
#define MUL(a, b) ((a*b)>>PRECISION)
/* Divise le nombre a virgule fixe a par le nombre a virgule fixe b. */
#define DIV(a, b) ((a<<PRECISION)/b)

Nous allons finir par définir un type fixed_t, pour bien les distinguer des autres entiers :

/* Type virgule fixe (pour les distinguer des autres entiers). */
typedef long fixed_t;

NOTE : Ce code suppose que le décalage de bits vers la droite est arithmétique (ce n'est pas forcément le cas en C), et il y a un risque de débordement lors des multiplications et des divisions si a et/ou b sont trop grands.

ball.c : mouvement de la balle et collisions avec la raquette

Tout d'abord, nous allons créer une structure contenant des données associées à la balle.

Nous allons y stocker la position de la balle (x et y), la direction dans laquelle elle se déplace (x_inc et y_inc) et sa vitesse comme des fixed_t.

Ma structure :

/* La structure representant la balle (utilisee dans ball.c). */
struct ball {
    /* La position de la balle. */
    fixed_t x;
    fixed_t y;

    /* La direction dans laquelle on deplace la balle. */
    fixed_t x_inc;
    fixed_t y_inc;

    /* La vitesse de la balle. */
    fixed_t speed;
};

x et y contiennent la position du milieu de la balle.

Initialisation et dessin de la balle

Nous allons ensuite implémenter les fonctions suivantes dans ball.c :

/* Initialisation de la balle. */
void ball_init(struct ball *ball, fixed_t x_inc, fixed_t y_inc, int speed);
/* Dessin de la balle. */
void ball_draw(struct ball *ball);

Commençons par ball_init. Nous allons placer la balle au milieu de l'écran, et initialiser les autres membres de struct ball aux valeurs de x_inc, y_inc et speed.

Voici mon implémentation :

void ball_init(struct ball *ball, fixed_t x_inc, fixed_t y_inc, int speed) {
    /* Mettre la balle au milieu de l'ecran. */
    ball->x = TO_FIXED(DWIDTH/2);
    ball->y = TO_FIXED(DHEIGHT/2);

    /* Initialisation de la direction dans laquelle la balle se deplace et sa
     * vitesse. */
    ball->x_inc = x_inc;
    ball->y_inc = y_inc;
    ball->speed = speed;
}

Maintenant dessinons la balle. Nous la dessinerons comme un rectangle rempli. Nous définirons une macro – BALL_SIZE – dans ball.h, contenant la moitié de la taille de la balle moins 1 (e.g. pour une balle d'une taille de trois pixels, BALL_SIZE est égal à (3 − 1) ÷ 2).

Nous allons lui donner une valeur différente en fonction de la plateforme. Ma balle a une taille de trois pixels sur calculatrices monochromes, et une taille de sept pixels sur calculatrices couleur (hors ClassPad).

Mon fichier ball.h contient maintenant le code suivant :

#if GINT_RENDER_MONO

/* La taille de la balle. */
#define BALL_SIZE 1

#elif GINT_RENDER_RGB && GINT_HW_CG

/* La taille de la balle. */
#define BALL_SIZE 3

#else

#error "Unsupported!"

#endif

Codez ball_draw en utilisant BALL_SIZE.

Ma fonction ball_draw est la suivante :

void ball_draw(struct ball *ball) {
    /* Get the position of the ball on screen. */
    int screen_x = TO_INT(ball->x);
    int screen_y = TO_INT(ball->y);

    /* Draw a rectangle and fill it to make the ball more visible. */
    drect(screen_x-BALL_SIZE, screen_y-BALL_SIZE,
          screen_x+BALL_SIZE, screen_y+BALL_SIZE, C_BLACK);
}

Il est maintenant temps de tester si notre nouveau code fonctionne bien !

Pour cela, ajoutons une balle à struct game.

Ma struct game avec cet ajout :

struct game {
    /* La balle. */
    struct ball ball;

    /* La raquette du joueur. */
    struct paddle paddle1;
    /* La raquette de l'ordinateur. */
    struct paddle paddle2;

    /* L'etat dans lequel se trouve le jeu. */
    unsigned char state;
};

Appelez ball_init dans game_init.

Définissons une macro – DEFAULT_SPEED – dans ball.h, contenant la vitesse de la balle au début d'une partie.

Définitions de macros dans mon fichier ball.h :

#if GINT_RENDER_MONO

/* La vitesse de la balle par defaut. */
#define DEFAULT_SPEED 0.2

/* La taille de la balle. */
#define BALL_SIZE 1

#elif GINT_RENDER_RGB && GINT_HW_CG

/* La vitesse de la balle par defaut. */
#define DEFAULT_SPEED 0.6

/* La taille de la balle. */
#define BALL_SIZE 3

#else

#error "Unsupported!"

#endif

Ma fonction game_init avec cet ajout :

void game_init(struct game *game) {
    /* Initialisation de la balle et initialisation de sa direction pour un
     * deplacement en direction du joueur. */
    ball_init(&game->ball, TO_FIXED(-1), TO_FIXED(1), TO_FIXED(DEFAULT_SPEED));

    /* Initialisation des raquettes aux deux cotes de l'ecran. */
    paddle_init(&game->paddle1, 16);
    paddle_init(&game->paddle2, DWIDTH-16-PADDLE_WIDTH);

    game->state = S_INGAME;
}

NOTE : Notre vecteur de direction dans laquelle la balle se déplace n'est pas normalisé, et la vitesse de la balle diffèrera un peu avec la direction dans laquelle la balle se déplace. Ce n'est pas trop grave dans ce jeu, et cela évite de devoir faire des calculs un peu plus compliqués.

Appelez ball_draw dans game_draw.

Ma fonction game_draw :

void game_draw(struct game *game) {
    switch(game->state){
        case S_INGAME:
            /* Dessin des raquettes. */
            paddle_draw(&game->paddle1);
            paddle_draw(&game->paddle2);

            /* Dessin de la balle. */
            ball_draw(&game->ball);

            break;

        default:
            /* Cela ne devrait jamais arriver. */
            game->state = S_INGAME;
    }
}

Testez le jeu. Est-ce que votre jeu compile sans erreurs ? Est-ce que vous voyez bien une balle au centre de l'écran avec des raquettes des deux côtés de l'écran que vous pouvez déplacer ?

Si oui, continuez, sinon cherchez les erreurs dans votre code.

Mouvement de la balle et rebonds aux bords de l'écran

Nous allons maintenant programmer ball_move pour déplacer notre balle.

/* Mouvement de la balle. */
char ball_move(struct ball *ball);

Nous allons calculer la nouvelle position de la balle en ajoutant à sa position actuelle (ball->x_inc × ball->speed; ball->y_inc × ball->speed).

Nous ajusterons cette nouvelle position pour faire rebondir la balle sur les bords en haut et en bas de l'écran.

Si la balle sort de l'écran à gauche, nous retournons 2 (l'ordinateur marque un point). Si elle sort de l'écran à droite, nous retournons 1 (le joueur marque un point). Sinon nous retournons 0. Cela nous permettra de compter les points.

Codez ball_move.

Voici ma fonction ball_move :

char ball_move(struct ball *ball) {
    /* Les nouveaux coordonnés de la balle (à l'écran). */
    int dest_x = TO_INT(ball->x+MUL(ball->x_inc, ball->speed));
    int dest_y = TO_INT(ball->y+MUL(ball->y_inc, ball->speed));

    /* Si dest_x > DWIDTH le joueur marque un point. */
    if(dest_x > DWIDTH-BALL_SIZE) return 1;
    /* Si dest_x < 0 l'ordinateur marque un point. */
    if(dest_x < BALL_SIZE) return 2;

    /* Faire rebondir la balle sur les bords de l'écran. */
    if(dest_y < BALL_SIZE){
        ball->y_inc = -ball->y_inc;
        ball->y = TO_FIXED(BALL_SIZE);
    }
    if(dest_y > DHEIGHT-BALL_SIZE){
        ball->y_inc = -ball->y_inc;
        ball->y = TO_FIXED(DHEIGHT-BALL_SIZE);
    }

    /* Déplacement de la balle. */
    ball->x += MUL(ball->x_inc, ball->speed);
    ball->y += MUL(ball->y_inc, ball->speed);

    return 0;
}

Nous allons tester ce nouveau bout de code en appelant dans game_logic.

Pour l'instant, nous ferrons rebondir la balle sur les bords de l'écran si la valeur renvoyée par ball_move est non nulle.

Appelez ball_move dans game_logic.

Mon code de game_logic :

void game_logic(struct game *game) {
    switch(game->state){
        case S_INGAME:
            {
                char move;

                /* Permettre au joueur de deplacer sa raquette. */
                paddle_move(&game->paddle1);

                /* Deplacement de la balle. */
                move = ball_move(&game->ball);
                if(move == 1){
                    game->ball.x = TO_FIXED(DWIDTH-BALL_SIZE);
                    game->ball.x_inc = -game->ball.x_inc;
                }else if(move == 2){
                    game->ball.x = TO_FIXED(BALL_SIZE);
                    game->ball.x_inc = -game->ball.x_inc;
                }
            }

            break;

        default:
            /* Cela ne devrait jamais arriver. */
            game->state = S_INGAME;
    }
}

Testez le jeu. Est-ce que le code compile et est-ce que la balle rebondit bien sur les bords de l'écran ?

Rebonds sur la raquette

Notre jeu est presque fini (si l'on exclut l'écran de titre et la fin d'une partie) ! Faisons rebondir la balle sur les raquettes !

Pour cela nous allons coder paddle_will_collide_with_ball et ball_bounce, qui ont les prototypes suivants :

/* Verifier si la balle heurte la raquette. */
char paddle_will_collide_with_ball(struct paddle *paddle, struct ball *ball);
/* Faire rebondir la balle sur une raquette. */
void ball_bounce(struct ball *ball, struct paddle *paddle);

Programmez paddle_will_collide_with_ball qui renvoie 1 si la balle va heurter la raquette lors de son déplacement (on ne vérifie pas si elle heurte le haut ou le bas de la raquette pour que notre code soit plus simple).

Voici mon implémentation :

char paddle_will_collide_with_ball(struct paddle *paddle, struct ball *ball) {
    /* On calcule la nouvelle abscisse de la balle. */
    fixed_t dest_x = ball->x+MUL(ball->x_inc, ball->speed);

    /* NOTE: Un seul *if statement* serait suffisant, mais le code serait
     * peut-etre un peu plus dur a comprendre. */
    /* Est-ce que la balle est sur la raquette sur l'axe des ordonnees ? */
    if(ball->y+TO_FIXED(BALL_SIZE) >= TO_FIXED(paddle->y) &&
       ball->y-TO_FIXED(BALL_SIZE) <= TO_FIXED(paddle->y+PADDLE_HEIGHT)){
        /* Est-ce que la balle traverserait la raquette (sur l'axe des
         * abscisses) ? */
        if((ball->x > TO_FIXED(paddle->x+PADDLE_WIDTH+BALL_SIZE) &&
            dest_x <= TO_FIXED(paddle->x+PADDLE_WIDTH+BALL_SIZE)) ||
           (ball->x < TO_FIXED(paddle->x-BALL_SIZE) &&
            dest_x >= TO_FIXED(paddle->x-BALL_SIZE))){
            /* The ball will hit the paddle. */
            return 1;
        }
    }
    return 0;
}

NOTE : Je ne calcule pas la nouvelle ordonnée de la balle. Si nous la calculons, la différence ne se remarquerait sûrement pas. Une méthode plus exacte pour vérifier s'il y a une collision serait de vérifier si le segment formé par la position actuelle et la prochaine position de la balle coupe l'un des segments verticaux du rectangle qu'est notre raquette.

Dans ball_bounce nous allons calculer la nouvelle direction dans laquelle la balle se déplace en fonction de la position où la balle heurte la raquette.

Nous allons juste inverser x_inc, c'est y_inc que nous ajusterons en fonction de la position à laquelle la balle heurte la raquette. Nous allons définir quelque macros pour pouvoir ajuster comment y_inc est calculé.

  • ANGLE_MIN : La valeur de y_inc après que la balle heurte le haut de la raquette
  • ANGLE_MAX : La valeur de y_inc après que la balle heurte le bas de la raquette
  • ANGLE_RANGE : La valeur absolue de la différence entre ANGLE_MIN et ANGLE_MAX

Nous calculerons y_inc en effectuant de l'interpolation linéaire entre ANGLE_MIN et ANGLE_MAX avec la position à laquelle la balle heurte la raquette.

Nous allons aussi définir SPEED_INC et MAX_SPEED pour augmenter la vitesse de la balle à chaque rebond, en la limitant à un certain seuil.

Je définis maintenant les macros suivantes dans ball.h :

#if GINT_RENDER_MONO

/* La vitesse de la balle par defaut. */
#define DEFAULT_SPEED 0.2
/* La vitesse maximale de la balle. */
#define MAX_SPEED 2
/* L'augmentation de la vitesse de la balle a chaque rebond. */
#define SPEED_INC 0.1

/* La taille de la balle. */
#define BALL_SIZE 1

#elif GINT_RENDER_RGB && GINT_HW_CG

/* La vitesse de la balle par defaut. */
#define DEFAULT_SPEED 0.6
/* La vitesse maximale de la balle. */
#define MAX_SPEED 6
/* L'augmentation de la vitesse de la balle a chaque rebond. */
#define SPEED_INC 0.3

/* La taille de la balle. */
#define BALL_SIZE 3

#else

#error "Unsupported!"

#endif

/* La valeur de y_inc lorque la balle heurte le haut de la raquette. */
#define ANGLE_TOP -1
/* La valeur de y_inc lorque la balle heurte le bas de la raquette. */
#define ANGLE_BOTTOM 1
/* La l'ecart entre les valeurs de y_inc pour le haut et le bas de la
* raquette. */
#define ANGLE_RANGE 2

Codez maintenant ball_bounce.

Voici mon implémentation de ball_bounce :

void ball_bounce(struct ball *ball, struct paddle *paddle) {
    /* Linear interpolation to calculate the new angle of the ball. */
    int pos_on_the_paddle = ball->y-TO_FIXED(paddle->y);

    /* Clamp pos_on_the_paddle between 0 and TO_FIXED(PADDLE_HEIGHT). */
    if(pos_on_the_paddle < 0){
        pos_on_the_paddle = 0;
    }else if(pos_on_the_paddle > TO_FIXED(PADDLE_HEIGHT)){
        pos_on_the_paddle = TO_FIXED(PADDLE_HEIGHT);
    }

    /* Perform the linear interpolation. */
    ball->y_inc = TO_FIXED(ANGLE_TOP)+DIV(ANGLE_RANGE*pos_on_the_paddle,
                                          TO_FIXED(PADDLE_HEIGHT));
    /* Flip ball->x_inc's sign to have it going towards the other paddle. */
    ball->x_inc = -ball->x_inc;

    /* Increase the ball speed because we hit a paddle. */
    if(ball->speed < TO_FIXED(MAX_SPEED)){
        ball->speed += TO_FIXED(SPEED_INC);
    }
}

Nous allons ensuite utiliser nos deux nouvelles fonctions dans game_logic.

Faites rebondir la balle sur les raquettes et réinitialisez la position de la balle lorsqu'un joueur marque un point. Si le joueur marque, elle ira en direction de l'ordinateur. Sinon elle ira en direction de la raquette controllée par la calculatrice.

Voici ma fonction game_logic maintenant :

void game_logic(struct game *game) {
    switch(game->state){
        case S_INGAME:
            {
                char move;

                /* Permettre au joueur de deplacer sa raquette. */
                paddle_move(&game->paddle1);

                /* Si la balle heurte la raquette du joueur, on la fait
                 * rebondir. */
                if(paddle_will_collide_with_ball(&game->paddle1, &game->ball)){
                    ball_bounce(&game->ball, &game->paddle1);
                }
                /* Si la balle heurte la raquette de l'ordinateur, on la fait
                 * rebondir. */
                if(paddle_will_collide_with_ball(&game->paddle2, &game->ball)){
                    ball_bounce(&game->ball, &game->paddle2);
                }

                /* Deplacement de la balle. */
                move = ball_move(&game->ball);
                if(move == 1){
                    /* On remet la balle au milieu de l'ecran et on l'envoie en
                     * direction de l'ordinateur. */
                    ball_init(&game->ball, TO_FIXED(1), TO_FIXED(1),
                              TO_FIXED(DEFAULT_SPEED));
                }else if(move == 2){
                    /* On remet la balle au milieu de l'ecran et on l'envoie en
                     * direction du joueur. */
                    ball_init(&game->ball, TO_FIXED(-1), TO_FIXED(1),
                              TO_FIXED(DEFAULT_SPEED));
                }
            }

            break;

        default:
            /* Cela ne devrait jamais arriver. */
            game->state = S_INGAME;
    }
}

Testez le jeu, vérifiez qu'il compile bien et que vous pouvez renvoyer la balle.

Mouvement de la raquette de l'ordinateur.

La raquette à droite ne se déplace pas pour l'instant. Corrigeons ça pour que la calculatrice joue contre nous.

Nous allons écrire ce code dans la fonction paddle_computer_move :

/* Déplacement de la raquette par l'ordinateur. */
void paddle_computer_move(struct paddle *paddle, struct ball *ball);

Nous déplacerons la raquette vers le bas, si cela permet au milieu de la raquette de se rapprocher de la balle, ou vers le haut, si cela rapproche le milieu de la raquette de la balle. Nous ferrons attention que la raquette reste bien à l'intérieur de l'écran.

Programmez paddle_computer_move.

Mon implémentation de cette fonction :

void paddle_computer_move(struct paddle *paddle, struct ball *ball) {
    /* Distance between the center of the paddle and the ball on the y axis. */
    int distance = abs(TO_INT(ball->y)-(paddle->y+PADDLE_HEIGHT/2));

    /* Move the paddle up if it reduces its distance to the ball. */
    if(abs(TO_INT(ball->y)-
           (paddle->y+PADDLE_HEIGHT/2-PADDLE_SPEED)) < distance){
        if(paddle->y-PADDLE_SPEED >= 0){
            paddle->y -= PADDLE_SPEED;
        }else{
            paddle->y = 0;
        }
    }else if(abs(TO_INT(ball->y)-
                 (paddle->y+PADDLE_HEIGHT/2+PADDLE_SPEED)) < distance){
        /* Move the paddle down if the ball y position is bigger than the
         * paddle y position. */
        if(paddle->y+PADDLE_SPEED < DHEIGHT-PADDLE_HEIGHT){
            paddle->y += PADDLE_SPEED;
        }else{
            paddle->y = DHEIGHT-PADDLE_HEIGHT-1;
        }
    }
}

Appelez cette fonction dans game_logic.

Voici à quoi ressemble ma fonction game_logic maintenant :

void game_logic(struct game *game) {
    switch(game->state){
        case S_INGAME:
            {
                char move;

                /* Permettre au joueur de deplacer sa raquette. */
                paddle_move(&game->paddle1);
                /* Deplacement de la raquette de l'ordinateur. */
                paddle_computer_move(&game->paddle2, &game->ball);

                /* Si la balle heurte la raquette du joueur, on la fait
                 * rebondir. */
                if(paddle_will_collide_with_ball(&game->paddle1, &game->ball)){
                    ball_bounce(&game->ball, &game->paddle1);
                }
                /* Si la balle heurte la raquette de l'ordinateur, on la fait
                 * rebondir. */
                if(paddle_will_collide_with_ball(&game->paddle2, &game->ball)){
                    ball_bounce(&game->ball, &game->paddle2);
                }

                /* Deplacement de la balle. */
                move = ball_move(&game->ball);
                if(move == 1){
                    /* On remet la balle au milieu de l'ecran et on l'envoie en
                     * direction de l'ordinateur. */
                    ball_init(&game->ball, TO_FIXED(1), TO_FIXED(1),
                              TO_FIXED(DEFAULT_SPEED));
                }else if(move == 2){
                    /* On remet la balle au milieu de l'ecran et on l'envoie en
                     * direction du joueur. */
                    ball_init(&game->ball, TO_FIXED(-1), TO_FIXED(1),
                              TO_FIXED(DEFAULT_SPEED));
                }
            }

            break;

        default:
            /* Cela ne devrait jamais arriver. */
            game->state = S_INGAME;
    }
}

Testez le jeu. Maintenant, nous avons enfin un jeu de pong où l'on peut s'échanger la balle avec la raquette de droite ! Il ne reste plus qu'à compter les points, ajouter un écran de titre et un écran de fin pour afficher qui a gagné, et le jeu est fini !

game.c : comptage des points, écran de fin et écran de titre

Comptage des points

Pour compter les points marqués par le joueur ou l'ordinateur, nous allons ajouter deux membres à struct game : score1 et score2. Ils auront le type unsigned char, car nos scores ne dépasseront pas 255.

Voici ma struct game après ces ajouts :

struct game {
    /* La balle. */
    struct ball ball;

    /* La raquette du joueur. */
    struct paddle paddle1;
    /* La raquette de l'ordinateur. */
    struct paddle paddle2;

    /* Le score du joueur. */
    unsigned char score1;
    /* Le score de l'ordinateur. */
    unsigned char score2;

    /* L'etat dans lequel se trouve le jeu. */
    unsigned char state;
};

score1 et score2 seront initialisés à 0 dans game_init.

Mon implémentation de game_init :

void game_init(struct game *game) {
    /* Initialisation de la balle et initialisation de sa direction pour un
     * deplacement en direction du joueur. */
    ball_init(&game->ball, TO_FIXED(-1), TO_FIXED(1), TO_FIXED(DEFAULT_SPEED));

    /* Initialisation des raquettes aux deux cotes de l'ecran. */
    paddle_init(&game->paddle1, 16);
    paddle_init(&game->paddle2, DWIDTH-16-PADDLE_WIDTH);

    /* Initialisation du score a 0. */
    game->score1 = 0;
    game->score2 = 0;

    game->state = S_INGAME;
}

Nous allons définir MAX_SCORE, le score maximal que l'on peut atteindre dans game.h.

Par exemple :

#define MAX_SCORE 11

Dans notre enumération d'états du jeu dans game.h nous allons ajouter S_END, pour la fin d'une partie, où le gagnant est affiché, et S_TITLE pour l'écran de titre.

Voici mon enumération :

/* The screens. */
enum {
    S_TITLE,
    S_INGAME,
    S_END,

    S_AMOUNT
};

Nous allons incrémenter le score du joueur ou celui de l'ordinateur dans game_logic lorsqu'un point est marqué. Si le score dépasse SCORE_MAX, nous passerons à l'état S_END.

Voici ma fonction game_logic maintenant :

void game_logic(struct game *game) {
    switch(game->state){
        case S_INGAME:
            {
                char move;

                /* Permettre au joueur de deplacer sa raquette. */
                paddle_move(&game->paddle1);
                /* Deplacement de la raquette de l'ordinateur. */
                paddle_computer_move(&game->paddle2, &game->ball);

                /* Si la balle heurte la raquette du joueur, on la fait
                 * rebondir. */
                if(paddle_will_collide_with_ball(&game->paddle1, &game->ball)){
                    ball_bounce(&game->ball, &game->paddle1);
                }
                /* Si la balle heurte la raquette de l'ordinateur, on la fait
                 * rebondir. */
                if(paddle_will_collide_with_ball(&game->paddle2, &game->ball)){
                    ball_bounce(&game->ball, &game->paddle2);
                }

                /* Deplacement de la balle. */
                move = ball_move(&game->ball);
                if(move == 1){
                    /* Le joueur a un point. */
                    game->score1++;

                    /* On remet la balle au milieu de l'ecran et on l'envoie en
                     * direction de l'ordinateur. */
                    ball_init(&game->ball, TO_FIXED(1), TO_FIXED(1),
                              TO_FIXED(DEFAULT_SPEED));

                    /* Si le joueur a eu le score maximal, il gagne, et on va a
                     * l'ecran de fin de partie. */
                    if(game->score1 >= MAX_SCORE) game->state = S_END;
                }else if(move == 2){
                    /* L'ordinateur a un point. */
                    game->score2++;

                    /* On remet la balle au milieu de l'ecran et on l'envoie en
                     * direction du joueur. */
                    ball_init(&game->ball, TO_FIXED(-1), TO_FIXED(1),
                              TO_FIXED(DEFAULT_SPEED));

                    /* Si l'ordinateur a eu le score maximal, il gagne, et on
                     * va a l'ecran de fin de partie. */
                    if(game->score2 >= MAX_SCORE) game->state = S_END;
                }
            }

            break;

        default:
            /* Cela ne devrait jamais arriver. */
            game->state = S_INGAME;
    }
}

Dans game_draw nous allons afficher le score avec dprint.

dprint a le prototype suivant :

void dprint(int x, int y, int fg, char const *format, ...);

Le format est pareil que pour printf.

Voici comment j'ai affiché le score dans game_draw :

/* Affichage du score du joueur. */
dprint(DWIDTH/2-8-5*3, 8, C_BLACK, "%d", game->score1);
/* Affichage du score de l'ordinateur. */
dprint(DWIDTH/2+8, 8, C_BLACK, "%d", game->score2);

Testez le jeu, et vous devriez voir le score changer en haut de votre écran.

Écran de fin

Pour l'écran de fin, dans game_logic, nous allons vérifier si KEY_SHIFT est pressée. Nous allons utiliser keypressed, qui permet de savoir si une touche a été enfoncée depuis la dernière image (ici nous pourrions aussi utiliser keydown).

Si KEY_SHIFT est pressée, nous allons aller à l'écran de titre que nous coderons plus tard.

Gérez l'écran de fin de partie dans game_logic.

Voici mon code :

case S_END:
    /* Si shift est pressee, aller a l'ecran de titre. */
    if(keypressed(KEY_SHIFT)){
        game->state = S_TITLE;
    }

    break;

Dans game_draw nous allons afficher qui a gagné, tout en affichant la balle et les raquettes avec les scores. Nous ne mettrons donc pas de break après avoir traité le cas S_END, au-dessus du code pour traiter le cas où game->state est égal à S_INGAME, pour continuer dans le code pour dessiner la balle, les raquettes et les scores.

NOTE : Les raquettes, les scores et la balle sont donc dessinés au-dessus de ce que l'on dessine pour le cas où game->state est égal à S_INGAME.

Pour éviter un avertissement, il suffit d'ajouter le commentaire /* FALLTHRU */.

Pour afficher qui a gagné, nous utiliserons dtext pour afficher du texte. Voici son prototype dans <gint/display.h> :

void dtext(int x, int y, int fg, char const *str);

Affichez qui a gagné.

Voici comment j'ai procédé :

J'ai utilisé des macros pour calculer où placer le texte :

#define WIN "YOU WIN!"
#define GAME_OVER "GAME OVER"

/* La position du texte GAME_OVER sur l'axe des abscisses. */
#define GAME_OVER_X (DWIDTH/2-5*sizeof(GAME_OVER)/2)
/* La position du texte WIN sur l'axe des abscisses. */
#define WIN_X (DWIDTH/2-5*sizeof(WIN)/2)
/* La position du texte WIN ou GAME_OVER sur l'axe des ordonnees. */
#define END_Y (DHEIGHT/2-7/2)

Voici ma fonction game_draw :

void game_draw(struct game *game) {
    switch(game->state){
        case S_END:
            if(game->score1 > game->score2){
                /* Le joueur a gagne, donc on l'affiche. */
                dtext(WIN_X, END_Y, C_BLACK, WIN);
            }else{
                /* Le joueur a perdu, on l'affiche. */
                dtext(GAME_OVER_X, END_Y, C_BLACK, GAME_OVER);
            }

            /* Do not break here to draw the in-game screen. */
            /* FALLTHRU */

        case S_INGAME:
            /* Affichage du score du joueur. */
            dprint(DWIDTH/2-8-5*3, 8, C_BLACK, "%d", game->score1);
            /* Affichage du score de l'ordinateur. */
            dprint(DWIDTH/2+8, 8, C_BLACK, "%d", game->score2);

            /* Dessin des raquettes. */
            paddle_draw(&game->paddle1);
            paddle_draw(&game->paddle2);

            /* Dessin de la balle. */
            ball_draw(&game->ball);

            break;

        default:
            /* Cela ne devrait jamais arriver. */
            game->state = S_INGAME;
    }
}

Testez, est-ce que l'écran de fin s'affiche correctement ?

Écran de titre

Nous allons maintenant coder la dernière chose manquante à notre jeu : l'écran de titre.

Normalement vous avez déjà réalisé des jolis écrans de titre : un de 128 × 64 pour les calculatrices monochromes, et un autre d'une dimension de 396 × 224 pixels pour les calculatrices couleur.

la variable qui contient les données de cette image (qui est différente selon la plateforme) s'appelle title_img comme nous l'avons indiqué dans fxconv_metadata.txt.

Pour pouvoir y accéder, il suffit d'ajouter la ligne suivante à game.c :

extern const bopti_image_t title_img;

Ensuite, dans game_draw, lorsque game->state est égal à S_TITLE, nous allons remplir l'écran avec cette image en utilisant dimage.

Le prototype de dimage dans <gint/display.h> :

void dimage(int x, int y, bopti_image_t const *image);

Gérez le cas où game->state est égal à S_TITLE dans game_draw.

Voici mon implémentation :

void game_draw(struct game *game) {
    switch(game->state){
        case S_TITLE:
            /* Affichage de l'image de l'écran de titre. */
            dimage(0, 0, &title_img);

            break;

        case S_END:
            if(game->score1 > game->score2){
                /* Le joueur a gagne, donc on l'affiche. */
                dtext(WIN_X, END_Y, C_BLACK, WIN);
            }else{
                /* Le joueur a perdu, on l'affiche. */
                dtext(GAME_OVER_X, END_Y, C_BLACK, GAME_OVER);
            }

            /* Do not break here to draw the in-game screen. */
            /* FALLTHRU */

        case S_INGAME:
            /* Affichage du score du joueur. */
            dprint(DWIDTH/2-8-5*3, 8, C_BLACK, "%d", game->score1);
            /* Affichage du score de l'ordinateur. */
            dprint(DWIDTH/2+8, 8, C_BLACK, "%d", game->score2);

            /* Dessin des raquettes. */
            paddle_draw(&game->paddle1);
            paddle_draw(&game->paddle2);

            /* Dessin de la balle. */
            ball_draw(&game->ball);

            break;

        default:
            /* Cela ne devrait jamais arriver, mais si cela arrive, nous
             * retournons a l'ecran de titre. */
            game->state = S_TITLE;
    }
}

NOTE : J'ai changé l'état auquel nous allons lorsque game->state contient une valeur inattendue : on va maintenant à l'écran de titre.

Traitons ce cas aussi dans game_logic. Si KEY_SHIFT est pressée, nous allons réinitialiser le jeu (avec game_init) et passer à l'état S_INGAME.

NOTE : Ici nous devons utiliser keypressed (ou keyreleased) : Lorsque le jeu se trouve à l'état S_END, et que le joueur appuie sur shift, le jeu passe à l'état S_TITLE et il est très probable que le joueur n'ait pas encore relaché la touche, donc keydown retournerait true. Le jeu passerait donc directement à l'état S_INGAME (après avoir appelé game_init).

Gérez le passage de l'écran de titre à l'état S_INGAME.

Voici mon implémentation :

void game_logic(struct game *game) {
    switch(game->state){
        case S_TITLE:
            /* On attend que la touch shift soit pressee pour lancer une
             * partie. */
            if(keypressed(KEY_SHIFT)){
                /* Reinitialisation du jeu. */
                game_init(game);
                game->state = S_INGAME;
            }

            break;

    switch(game->state){
        case S_INGAME:
            {
                char move;

                /* Permettre au joueur de deplacer sa raquette. */
                paddle_move(&game->paddle1);
                /* Deplacement de la raquette de l'ordinateur. */
                paddle_computer_move(&game->paddle2, &game->ball);

                /* Si la balle heurte la raquette du joueur, on la fait
                 * rebondir. */
                if(paddle_will_collide_with_ball(&game->paddle1, &game->ball)){
                    ball_bounce(&game->ball, &game->paddle1);
                }
                /* Si la balle heurte la raquette de l'ordinateur, on la fait
                 * rebondir. */
                if(paddle_will_collide_with_ball(&game->paddle2, &game->ball)){
                    ball_bounce(&game->ball, &game->paddle2);
                }

                /* Deplacement de la balle. */
                move = ball_move(&game->ball);
                if(move == 1){
                    /* Le joueur a un point. */
                    game->score1++;

                    /* On remet la balle au milieu de l'ecran et on l'envoie en
                     * direction de l'ordinateur. */
                    ball_init(&game->ball, TO_FIXED(1), TO_FIXED(1),
                              TO_FIXED(DEFAULT_SPEED));

                    /* Si le joueur a eu le score maximal, il gagne, et on va a
                     * l'ecran de fin de partie. */
                    if(game->score1 >= MAX_SCORE) game->state = S_END;
                }else if(move == 2){
                    /* L'ordinateur a un point. */
                    game->score2++;

                    /* On remet la balle au milieu de l'ecran et on l'envoie en
                     * direction du joueur. */
                    ball_init(&game->ball, TO_FIXED(-1), TO_FIXED(1),
                              TO_FIXED(DEFAULT_SPEED));

                    /* Si l'ordinateur a eu le score maximal, il gagne, et on
                     * va a l'ecran de fin de partie. */
                    if(game->score2 >= MAX_SCORE) game->state = S_END;
                }
            }

            break;

        default:
            /* Cela ne devrait jamais arriver, mais si cela arrive, nous
             * retournons a l'ecran de titre. */
            game->state = S_TITLE;
    }
}

NOTE : J'ai changé l'état auquel nous allons lorsque game->state contient une valeur inattendue : on va maintenant à l'écran de titre.

Corrigez maintenant game_init pour se trouver à l'écran de titre au lancement du jeu.

Ma fonction game_init :

void game_init(struct game *game) {
    /* Initialisation de la balle et initialisation de sa direction pour un
     * deplacement en direction du joueur. */
    ball_init(&game->ball, TO_FIXED(-1), TO_FIXED(1), TO_FIXED(DEFAULT_SPEED));

    /* Initialisation des raquettes aux deux cotes de l'ecran. */
    paddle_init(&game->paddle1, 16);
    paddle_init(&game->paddle2, DWIDTH-16-PADDLE_WIDTH);

    /* Initialisation du score a 0. */
    game->score1 = 0;
    game->score2 = 0;

    game->state = S_TITLE;
}

Testez, et vérifiez que tout fonctionne correctement. Le jeu est fini.

Le mot de la fin

Bravo ! Vous êtes arrivés au bout de ce tutoriel ! Vous avez développé tout un jeu avec gint !

Si vous cherchez des informations sur des fonctions de gint, l'un des meilleures ressources sont les en-têtes de gint, elles sont très bien documentées.

Les topics de gint, du fxsdk et le tutoriel officiel d'utilisation de gint sont aussi très utiles :


Et un grand merci à Lephé pour avoir développé ce super SDK !


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

Citer : Posté le 19/07/2026 21:57 | #


Super tutoriel Mb88, je propose qu'il soit ajouté aux tutoriels de Planete Casio pour le dev d'addins.
J'espère pendant les vacances être en capacité de terminer ma serie "Papy Sly" sur le dev du tower defense en C++, comme cela on aura un set "up to date" de tutos sur gint/fxsdk.

En tout cas, bravo pour le boulot car faire un tuto est un exercice vraiment très long.
There are only 10 types of people in the world: Those who understand binary, and those who don't ...
Mb88 Hors ligne Rédacteur Points: 1334 Défis: 3 Message

Citer : Posté le 19/07/2026 22:04 | #


Merci ! J'espère qu'il est assez facile à suivre et qu'il n'y a pas trop de coquilles (je ne l'ai pas vraiment relu, je l'ai juste survolé une fois que je l'avais fini et j'ai cherché des fautes avec LanguageTool – je sais Grammalecte est probablement meilleur pour le français).

EDIT :

Et j'espère avoir bien utilisé gint et que je n'ai pas dit de bêtises sur le fonctionnement de certaines de ses fonctions.
Slyvtt En ligne Maître du Puzzle Points: 2999 Défis: 17 Message

Citer : Posté le 19/07/2026 22:26 | #


Je n'ai rien vu de choquant.

Juste, on voit que tu as principalement fait le tuto pour les monochromes car il y a pas mal de petits détails qui sont très importants pour la fxCG50 et autres versions couleurs, notamment de les fichiers fxconv-metadata.txt, il faut absolument spécifier le format final de la conversion (P4, P8, avec ou sans alpha), car sinon le convertisseur ne saura pas quoi choisir. Cela aura aussi un énorme impact sur la taille du binaire généré.

Il faudrait aussi peut être évoquer le cas de fontes/polices car c'est un sujet pas toujours facile quand on débute, notamment sur les divers paramètres et la taille de la grille, proportionnelle ou non, etc...

Sinon pour le reste, cela me semble plutôt bien, je ferai une lecture plus fine un de ces 4.
There are only 10 types of people in the world: Those who understand binary, and those who don't ...

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:  ^^  >:)

Σ π θ ± α β γ δ Δ σ λ
captcha
Rafraîchissez la page si vous souhaitez obtenir un nouveau CAPTCHA.

Planète Casio v4.3 © créé par Neuronix et Muelsaco 2004 - 2026 | Il y a 85 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