Tutoriel d'utilisation de libMicrofx
Posté le 25/08/2024 13:22
Pour que le tutoriel soit plus visible que dans le repo de mon jeu de pong ( https://git.planet-casio.com/mibi88/pong/src/branch/master/TUTORIAL_fr.md ), je le poste ici aussi. J'ai utilisé https://rtxa.github.io/mdtobb/ pour la conversion du markdown vers du BBCode. J'ai réécrit la table à la main. J'ai aussi réécrit les listes.
Tutoriel d'utilisation de libMicrofx
Dans ce tutoriel nous allons coder un jeu de pong en C avec libMicrofx.
Préréquis
Des connaissances solides du C sont requises.
Pour pouvoir suivre ce tutoriel, vous avez besoin du fxsdk, car libMicrofx réquis sh-elf-gcc etc. et fxgxa. 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 sh-4 pour pouvoir tester l'add-in.
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, car l'écran fait 128x64 pixels.
Création du projet
Clonez le dépot libMicrofx (branche master). Faites une copie du dossier template, c'est un exemple de projet. Renommez votre copie de ce dossier avec le nom de votre nouveau projet (par exemple: pong).
Lancez
make dans ce dossier. Si vous n'avez pas d'erreurs et obtenez un add-in fonctionnel, vous pouvez continuer.
Dans le dossier du projet, crééz un dossier
assets pour y mettre les graphismes, ainsi qu'un dossier
inc pour les headers.
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 (si vous ne savez pas ce que c'est, c'est pas grave, j'en parlerais plus tard).
Créez ces fichiers, les fichiers .c dans
src et les fichier .h dans
inc.
Dans assets, créez un écran de titre (
title_img.png) pour le jeu. Il doit avoir une dimension de 128x64 (la taille de l'écran). Vous pouvez aussi prendre celui du dépôt du tutoriel. Pour pouvoir l'utiliser depuis le code, il faut le convertir en code source. Vous pouvez utiliser sprite coder pour cela (
https://tools.planet-casio.com/SpriteCoder/index.php).
Vous devriez avoir un dossier comme ça :
$ tree
.
├── assets
│ ├── title_img.c
│ └── title_img.png
├── icon.png
├── inc
│ ├── ball.h
│ ├── fixed.h
│ ├── game.h
│ └── paddle.h
├── lib
│ ├── fx98xx.ld
│ ├── include
│ │ └── microfx
│ │ ├── ext
│ │ │ ├── img.h
│ │ │ └── strtools.h
│ │ ├── keycodes.h
│ │ └── microfx.h
│ └── libMicrofx.a
├── Makefile
└── src
├── ball.c
├── game.c
├── main.c
└── paddle.c
Makefile
Ajoutez les fichiers .c à SRC, changez NAME et ajoutez
-Wall -Wextra -Wpedantic à la ligne
$(CC) -c $< -o $@ -Os -Ilib/include/ -std=c89
pour avoir plus de warnings, pour rendre le débogage plus facile.
Ajoutez aussi
-Iinc, pour ne pas avoir à écrire le chemin vers les headers.
Si vous n'avez pas l'habitude du C ANSI enlevez
-std=c89.
Code
Nous sommes enfin prêts à coder le jeu.
- 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 à lequel 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 voir avec
kcheck(KCEXIT).
J'ai donc choisi de limiter le framerate à 50fps (cela vous rapelle peut être les jeux vidéos PAL des années 80).
libMicrofx propose une variété de fonctions pour controller le temps. Nous allons utiliser
treset,
tgetticks et
tiselapsed, car ils nous offrent la meilleure précision que libMicrofx est capable de fournir (1/128 secondes). Si vous avez besoin d'une plus grande précision, utilisez gint.
tgetticks permet d'obtenir le temps écoulé depuis minuit.
tiselapsed permet de savoir si X milisecondes se sont écoulés depuis un certain nombre de ticks. Nous allons nous en servir pour attendre de 20ms s'écoulent depuis le début de l'image.
treset permet de réinitialiser le comptage du temps. On en a besoin, car au bout de 24h le temps est remis à zéro, ce qui pourrait faire crasher notre jeu.
Nous allons donc d'abord réinitialiser le RTC et ensuite utiliser
tgetticks pour obtenir le temps écoulé. Nous allons ensuite exécuter une image du jeu, puis, dans une boucle, utiliser
tiselapsed pour attendre que 20ms soient écoulées.
sclear efface l'écran et
supdate met à jour l'écran donc mettez les entre
tgetticks et
tiselapsed.
Essayez par vous même, et regardez ensuite mon implémentation.
while(!kcheck(KCEXIT)){
/* Reset RTC to avoid a crash if it gets midnight. */
treset();
/* Get the current time. */
ticks = tgetticks();
/* Run a frame of the game. */
sclear();
/* A game frame. */
supdate();
/* Wait that 20ms are elapsed to let the game run at 50fps (PAL). */
while(!tiselapsed(ticks, 20));
}
Testez. Si vous arrivez bien à quitter le jeu en appuyant sur EXIT, continuez.
- game.h, les structures
Maintenant que nous avons une boucle de jeu, nous pouvons commencer à coder le jeu en lui même.
Nous allons créer trois structures,
Game, qui rassemble toutes les informations sur le jeu,
Ball, qui représente la balle, et
Paddle qui représente une raquette.
Nous allons mettre toutes les structures dans
game.h (pour éviter le cross referencing).
Nous allons commencer par créer la structure
Paddle, c'est celle qui nous intéressera en premier. Celle pour la balle sera plus compliquée.
Nous avons juste besoin de stocker la position de la raquette.
/* A structure representing a paddle (used in paddle.c) */
typedef struct {
/* The position of the paddle. */
int x;
int y;
} Paddle;
Ensuite nous allons créer la structure
Game avec deux raquettes.
/* A structure storing all the informations about the game. */
typedef struct {
/* The player's paddle */
Paddle paddle1;
/* The computer's paddle */
Paddle paddle2;
} Game;
- paddle.c et paddle.h, gestion des raquettes.
Maintenant que nous avons crée toutes les structures, il est temps de coder le mouvement et l'affichage des raquettes.
- Initialisation d'une raquette.
Dans paddle.h, écrivez, comme vous en avez sûrement l'habitude
#ifndef PADDLE_H
#define PADDLE_H
#include <game.h>
#endif
Dans paddle.c, nous allons commencer par coder inclure
paddle.h. Nous allons aussi inclure
microfx/microfx.h.
#include <paddle.h>
#include <microfx/microfx.h>
Nous allons créer une fonction
paddle_init pour initialiser la raquette:
void paddle_init(Paddle *paddle, int x) {
/* TODO */
}
Dans cette fonction, nous allons mettre
paddle->y à
SHEIGHT/2:
SWIDTH et
SHEIGHT sont définis par
microfx/microfx.h et
SWIDTH contient la largeur de l'écran.
SHEIGHT contient sa hauteur.
paddle->x sera égal à
x.
void paddle_ini(Paddle *paddle, int x) {
paddle->x = x;
paddle->y = SHEIGHT/2;
}
Mettez le prototype de cette fonction dans
paddle.h.
- Mouvement d'une raquette.
Nous allons ensuite créer une fonction
paddle_move pour pouvoir bouger la raquette.
void paddle_move(Paddle *paddle) {
/* TODO */
}
Comme nous l'avons fait précédemment, nous allons utiliser
kcheck pour récupérer les entrées clavier.
KCUP est la flèche vers le haut et
KCDOWN est la flèche vers le bas.
Dans
paddle.h nous allons définir la vitesse du paddle, ansi que sa taille.
#define PADDLE_SPEED 1
#define PADDLE_WIDTH 3
#define PADDLE_HEIGHT 16
Nous n'allons pas bêtement incrémenter et décrémenter la position du paddle, car il ne doit pas sortir de l'écran.
Mon implémentation :
void paddle_move(Paddle *paddle) {
/* Move the paddle up if the up arrow key is pressed. */
if(kcheck(KCUP)){
if(paddle->y-PADDLE_SPEED >= 0){
paddle->y -= PADDLE_SPEED;
}else{
paddle->y = 0;
}
}
/* Move the paddle down if the down arrow key is pressed. */
if(kcheck(KCDOWN)){
if(paddle->y+PADDLE_SPEED < SHEIGHT-PADDLE_HEIGHT){
paddle->y += PADDLE_SPEED;
}else{
paddle->y = SHEIGHT-PADDLE_HEIGHT-1;
}
}
}
Mettez le prototype de cette fonction dans
paddle.h.
- Dessin d'une raquette
Maintenant que notre raquette peut être initialisée et bougée, nous devons la dessiner. Nous allons dessiner un rectangle.
Nous allons donc créer une fonction pour dessiner une raquette, et dessiner un rectangle à la position de la raquette, avec
void srect(int x1, int y1, int x2, int y2);
Mon implémentation :
void paddle_draw(Paddle *paddle) {
/* Draw a rectangle to represent the paddle. */
srect(paddle->x, paddle->y, paddle->x+PADDLE_WIDTH,
paddle->y+PADDLE_HEIGHT);
}
Mettez le prototype de cette fonction dans
paddle.h.
- game.c et game.h, initialisation, rendu et logique
Maintenant nous avons grand besoin de tester si tout ce que nous avons codé fonctionne.
Nous allons donc créer trois fonctions:
- game_init pour initialiser le jeu.
- game_logic pour la logique du jeu.
- game_draw pour le rendu.
- Initialisation du jeu.
Dans
game_init nous allons créer une première raquette avec une abscisse de
16 et une deuxième avec une abscisse de
SWIDTH-16-PADDLE_WIDTH.
Mon implémentation :
void game_init(Game *game) {
/* Initialize the paddles on the two sides of the screen. */
paddle_init(&game->paddle1, 16);
paddle_init(&game->paddle2, SWIDTH-16-PADDLE_WIDTH);
}
Ajoutez le prototype de cette fonction à
game.h.
- Logique du jeu.
Dans
game_logic nous allons appeler
paddle_move pour la première raquette.
Mon implémentation :
void game_logic(Game *game) {
/* Let the player move his paddle. */
paddle_move(&game->paddle1);
}
Ajoutez le prototype de cette fonction à
game.h.
- Rendu
Dans
game_draw nous allons dessiner les raquettes avec
paddle_draw.
Mon implémentation :
void game_draw(Game *game) {
/* Draw the paddles. */
paddle_draw(&game->paddle1);
paddle_draw(&game->paddle2);
}
Ajoutez le prototype de cette fonction à
game.h.
- main.c, appel des fonctions de game.c
Avant de pouvoir tester, nous devons appeler toutes ces nouvelles fonctions.
Créez une variable game, et au début de main, et initialisez la avec
game_init. Ensuite, dans la boucle du jeu, après
sclear et avant
supdate, appelez
game_logic puis
game_draw.
Mon implémentation de
main :
Game game;
int main(void) {
int ticks = 0;
/* Wait for the user to release all the keys */
while(kisdown());
/* Initialize the game. */
game_init(&game);
/* Main game loop. */
while(!kcheck(KCEXIT)){
/* Reset RTC to avoid a crash if it gets midnight. */
treset();
/* Get the current time. */
ticks = tgetticks();
/* Run a frame of the game. */
sclear();
game_logic(&game);
game_draw(&game);
supdate();
/* Wait that 20ms are elapsed to let the game run at 50fps (PAL). */
while(!tiselapsed(ticks, 20));
}
/* Add-ins should return 1 on success (yes, it's weird). */
return 1;
}
- game.h et fixed.h, la balle et les nombres à virgule fixe.
- Opérations à virgule fixe.
Comme vous le savez sûrement déjà, l'écran de la calculatrice fait 128x64 pixels. Si l'on dessine un pixel à (0;0), et qu'on le déplace de 1 pixel sur l'axe des abscisses 50 fois par seconde, le pixel traverse l'écran en 2,56 secondes. C'est très rapide. Nous avons donc besoin de bouger le pixel de moins d'un pixel par image, mais comment faire ?
C'est très simple: nous allons stocker sa position comme un nombre réel, et l'incrémenter d'un nombre plus petit que 1 (ex: 0,1), ce qui nous permet de gérer la vitesse de son déplacement plus précisément. Pour trouver ses coordonnés à l'écran, nous ne prendrons que les chiffres avant la virgule.
Intuitivement, vous utiliseriez des
floats mais la calculatrice n'a pas de FPU (une puce qui permet de faire des calculs à virgule flottante rapidement), donc tout est effectué sur le CPU ce qui rend les calculs avec des
float très lent. De plus cela rend l'add-in plus volumineux (ce que nous ne voulons pas).
Nous allons donc utiliser des entiers : nous allons garder quelque bits pour les chiffres après la virgule. C'est ce que l'on appelle les nombres à virgule fixe.
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 |
[/table]
Si l'on reconvertit ce chiffre en float, on obtient environ 19,199, car 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 !).
Maintenant, codons quelque macros pour convertir des nombres en nombres à virgule fixe, 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.
/* The precision of the fixed point numbers. */
#define PRECISION 8
/* Convert a float to a fixed point number. */
#define TO_FIXED(num) (int)((num)*(1<<PRECISION))
/* Convert a fixed point number to an integer. */
#define TO_INT(num) ((num)>>PRECISION)
Note : Je multiplie num par (1*PRECISION) pour pouvoir passer des floats à la macro.
Les macros pour la multiplication et la division sont très simples, elles aussi :
/* Multiply two fixed point numbers together. */
#define MUL(a, b) ((a*b)>>PRECISION)
#define DIV(a, b) ((a<<PRECISION)/b)
Nous allons finir par définir un type
fixed_t, pour que notre code soit plus lisible :
/* The fixed point type (to make the code easier to read). */
typedef int fixed_t;
- Structure Ball.
Dans game.h, nous pouvons donc créer une structure pour la balle, qui va contenir sa position, sa direction (sous forme de vecteur) et sa vitesse.
/* The ball structure (used in ball.c). */
typedef struct {
/* The position of the ball. */
fixed_t x;
fixed_t y;
/* How the ball position is increased on each frame. */
fixed_t x_inc;
fixed_t y_inc;
/* The speed of the ball. */
fixed_t speed;
} Ball;
Nous allons aussi l'ajouter à la structure
Game:
/* The ball */
Ball ball;
- ball.c, ball.h et game.c, le dessin et le mouvement de la balle.
- Initialisation de la balle.
Avant de faire quoi que ce soit avec la balle, nous devons l'initialiser. Nous allons donc créer une fonction
ball_init dans ball.c. Elle aura pour paramètres la direction et la vitesse de la balle. Elle sera placée au milieu de l'écran.
Mon implémentation :
void ball_init(Ball *ball, fixed_t x_inc, fixed_t y_inc, int speed) {
/* Set the ball position to the center of the screen. */
ball->x = TO_FIXED(SWIDTH/2);
ball->y = TO_FIXED(SHEIGHT/2);
/* Set the increase on the x and y axis and the speed of the ball. */
ball->x_inc = x_inc;
ball->y_inc = y_inc;
ball->speed = speed;
}
Ajoutez le prototype à ball.h et appelez cette fonction depuis game_init, pour l'initialiser au lancement du jeu.
- Dessin de la balle
Mainenant que nous avons initialisé la balle, nous pouvons la dessiner. Nous allons tout simplement récupérer ses coordonnés à l'écran, et dessiner un rectangle de 3x3 que nous allons remplir (vu que l'intérieur ne fait qu'un pixel, nous allons utiliser spixel), pour qu'elle soit assez visible.
void ball_draw(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. */
srect(screen_x-1, screen_y-1, screen_x+1, screen_y+1);
spixel(screen_x, screen_y, SBLACK);
}
Nous allons appeler cette fonction dans
game_draw.
Vérifiez que tout fonctionne, et si tout est bon, vous pouvez continuer. Sinon relisez votre code et le tutoriel.
- Mouvement de la balle
Mainenant que nous dessinons la balle, nous pouvons la faire bouger. Nous allons créer une nouvelle fonction
char ball_move(Ball *ball); et commencer par calculer la position qu'elle atteindra à l'écran :
/* The destination coordinates (on the screen) */
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));
Nous allons tout d'abord vérifier si la balle sortira du côté droit ou gauche de l'écran, pour pouvoir donner un point au joueur qui a marqué.
/* If dest_x > SWIDTH the player should get a point. */
if(dest_x > SWIDTH) return 1;
/* If dest_x < 0 the computer should get a point. */
if(dest_x < 0) return 2;
Nous pouvons aussi vérifier si elle doit rebondir sur les bords de l'écran, et nous allons la faire rebondir si besoin :
/* Make the ball bounce on the sides of the screen. */
if(dest_y < 0){
ball->y_inc = -ball->y_inc;
ball->y = TO_FIXED(0);
}
if(dest_y >= SHEIGHT){
ball->y_inc = -ball->y_inc;
ball->y = TO_FIXED(SHEIGHT-1);
}
Nous allons finir par faire bouger la balle :
/* Move the ball. */
ball->x += MUL(ball->x_inc, ball->speed);
ball->y += MUL(ball->y_inc, ball->speed);
Mon implémentation :
char ball_move(Ball *ball) {
/* The destination coordinates (on the screen) */
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));
/* If dest_x > SWIDTH the player should get a point. */
if(dest_x > SWIDTH) return 1;
/* If dest_x < 0 the computer should get a point. */
if(dest_x < 0) return 2;
/* Make the ball bounce on the sides of the screen. */
if(dest_y < 0){
ball->y_inc = -ball->y_inc;
ball->y = TO_FIXED(0);
}
if(dest_y >= SHEIGHT){
ball->y_inc = -ball->y_inc;
ball->y = TO_FIXED(SHEIGHT-1);
}
/* Move the ball. */
ball->x += MUL(ball->x_inc, ball->speed);
ball->y += MUL(ball->y_inc, ball->speed);
return 0;
}
Appelez cette fonction depuis
game_logic, et récupérez ce qu'elle retourne, on en aura besoin.
Si la balle sort, réinitialisez la, pour pouvoir tester ce que nous venons de coder.
- paddle.h et paddle.c, collisions avec la balle et paddle de l'ordinateur
- Collision avec le paddle
Maintenant que nous avons une balle qui bouge, nous devonss pouvoir vérifier lorsqu'elle touche le paddle. Pour la simplicité de ce tutoriel, je garderais les collisions très simple, mais n'hésitez pas à les améliorer (en vérifiant pour une collision des deux côtés du paddle). Créez une fonction
paddle_will_collide_with_ball qui a pour paramètres la raquette et la balle. Elle vérifiera si, lors du prochain mouvement de la balle, elle entrera en collision avec le paddle.
Nous allons commencer par calculer la position de la balle sur l'axe des abscisses après son mouvement :
/* Get the position on the x axis the ball will go to. */
fixed_t dest_x = ball->x+MUL(ball->x_inc, ball->speed);
Ensuite nous allons vérifier si la balle passera d'un côté à l'autre du côté gauche du paddle. Si c'est le cas, nous retournons 1 :
/* Check if the ball would go through the paddle (on the X axis). */
if((ball->x > TO_FIXED(paddle->x) && dest_x <= TO_FIXED(paddle->x)) ||
(ball->x < TO_FIXED(paddle->x) && dest_x >= TO_FIXED(paddle->x))){
/* Check if the ball is on the paddle on the Y axis. */
if(ball->y >= TO_FIXED(paddle->y) &&
ball->y < TO_FIXED(paddle->y+PADDLE_HEIGHT)){
/* The ball will hit the paddle. */
return 1;
}
}
Sinon, retournez 0.
Appelez cette fonction depuis
game_logic et inversez
y_inc lors d'une collision pour pouvoir tester les collisions :
if(paddle_will_collide_with_ball(&game->paddle1, &game->ball)){
game->ball.y_inc = -game->ball.y_inc;
}
if(paddle_will_collide_with_ball(&game->paddle2, &game->ball)){
game->ball.y_inc = -game->ball.y_inc;
}
- Mouvement du paddle de l'ordinateur
Nous pouvons maintenant faire bouger le paddle de l'ordinateur en fonction de la position de la balle, maintenant que nous pouvons la renvoyer. Créez une fonction
paddle_computer_move qui prend pour paramètres le paddle à bouger et la balle.
Si la position à l'écran, sur l'axe des ordonnées, de la balle est plus petite que la position de la raquette, nous allons la monter, et si elle est plus grande nous allons la monter. Sinon nous allons rien faire vu que la raquette est en face de la balle.
void paddle_computer_move(Paddle *paddle, Ball *ball) {
/* Move the paddle up if the ball y position is smaller than the paddle y
* position. */
if(TO_INT(ball->y) < paddle->y+PADDLE_HEIGHT/2){
if(paddle->y-PADDLE_SPEED >= 0){
paddle->y -= PADDLE_SPEED;
}else{
paddle->y = 0;
}
}else if(TO_INT(ball->y) > paddle->y+PADDLE_HEIGHT/2){
/* Move the paddle down if the ball y position is bigger than the paddle
* y position. */
if(paddle->y+PADDLE_SPEED < SHEIGHT-PADDLE_HEIGHT){
paddle->y += PADDLE_SPEED;
}else{
paddle->y = SHEIGHT-PADDLE_HEIGHT-1;
}
}
}
- game.c et game.h, affichage des scores.
- Initialisation et affichage des scores.
Nous avons déjà (presque) un jeu fonctionnel, mais nous devons compter le score, pour pouvoir savoir qui a gagné et qui a perdu.
Nous allons commencer par ajouter deux variables à la structure Game pour le stocker :
/* The player's score */
unsigned char score1;
/* The computer's score */
unsigned char score2;
Nous allons ensuite l'initialiser dans
game_init :
/* Reset the score. */
game->score1 = 0;
game->score2 = 0;
Pour l'afficher, nous allons utiliser
itoa pour convertir le score en chaine de caractères et
stext pour l'afficher à l'écran.
Les scores sont des
unsigned char, donc un tableau de 4 caractères sera suffisant.
Pour afficher le score, nous allons d'abord appeler
itoa pour obtenir une chaine de caractères :
itoa(game->score1, score_buffer);
Ensuite nous pouvons l'afficher avec
stext :
stext(SWIDTH/2-8-5*3, 8, score_buffer, SBLACK);
Les deux premiers arguments sont la position à laquelle afficher le texte, le troisième, le texte à afficher et le dernier argument est la couleur (
SWHITE ou
SBLACK).
Nous allons faire de même pour le score de l'ordinateur ce qui nous donne
/* Convert the score to a string. */
itoa(game->score1, score_buffer);
/* Display the score */
stext(SWIDTH/2-8-5*3, 8, score_buffer, SBLACK);
/* Do the same with the score of the computer. */
itoa(game->score2, score_buffer);
stext(SWIDTH/2+8, 8, score_buffer, SBLACK);
- Incrémentation des scores.
Le nombre retourné par
ball_move va enfin nous être utile ! Si il retourne 1 le joueur obtient un point, sinon c'est l'ordinateur qui obtient un point :
move = ball_move(&game->ball);
if(move == 1){
/* Player 1 got a point. */
game->score1++;
/* Put the ball in the middle and send it to the computer */
ball_init(&game->ball, TO_FIXED(1), TO_FIXED(1),
TO_FIXED(DEFAULT_SPEED));
}
if(move == 2){
/* Player 2 got a point. */
game->score2++;
/* Put the ball in the middle and send it to the player */
ball_init(&game->ball, TO_FIXED(-1), TO_FIXED(1),
TO_FIXED(DEFAULT_SPEED));
}
- game.c, écran de titre et écran de fin du jeu.
- Mise en place
Lorsque le joueur ou l'ordinateur atteint un certain score, il faut que la partie se termine et que le joueur puisse recommencer. Nous avons donc besoin d'ajouter un écran de fin. Nous allons aussi ajouter un écran de titre.
Nous allons définir une énumération dans game.h qui liste les différents écrans :
/* The screens. */
typedef enum {
S_TITLE,
S_INGAME,
S_END,
S_AMOUNT
} State;
Nous allons stocker l'écran actuel dans la structure Game :
/* What we're currently displaying */
unsigned char state;
Ensuite, nous allons l'initialiser dans
game_init :
/* Display the title screen. */
game->state = S_TITLE;
Nous allons créer un grand switch dans
game_logic et
game_draw pour effectuer des actions différentes selon l'écran affiché. Nous allons mettre le code que nous y avons actuellement dans le cas
S_INGAME.
Dans
game_logic :
switch(game->state){
case S_TITLE:
break;
case S_INGAME:
/* Let the player move his paddle. */
paddle_move(&game->paddle1);
/* Let the computer move his paddle. */
paddle_computer_move(&game->paddle2, &game->ball);
/* If the ball hits the player paddle, make it bounce off of it.
*/
if(paddle_will_collide_with_ball(&game->paddle1, &game->ball)){
game->ball.y_inc = -game->ball.y_inc;
}
/* If the ball hits the computer paddle, make it bounce off of it.
*/
if(paddle_will_collide_with_ball(&game->paddle2, &game->ball)){
game->ball.y_inc = -game->ball.y_inc;
}
move = ball_move(&game->ball);
if(move == 1){
/* Player 1 got a point. */
game->score1++;
/* Put the ball in the middle and send it to the computer */
ball_init(&game->ball, TO_FIXED(1), TO_FIXED(1),
TO_FIXED(DEFAULT_SPEED));
}
if(move == 2){
/* Player 2 got a point. */
game->score2++;
/* Put the ball in the middle and send it to the player */
ball_init(&game->ball, TO_FIXED(-1), TO_FIXED(1),
TO_FIXED(DEFAULT_SPEED));
}
break;
case S_END:
break;
default:
/* This should never happen, but if it happens we go to the title
* screen. */
game->state = S_TITLE;
}
Dans
game_draw :
switch(game->state){
case S_TITLE:
break;
case S_END:
/* Coming soon */
case S_INGAME:
/* Convert the score to a string. */
itoa(game->score1, score_buffer);
/* Display the score */
stext(SWIDTH/2-8-5*3, 8, score_buffer, SBLACK);
/* Do the same with the score of the computer. */
itoa(game->score2, score_buffer);
stext(SWIDTH/2+8, 8, score_buffer, SBLACK);
/* Draw the paddles. */
paddle_draw(&game->paddle1);
paddle_draw(&game->paddle2);
/* Draw the ball. */
ball_draw(&game->ball);
default:
/* This should never happen, but if it happens we go to the title
* screen. */
game->state = S_TITLE;
}
Note : Je n'ai pas mis de break à la fin de
S_END, et je l'ai mis avant
S_INGAME dans
game_draw pour toujours afficher l'écran de jeu lorsque la partie est terminé. On écrira juste du texte en plus ("GAME OVER" ou "YOU WIN").
- Écran de titre
Nous allons commencer par ajouter l'écran de titre. Dans
game_draw nous allons juste dessiner l'image de l'écran de titre :
/* Show the title image. */
simage(0, 0, 128, 64, (unsigned char*)title_img, SNORMAL);
Les deux premiers paramètres de
simage indiquent la position à laquelle l'image doit être dessinée. Les deux prochains sont la taille de l'image, le cinquième un pointeur vers les données de l'image et le dernier comment l'image doit être dessinée :
- SNORMAL : On dessine le noir aussi bien que le blanc
- SINVERTED : On inverse les couleurs.
- STRANSP : le blanc est dessiné comme du noir et le noir n'est pas dessiné. Utile pour faire de la transparence.
- SNOWHITE : On ne dessine pas le blanc.
- SNOBLACK : On ne dessine pas le noir.
Dans
game_logic nous allons réinitialiser le jeu (pour tout remettre à zéro) et aller vers l'écran
S_INGAME, si la touche SHIFT est pressée :
/* We wait for the user to press shift to go to the in game screen.
*/
if(kcheck(KCSHIFT)){
/* Reset the game. */
game_init(game);
game->state = S_INGAME;
}
- Écran de fin de la partie.
Nous allons ensuite coder l'écran de fin de la partie. Dans
game_draw, nous allons afficher "GAME OVER" ou "YOU WIN!" selon le score.
Pour l'afficher au milieu, j'ai ajouté quelque defines en haut de game.c :
#define WIN "YOU WIN!"
#define GAME_OVER "GAME OVER"
/* The position of the game over text. */
#define GAME_OVER_X (SWIDTH/2-5*sizeof(GAME_OVER)/2)
/* The position of the win text. */
#define WIN_X (SWIDTH/2-5*sizeof(WIN)/2)
Dans game_draw il suffit d'appeler
stext selon le score obtenu par le joueur :
if(game->score1 > game->score2){
/* The player won, so we display it. */
stext(WIN_X, END_Y, WIN, SBLACK);
}else{
/* The player lost, so we display it. */
stext(GAME_OVER_X, END_Y, GAME_OVER, SBLACK);
}
Ensuite, dans
game_logic nous allons attendre que SHIFT est pressé, puis attendre que cette touche soit relachée pour que l'écran de titre ne soit pas sauté.
/* Check if shift is pressed. If shift is pressed, go to the title
* screen. */
if(kcheck(KCSHIFT)){
game->state = S_TITLE;
/* Wait for shift to be released, to avoid directly going to the
* ingame screen after going to the title screen. */
while(kcheck(KCSHIFT)) csleep();
}
Ensuite, nous devons aller à l'écran de fin au score maximal, lorsqu'on change le score :
if(move == 1){
/* Player 1 got a point. */
game->score1++;
/* Put the ball in the middle and send it to the computer */
ball_init(&game->ball, TO_FIXED(1), TO_FIXED(1),
TO_FIXED(DEFAULT_SPEED));
/* If the player got the max score he won, so we go to the end
screen. */
if(game->score1 >= MAX_SCORE) game->state = S_END;
}
if(move == 2){
/* Player 2 got a point. */
game->score2++;
/* Put the ball in the middle and send it to the player */
ball_init(&game->ball, TO_FIXED(-1), TO_FIXED(1),
TO_FIXED(DEFAULT_SPEED));
/* If the computer got the max score he won, so we go to the end
screen. */
if(game->score2 >= MAX_SCORE) game->state = S_END;
}
- ball.c, ball.h et game.c, rebond de la balle sur la raquette.
Pour l'instant, le rebond de la balle n'est pas très fidèle à celui de pong. Je trouve que faire l'interpolation linéaire entre
y_inc = -1 et
y_inc = 1 de la position de la balle par rapport à la raquette est assez fidèle à l'original.
Nous allons donc créer une fonction
ball_bounce qui a pour paramètre la balle et le paddle, dans laquelle nous allons calculer le nouveau
y_inc comme je l'ai décrit ci-dessus.
Mon implémentation :
void ball_bounce(Ball *ball, Paddle *paddle) {
/* Linear interpolation to calculate the new angle of the ball. */
int pos_on_the_paddle = ball->y-TO_FIXED(paddle->y);
fixed_t angle_inc = DIV(TO_FIXED(ANGLE_RANGE), TO_FIXED(PADDLE_HEIGHT));
/* Fix the position of the ball on the paddle. */
if(pos_on_the_paddle < 0) pos_on_the_paddle = 0;
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)+MUL(angle_inc, pos_on_the_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);
}
}
Ajoutez son prototype à ball.h et appelez la lors d'une collision, là où on a précedemment fait
game->ball.y_inc = -game->ball.y_inc;.
Améliorations possibles
Améliorez les collisions, essayez de rendre le jeu le plus petit possible, cela vous entrainera bien. Vous pouvez aussi ajouter un mode deux joueurs ou un mode robot contre robot. Vous pouvez aussi rendre possible la séléction du score maximal (libMicrofx propose des fonctions de saisie de nombres et de texte).
Étendre vos connaissances de libMicrofx
Pour étendre vos connassances de libMicrofx je vous conseille de lire les headers dans lib/include/microfx. La plupart des fonctions sont dans microfx.h. Il y a des fonctions très utiles à la création de jeux vidéos dans les headers dans le dossier ext.
C'est la fin de ce tutoriel
Voilà ! Vous avez fini de lire ce tutoriel (ou presque). J'espère que ce tutoriel vous a plu et que vous vous amuserez bien avec libMicrofx !
Citer : Posté le 25/08/2024 16:48 | #
Splendide. Ça mériterait d'être en news en page d'accueil si je peux me permettre. Ça fera une jolie publication.
Bravo à toi
Citer : Posté le 25/08/2024 16:55 | #
Merci pour ton retour !
@RDP alors
(tiens je pourrais la rédiger comme candidature comme rédacteur)
libMicrofx : https://www.planet-casio.com/Fr/forums/topic17259-2-libmicrofx-remplacez-fxlib-pour-faire-des-add-ins-tres-legers.html !
Racer3D : https://www.planet-casio.com/Fr/programmes/programme4444-1-racer3d-mb88-jeux-add-ins.html