tutorial-02: walkable map (first half) and images

This commit is contained in:
Lephenixnoir 2020-08-20 17:05:40 +02:00
parent 8fa119937d
commit 74d6ab74e3
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
7 changed files with 770 additions and 0 deletions

View File

@ -0,0 +1,489 @@
[center][big][brown][b]01 : Du mystère au menu ![/b][/brown][/big][/center]
Je vous propose de commencer cette série par l'écran d'accueil du jeu et un peu de gestion de projet. Normalement il faudrait coder le moteur d'abord, mais ce serait trop brutal. Voilà ce qu'on va réaliser à la place :
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-01-menu-animated.gif[/img]
[i]Image 1 : la pub ne mentait pas sur le « noir et blanc ».[/i][/center]
Une police facile à dessiner, un titre qui en promet plus que le jeu n'en propose, et un joli effet graphique pour vous surprendre un peu, rien de bien extravagant. J'ai fait sobre dans le menu pour rester dans le minimalisme de [url=https://www.planet-casio.com/Fr/forums/topic16318-1-1kbcj3-le-theme.html]la 1kBCJ#3[/url] dont ce jeu est issu, mais vous allez voir que ce menu tout simple va déjà nous occuper un bon moment.
On a donc une image fixe et une sélection de niveaux tout ce qu'il y a de plus classique. Le but c'est d'avoir un fichier de sauvegarde qui dit quels niveaux on a débloqués, mais ça ce sera pas pour tout de suite, donc pour l'instant on va automatiquement débloquer tous les niveaux sauf le dernier pendant les tests. :)
Avant de commencer, notez que tout ce tutoriel est suivi sur [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb]un dépôt Git[/url]. Si vous ne voulez pas vous lancer dans un projet de zéro tout de suite, vous pouvez clôner celui du jeu et vous déplacer dans l'historique tout en suivant le tutoriel. Dans tous les cas, vous y trouverez les images et ressources qu'on va utiliser tout le long ; je donnerai donc régulièrement des liens qui y pointent.
[brown][b]Création du projet[/b][/brown]
Commençons par créer les fichiers dont on a besoin. Le fxSDK fournit un modèle de projet avec tout ce qu'il faut pour compiler, via l'outil [b][brown]`fxsdk`[/brown][/b]. Cependant, son ambition n'est que de fournir une base, pas d'être exhaustif. Si votre projet devient compliqué, il est attendu que vous compreniez comment la compilation fonctionne pour prendre en main votre application. On verra plus tard ce que ça veut dire concrètement ; pour l'instant, commençons avec le modèle. Mettez-vous dans un dossier de votre choix (pour moi `~/Programs`) et créez un nouveau projet :
[code]% fxsdk new mystere-noir-et-blanc
Creating a new project in folder 'mystere-noir-et-blanc'.
Full project name ? (at most 8 characters)
> MystNB
Internal name ? ('@' followed by at most 7 uppercase letters)
(Add-in might not appear on calc if format is wrong)
> @MYSTNB
Your project 'MystNB' has been created.
Type 'fxsdk build-fx' or 'fxsdk build-cg' to compile the program.[/code]
Le fxSDK vous demande quelques informations sur votre add-in. Le premier est le nom de l'add-in tel qu'on le voit dans l'onglet `VERSION` de l'application `SYSTEM`, et le second est le nom interne qui sert notamment à attribuer des dossiers dans la mémoire principale. Mettez bien que chose de la forme `@NOM` sinon l'add-in ne s'affiche parfois pas dans le menu. ^^
Maintenant voyons voir ce qu'il y a dans ce dossier !
[code]% tree mystere-noir-et-blanc
mystere-noir-et-blanc
├── assets-cg
│   ├── icon-cg-sel.png
│   └── icon-cg-uns.png
├── assets-fx
│   └── icon-fx.png
├── Makefile
├── project.cfg
└── src
└── main.c[/code]
Le fxSDK a créé un certain nombre de dossiers et fichiers. Voici à quoi ils servent :
• `assets-cg` contient toutes les images, polices et autres ressources pour la Graph 90+E. Le fxSDK permet de programmer pour la Graph 90+E, et même de faire un add-in pour les Graph mono et la Graph 90+E en même temps. Ici, on va programmer uniquement sur Graph mono, donc je vais le supprimer.
• `assets-fx` contient les images, polices et autres ressources pour les Graph mono. C'est là qu'on va mettre la plupart de nos données ! Toutes les Graph mono sont identiques du point de vue de gint, de la Graph 75+E que j'utilise aux Graph 35+ USB, 35+E, 35+E II, et même les vieilles SH3, tout est compatible.
• Le `Makefile` est un fichier indiquant comment compiler l'application. Quand vous tapez "`make`" ou "`fxsdk build-fx`" dans le terminal, c'est lui qui donne toutes les instructions. Vous avez ici un Makefile fourni par le fxSDK qui suffit pour les add-ins simples. Comprendre comment il est écrit nécessiterait un tutoriel entier, donc pour l'instant on ne va pas le lire du tout.
• `project.cfg` est un fichier de configuration créé par le fxSDK pour personnaliser le comportement du Makefile. On s'en servira très vite pour ajouter la police d'écriture du jeu, et vous pouvez aussi y changer plein d'options de compilation. :)
• `src` contient comme d'habitude tous les fichiers de code. Le fxSDK a copié un `main.c` avec un code d'exemple.
[brown][b]Hello, World![/b][/brown]
C'est parti ! Commençons avec le "Hello, World!" de gint que le fxSDK a copié dans `src/main.c` pour vous.
[code]#include <gint/display.h>
#include <gint/keyboard.h>
int main(void)
{
dclear(C_WHITE);
dtext(1, 1, C_BLACK, "Sample fxSDK add-in.");
dupdate();
getkey();
return 1;
}[/code]
Pour le réaliser, on a besoin d'utiliser des fonctions de gint qui sont décrites par deux en-têtes, `<gint/display.h>` (le partie dessin et l'affichage) et `<gint/keyboard.h>` (la gestion du clavier). Lorsque vous les incluez, ces en-têtes expliquent au compilateur quelles sont les fonctions proposées par gint, mais aussi quels sont les noms des couleurs et comment reconnaître les images et les polices. Si vous essayez d'utiliser les fonctionnalités de gint sans avoir inclus les en-têtes correspondants, le compilateur se plaindra qu'il ne sait pas de quoi vous parlez. ^^
Et donc une fois les en-têtes inclus on commence tout de suite à sortir les pinceaux.
• `dclear(C_WHITE)` efface la VRAM et remplit tout en blanc. C'est comme `Bisp_AllClr_VRAM()` excepté que vous pouvez changer de couleur. Les couleurs sont définies dans `<gint/display-fx.h>` et `<gint/display-cg.h>`, j'en reparle dans un instant.
• `dtext(x,y,fg,str)` affiche la chaîne de caractères `str` à la position indiquée et avec la couleur spécifiée. La position est en pixels, avec `(0,0)` en haut à gauche. Ce sera le cas pour toutes les fonctions de dessin, sans exception aucune ! `fg` est la couleur du texte. Cette fonction ressemble à `PrintXY()`, sauf qu'on a plus de choix de couleurs et que plus tard on pourra changer la couleur de fond, l'alignment du texte et la police !
• `dupdate()` affiche les contenus de la VRAM à l'écran, c'est l'équivalent de `Bdisp_PutDisp_DD()` tant qu'on n'active pas le moteur de gris.
En plus des informations importantes pour le compilateur, les en-têtes de gint contiennent aussi des informations importantes pour vous, avec la liste des fonctions, leurs paramètres et leurs rôles. Je vous conseille de prendre l'habitude d'aller les lire si vous avez besoin d'informations. Par exemple, la liste des couleurs est dans les en-têtes `<gint/display-fx.h>` et `<gint/display-cg.h>`. (Comme le dessin est très différent entre Graph mono et Graph 90+E, `<gint/display.h>` est séparé en deux versions.) Tous ces en-têtes sont dans le dossier `include` de gint. Vous pouvez les trouver dans le dossier où vous avez clôné gint lors de l'installation, ou [url=https://gitea.planet-casio.com/Lephenixnoir/gint/src/branch/master/include/gint]en ligne sur le dépôt Gitea[/url].
Vous savez certainement que les add-ins s'exécutent vite, c'est sans doute pour ça que vous êtes ici. Dans ce programme, l'affichage va prendre entre 1 et 2 ms. (Sur Graph 90+E, il faudrait compter entre 15 et 20 ms, tout simplement parce qu'il y a 170 fois plus de donnés graphiques à manipuler.) On ne veut pas que l'add-in s'arrête maintenant, parce que contrairement à un programme Basic il reviendrait directement au menu de la calculatrice et on ne verrait rien.
On utilise pour éviter ça la fonction `getkey()`, qui met le programme en pause jusqu'à ce que l'utilisateur appuie sur une touche. `getkey()` renvoie un [i]« événement »[/i] indiquant quelle touche a été pressée, quand, et d'autres informations utiles. Chaque touche a un nom, que vous pouvez trouver [url=https://gitea.planet-casio.com/Lephenixnoir/gint/src/branch/master/include/gint/keycodes.h]dans `<gint/keycodes.h>`[/url] (qui est inclu par `<gint/keyboard.h>`, c'est pour ça que je ne vous l'ai pas fait inclure tout à l'heure).
La fonction `getkey()` est centrale, vous l'utiliserez pour toutes les entrées clavier sur tous les écrans qui ne sont pas en temps réel (comme les menus ou les applications utilitaires), et parfois même pour les écrans en temps réel dans les jeux. J'aimerais donc éclaircir trois choses au sujet de `getkey()` pour éviter toute confusion avec la fonction `GetKey()` que l'on trouve dans fxlib et que vous avez peut-être déjà utilisée.
1. `getkey()` [b]attend[/b]. Le code qui suit (le `return`) ne sera pas exécuté tant que l'utilisateur n'aura pas appuyé sur une touche, peu importe si ça lui prend des heures !
2. `getkey()` [b]renvoie[/b] un événement, alors que `GetKey()` modifie un pointeur qu'on lui passe en argument.
3. `getkey()` [b]ne rafraîchit pas l'écran[/b], contrairement à `GetKey()` qui appelle `Bdisp_PutDisp_DD()` avant de se mettre en attente. Il faut appeler `dupdate()` explicitement.
Pour l'instant on ignore complètement la valeur de retour de `getkey()` (l'événement qui nous dit, entre autres, quelle touche a été pressée) donc on peut appuyer sur n'importe quelle touche pour quitter. On changera ça bientôt !
[brown][b]Compiler et tester[/b][/brown]
La compilation d'un add-in mérite un tutoriel complet, que j'écrirai sans doute un jour si personne ne me grille la politesse. Pour l'instant, on va ignorer tous ces détails et utiliser le Makefile que le fxSDK a copié pour nous. On se rappelle que ce Makefile vient avec un fichier `project.cfg` dans lequel vous pouvez modifier des options : on en aura besoin très vite. Pour compiler votre application pour la famille des Graph mono (Graph 35+E et affiliées), utilisez la commande "`fxsdk build-fx`". (Dans le fxSDK, "`fx`" représente la famille des Graph mono, tandis que "`cg`" représente les Prizm et Graph 90+E. Naturellement il existe aussi "`fxsdk build-cg`" pour compiler une version Graph 90+E.)
[code]% fxsdk build-fx
:: Making into build-fx
sh-elf-gcc -c src/main.c -o build-fx/src/main.c.o -mb -ffreestanding -nostdlib -fstrict-volatile-bitfields -Wall -Wextra -Os -D FX9860G -m3 -I include -MMD -MT build-fx/src/main.c.o -MF build-fx/src/main.c.d -MP
sh-elf-gcc -o build-fx/MystNB.elf build-fx/src/main.c.o -mb -ffreestanding -nostdlib -fstrict-volatile-bitfields -Wall -Wextra -Os -D FX9860G -m3 -I include -T fx9860g.ld -lgint-fx -lgint-fx -lgcc -Wl,-Map=build-fx/map
sh-elf-objcopy -O binary -R .bss -R .gint_bss build-fx/MystNB.elf build-fx/MystNB.bin
fxg1a build-fx/MystNB.bin -o MystNB.g1a -i "assets-fx/icon-fx.png" -n "MystNB" --internal="@MYSTNB"[/code]
Il y a pas mal de détails sordides ici, mais vous devez pouvoir comprendre une partie du texte qui apparaît à l'écran, car c'est ici que vous aurez quasiment tous vos messages d'erreur pendant le développement. Bien comprendre les erreurs et qui vous les a envoyées vous évitera beaucoup de frustration et de temps perdu.
Chaque ligne est une commande que le Makefile a lancé et qui contribue à compiler votre add-in. Le premier mot de chaque commande est le nom d'un outil qui a travaillé pour vous, les autres mots sont des options.
Les deux premières commandes font appel à [brown][b]`sh-elf-gcc`[/b][/brown] : c'est le compilateur. C'est lui qui transforme chaque fichier de code C en binaire et ensuite réunit ces binaires tous ensemble. Les deux commandes suivantes font appel à [brown][b]`sh-elf-objcopy`[/b][/brown], un copain de GCC, et [brown][b]`fxg1a`[/b][/brown], un outil du fxSDK. Ces deux vont ensemble et s'occupent de générer un fichier g1a avec le résultat de la compilation.
C'est pas grave si tout cela vous échappe un peu, j'y reviendrai de temps en temps. Pour l'instant, vous avez deux nouvelles choses dans le dossier de votre projet :
• Un dossier `build-fx` qui contient tous les fichiers compilés. Vous pouvez le supprimer à tout moment, mais il permet de recompiler l'application plus vite en récupérant le code déjà compilé quand vous ne l'avez pas modifié. Par exemple, si vous retapez `fxsdk build-fx` il ne se passe rien car vous n'avez rien modifié depuis la dernière compilation. En général vous voulez donc le laisser tranquille. Toutefois il n'est pas apprécié sur un dépôt Git donc ajoutez-le dans votre `.gitignore`. ;)
• Le fichier `MystNB.g1a` qui est notre add-in compilé !
Et c'est terminé ! :D Il ne reste qu'à envoyer l'add-in sur votre calculatrice par votre méthode préférée. Sur ma Graph 35+E II je l'enverrai par USB. Pour les autres Graph, un outil de choix sous Linux est [url=https://www.planet-casio.com/Fr/forums/topic14487-1--GNU-Linux--P7,-pour-des-transferts-a-repasser.html]l'utilitaire P7 de Cakeisalie5[/url] (que `fxsdk send-fx` appelle pour vous !). Vous pouvez aussi utiliser FA-124, mais si vous en arrivez là je vous plains. ^^"
Une fois transféré, l'add-in apparaît dans le menu et on peut observer le résultat attendu :
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-01-sample-add-in.png[/img]
[i]Image 2 : Yeah![/i][/center]
L'état actuel du tutoriel correspond au commit [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/commit/de1281715b300a92dc7bf5261d9f98e62f11bb55]`de12817`[/url] dans l'historique du dépôt. Sur ce, il est temps de passer aux choses sérieuses !
[brown][b]Les assets[/b][/brown]
Voici les assets que l'on va utiliser : une icône pour l'add-in, l'image du titre, les icônes pour les niveaux, et la police de caractères pour écrire le texte du jeu et les numéros des niveaux. On ne va pas utiliser celle par défaut de gint, qui manque un peu de style. Le PNG est fortement conseillé et même obligatoire pour l'icône. :)
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-01-icon.png[/img]
[i]Image 3 : La clé du mystère était dans l'icône depuis le début ![/i]
[img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-01-title.png[/img]
[i]Image 4 : Aujourd'hui on repousse les limites du lisible.[/i]
[img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-01-levels.png[/img]
[i]Image 5 : Il y aura plus que deux niveaux quand même.[/i]
[img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-01-font.png[/img]
[i]Image 6 : Une police qui a du caractère ![/i][/center]
Les images ci-dessus sont agrandies, bien sûr pour le projet il vous faut les originaux. Vous pouvez les télécharger directement sur le dépôt :
[center][url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/src/commit/5955e0472d77e7fbb9602336e8ec84fa5e30cd6e/assets-fx][b]» Dossier `assets-fx` sur le dépôt à ce stade «[/b][/url][/center]
Comme vous pouvez le voir, la police qu'on utilise est vraiment une image, c'est juste une grille de caractères. Le fxSDK va la convertir en une police utilisable avec `dtext()` en utilisant un de ses outils, [brown][b]`fxconv`[/b][/brown].
Téléchargez ou copiez les trois images dans `assets-fx` (celles du dépôt, pas les versions agrandies visibles sur cette page !). L'icône du projet doit remplacer "`assets-fx/icon.png`", l'image `title.png` va dans un nouveau dossier "`assets-fx/img`", et la police `mystere.png` va dans un nouveau dossier "`assets-fx/fonts`". Ces noms de dossiers sont fixes, ce sont les mêmes pour tous les projets fxSDK. Vous devez obtenir ceci :
[code]% tree assets-fx
assets-fx
├── fonts
│   └── mystere.png
├── icon-fx.png
└── img
   ├── levels.png
   └── title.png[/code]
On utilise des sous-dossiers car le Makefile du fxSDK considère tout ce qui est dans `assets-fx/img` comme une image et tout ce qui est dans `assets-fx/fonts` comme une police, ça vous évite de le dire vous-même. Si vous tapez "`fxsdk build-fx`" maintenant, vous verrez que [brown][b]`fxconv`[/b][/brown] va automatiquement convertir l'image et la police. Enfin... il va essayer, car ça ne va pas marcher !
[code]% fxsdk build-fx
:: Making into build-fx
fxconv -f assets-fx/fonts/mystere.png -o build-fx/assets/fonts/mystere.png.o --fx --toolchain=sh-elf name:font_mystere
error: size of grid unspecified or invalid
make: *** [Makefile:147: build-fx/assets/fonts/mystere.png.o] Error 1[/code]
Prenons une seconde pour comprendre ce que ces quelques lignes nous disent. Comme précédemment, chaque ligne est une commande. Ici, on a lancé [brown][b]`fxconv`[/b][/brown], et on voit qu'une des options c'est le nom de notre fichier de police `assets-fx/fonts/mystere.png`, donc on l'a lancé pour convertir la police. Il a alors affiché la ligne d'en-dessous qui dit [i]"error: size of grid unspecified or invalid"[/i].
Comme une erreur s'est produite, le Makefile s'arrête puisqu'il est impossible de finir la compilation, ce qui se voit à la dernière ligne avec `make: ***` et puis `Error 1` à la fin. Cette ligne apparaît toujours quand il y a une erreur, mais elle ne dit pas quelle est l'erreur : c'est juste `make` qui dit [i]« au cas où vous auriez pas vu, un truc a planté, donc je m'arrête »[/i]. Ici l'erreur c'est bien celle qui parle de la taille de la grille.
En fait, [brown][b]`fxconv`[/b][/brown] ne sait pas comment notre police est construite. On lui a donné une grosse image sans indiquer comment il doit se débrouiller pour y lire des caractères. Il faut au moins lui dire quels caractères on a dessinés (B ? é ? ∀ ?) et leurs dimensions. Et ça, on le fait en modifiant le fichier de configuration du projet, `project.cfg`. C'est un fichier texte que vous pouvez modifier comme un fichier C. On y trouve plein de paramètres, auxquels on va tout de suite ajouter les informations dont [brown][b]`fxconv`[/b][/brown] a besoin pour convertir notre police.
[code]#---
# fxSDK project configuration file for MystNB
#---
# Project name, should be at most 8 bytes long.
# (You can also specify NAME_G1A or NAME_G3A to override individually.)
NAME := MystNB
# Internal name, should be '@' followed by at most 7 uppercase letters.
# WARNING: If this convention is not followed, the add-in might not appear in
# the main menu of the calculator!
INTERNAL := @MYSTNB
# Output file name. The default is to take <NAME>, replace spaces with dashes,
# and add .g1a (or .g3a). You can specify a different folder if you want.
TARGET_FX :=
TARGET_CG :=
# fx-9860G icon location
ICON_FX = assets-fx/icon-fx.png
# fx-CG 50 icon locations
ICON_CG_UNS = assets-cg/icon-cg-uns.png
ICON_CG_SEL = assets-cg/icon-cg-sel.png
#---
# Toolchain selection
#---
# Toolchain for fx9860g. Please see also CFLAGS_FX below.
TOOLCHAIN_FX := sh-elf
# Toolchain for fxcg50. Please see also CFLAGS_CG below.
TOOLCHAIN_CG := sh-elf
#---
# Compiler flags
#---
# Base compiler flags for the fxSDK, you usually want to keep these.
CFLAGS := -mb -ffreestanding -nostdlib -fstrict-volatile-bitfields
# Platform-specific compiler flags.
# <> If you are using sh3eb-elf, use -m3. (You can do this on both FX and CG.)
# <> If you are using sh4eb-elf, use -m4-nofpu. (Not ideal on FX but works.)
# <> If you are using sh4eb-nofpu-elf, then your compiler will likely use the
# FPU and cause problems on the calculator. Consider another configuration.
# <> If you are using an sh-elf with several targets, specify whichever you
# support. I recommend -m3 on FX and -m4-nofpu on CG.
# Please see also TOOLCHAIN_FX and TOOLCHAIN_CG above.
CFLAGS_FX := -D FX9860G -m3
CFLAGS_CG := -D FXCG50 -m4-nofpu
# Additional compiler flags, change to your own taste!
CFLAGS += -Wall -Wextra -Os
# Include paths. Add one -I option for each folder from which you want to
# be able to include files with #include<>.
INCLUDE := -I include
# Libraries. Add one -l option for each library you are using, and also
# suitable -L options if you have library files in custom folders. To use
# fxlib, add libfx.a to the project directory and use "-L . -lfx".
LIBS_FX :=
LIBS_CG :=
# Base linker flags for the fxSDK, you usually want to keep these.
LDFLAGS_FX := -T fx9860g.ld -lgint-fx $(LIBS_FX) -lgint-fx -lgcc
LDFLAGS_CG := -T fxcg50.ld -lgint-cg $(LIBS_CG) -lgint-cg -lgcc
# Additional linker flags, if you need any.
LDFLAGS :=
# Additional platform-specific linker flags.
LDFLAGS_FX += -Wl,-Map=build-fx/map
LDFLAGS_CG += -Wl,-Map=build-cg/map
#---
# File conversion parameters
#---
# Here you can add fxconv options for each converted file, individually.
# The syntax is "<type>.<file>". For example, to specify the parameters for a
# font named "hexa.png", you might write:
#
# FONT.hexa.png = charset:print grid.size:3x5 grid.padding:1[/code]
Il y a tellement de choses obscures là-dedans qu'on pourrait se demander si le mystère en noir et blanc ne serait pas en fait ce fichier. Ce qu'on va faire c'est ignorer royalement tout sauf les quelques lignes à la fin dans "File conversion parameters".
Tout ce dont vous avez besoin de savoir sur ce fichier pour l'instant se résume à deux points.
• Toutes les lignes qui commencent par un `#` sont des commentaires et toutes les autres donnent des valeurs à des paramètres avec une syntaxe du genre "`PARAMÈTRE = valeur`".
• Pour chaque fichier (image, police, etc) à convertir, il y a un paramètre (optionnel) qui nous permet de donner des informations à [brown][b]`fxconv`[/b][/brown].
Lors de la conversion des images et polices, le Makefile que le fxSDK a créé par nous va chercher toute information à destination de [brown][b]`fxconv`[/b][/brown] dans ce fichier. On va donc en ajouter tout à la fin avec la ligne suivante :
[code]FONT.mystere.png = charset:print grid.size:5x7 grid.padding:1 proportional:true[/code]
Notre fichier s'appelle `mystere.png`, et c'est une police. Le nom du paramètre qui va avec est `FONT.mystere.png`. Pour l'image `title.png`, ce serait `IMG.title.png`. Le reste c'est les informations supplémentaires ; voyons ce qu'elles veulent dire. :)
• "`charset:print`" indique quels sont les caractères qui sont dessinés. "`print`" c'est l'ensemble des caractères affichables de l'ASCII, il y en a 95 qui commencent par l'espace et se terminent par le tilde. (Le carré en bas à droite sera ignoré.) Avec cette information, [brown][b]`fxconv`[/b][/brown] sait que le 35ème caractère est "B".
• "`grid.size:5x7`" indique quelle taille fait chaque caractère. Comme vous pouvez le voir, on donne la même taille pour tout le monde, donc il faut indiquer une taille assez grande. La plupart des caractères font moins de 5 pixels de large, mais `M`, `W` et quelques autres nous obligent à utiliser une grille de largeur au moins 5. Pour la hauteur, la plupart des caractères font 6 pixels mais il y a des caractères comme `g` ou la virgule qui descendent une ligne plus bas que les autres, portant le total à 7. Cette grille dit à [brown][b]`fxconv`[/b][/brown] où les caractères sont dessinés dans notre PNG, mais ce n'est pas forcément la taille finale à l'écran de la calculatrice.
• "`grid.padding:1`" indique qu'autour de chaque caractère, j'ai laissé un cadre blanc de 1 pixel de large. Je l'ai fait pour conserver un espacement confortable pendant l'édition. Vous pouvez le voir sur l'image suivante où j'ai mis le padding en valeur en bleu et jaune :
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-01-font-padding.png[/img]
[i]Image 6 : Il y a un pixel de padding autour de chaque caractère.[/i][/center]
• "`proportional:true`" signifie qu'on veut une police à largeur variable : on veut que chaque caractère prenne juste la place qui est nécessaire pour le dessiner sur la calculatrice. Ainsi, même si `M` ou `W` prennent 5 pixels, `I` fera quand même 1 pixel et `A` 4 pixels. En pratique, après avoir isolé chaque caractère en utilisant la grille, [brown][b]`fxconv`[/b][/brown] va encore éliminer le blanc à gauche et à droite.
Vous n'avez pas besoin de comprendre tous les détails de comment les polices marchent pour l'instant. Si des choses vous échappent, vous aurez l'occasion d'y revenir plus tard. ;)
Pour les images, il n'y a aucun paramètre obligatoire, [brown][b]`fxconv`[/b][/brown] se débrouille tout seul pour convertir et sur Graph mono il n'y a généralement aucune raison de vouloir changer le comportement par défaut. Vous n'aurez donc pas besoin de modifier les paramètres de `project.cfg` trop souvent.
Vous pouvez maintenant recompiler avec "`fxsdk build-fx`" et observer que l'image et la police sont converties ! Si vous en avez l'habitude, vous pouvez aussi juste taper "`make`", ce qui va compiler automatiquement dans `build-fx` (s'il existe) puis dans `build-cg` (s'il existe).
[code]% make
fxconv -f assets-fx/fonts/mystere.png -o build-fx/assets/fonts/mystere.png.o --fx --toolchain=sh-elf name:font_mystere charset:print grid.size:5x7 grid.padding:1 proportional:true
fxconv --bopti-image assets-fx/img/title.png -o build-fx/assets/img/title.png.o --fx --toolchain=sh-elf name:img_title
sh-elf-gcc -o build-fx/MystNB.elf build-fx/src/main.c.o build-fx/assets/fonts/mystere.png.o build-fx/assets/img/title.png.o -mb -ffreestanding -nostdlib -fstrict-volatile-bitfields -Wall -Wextra -Os -D FX9860G -m3 -I include -T fx9860g.ld -lgint-fx -lgint-fx -lgcc -Wl,-Map=build-fx/map
sh-elf-objcopy -O binary -R .bss -R .gint_bss build-fx/MystNB.elf build-fx/MystNB.bin
fxg1a build-fx/MystNB.bin -o MystNB.g1a -i "assets-fx/icon-fx.png" -n "MystNB" --internal="@MYSTNB"[/code]
Reprenons un instant pour voir ce qui s'est passé. [brown][b]`fxconv`[/b][/brown] a été appelé deux fois, une pour convertir la police et une pour convertir l'image. Ensuite [brown][b]`sh-elf-gcc`[/b][/brown] a été appelé pour fusionner le résultat de la conversion avec le résultat de la compilation de `main.c`, qui avait été produit lors de la première compilation. Enfin, [brown][b]`sh-elf-objcopy`[/b][/brown] et [brown][b]`fxg1a`[/b][/brown] ont été appelés de nouveau pour recréer le fichier g1a. Contrairement à tout à l'heure, le compilateur [brown][b]`sh-elf-gcc`[/b][/brown] n'a été appelé qu'une fois car il n'a pas été nécessaire de recompiler `main.c` : le code n'a pas changé depuis la dernière fois !
[brown][b]Dessiner un menu principal[/b][/brown]
Grâce à notre image de titre et à notre police, on peut maintenant réaliser le début de notre menu principal :
[code]#include <gint/display.h>
#include <gint/keyboard.h>
int main(void)
{
extern bopti_image_t img_title;
extern bopti_image_t img_levels;
extern font_t font_mystere;
dfont(&font_mystere);
dclear(C_WHITE);
dimage(0, 2, &img_title);
for(int i = 1; i <= 8; i++)
{
int x = 20 + 11*(i-1);
int y = 36;
if(i != 8)
{
dsubimage(x, y, &img_levels, 0,0,10,10, DIMAGE_NONE);
dprint(x+3, y+2, C_BLACK, "%d", i);
}
else
{
dsubimage(x, y, &img_levels, 11,0,10,10, DIMAGE_NONE);
}
}
dupdate();
getkey();
return 1;
}[/code]
Il y a pas mal de choses à dire sur cette nouvelle fonction. D'abord, les trois premières lignes :
[code]extern bopti_image_t img_title;
extern bopti_image_t img_levels;
extern font_t font_mystere;[/code]
Ce sont des déclarations de variables. Les deux premières variables sont du type `bopti_image_t`, ce sont les images du titre et des niveaux. ([i]bopti[/i] c'est le nom d'un composant de gint chargé de l'affichage des images, c'est lui qui est derrière `dimage()` et `dsubimage()` qu'on va voir très vite. Le "`_t`" à la fin est une convention qui signifie "type".) La dernière est du type `font_t`, c'est notre police (pas fou je sais :p ).
Les deux sont marquées `extern`, ce qui est très important : ça signifie qu'on ne crée pas de variable (on les [i]déclare[/i] mais on ne les [i]définit[/i] pas, dans le jargon) ; on indique seulement au compilateur que ces variables existent [i]ailleurs[/i] et on lui promet de les lui fournir quand il en aura besoin. Ces variables sont en fait créées par [brown][b]`fxconv`[/b][/brown] lorsqu'il convertit les images et polices. :)
Désormais, plus besoin de tableaux longs et moches du genre `const unsigned char title[256] = { }` à copier-coller partout dans votre code, il vous suffit de créer votre image ou police au bon endroit dans `assets-fx` puis de la déclarer dans le code avec `extern`. Le fxSDK se charge de la convertir et de l'ajouter à votre add-in, sous le nom suivant :
• Pour une image, "`img_`" suivi du nom du fichier. Par exemple "`title.png`" devient la variable "`img_title`".
• Pour une police, "`font_`" suivi du nom du fichier. Par exemple "`island.png`" devient la variable "`font_island`".
Ce sont les noms par défaut mais vous pouvez les changer plus tard en ajoutant des paramètres à `project.cfg`. Notez que du coup vous devez utiliser des underscore (`_`) dans les noms de fichiers de `assets-fx` car les autres caractères (espace, tiret) sont interdits dans les noms de variables en C.
Voyons la suite.
[code]dfont(&font_mystere);[/code]
Ensuite, on change de police avec un appel à la fonction `dfont()`, qui prend en paramètre la nouvelle police à utiliser. Vous noterez qu'il faut donner [i]« l'adresse »[/i] de la variable (c'est ce petit "`&`" devant le nom). Si vous ne savez pas ce que c'est, pas grave. Retenez juste que dès que vous utiliserez une image ou une police de gint provenant de l'extérieur du programme il faudra systématiquement mettre ce "`&`" (libimg est la seule exception jusqu'ici).
Et c'est tout en fait, après cet appel à `dfont()`, `dtext()` affiche du texte avec notre police personnalisée. On peut alors effacer l'écran et dessiner l'image du titre :
[code]dclear(C_WHITE);
dimage(0, 2, &img_title);[/code]
`dimage()` est une fonction qui dessine une image complète à la position indiquée. Comme pour `dtext()`, les coordonnées sont dans l'ordre (x,y), en pixels en partant d'en haut à gauche, et ce sont les deux premiers paramètres. Comme avec la police à l'instant, il faut donner l'adresse de l'image, donc il y a encore un "`&`" devant "`img_title`". Contrairement à MonochromeLib vous n'avez plus besoin d'indiquer la dimension de l'image parce qu'elle est stockée avec les pixels !
Ensuite on a la partie dans laquelle on dessine les rectangles des niveaux. Pour les niveaux débloqués, on met le rectangle vide avec le numéro du niveau, et pour les autres on met le cadenas. Bon ici on n'a pas de sauvegarde donc on va prétendre que tous les niveaux sont débloqués sauf le dernier niveau, le 8. La boucle complète ressemble à ça.
[code]for(int i = 1; i <= 8; i++)
{
int x = 20 + 11*(i-1);
int y = 36;
if(i != 8)
{
dsubimage(x, y, &img_levels, 0,0,10,10, DIMAGE_NONE);
dprint(x+3, y+2, C_BLACK, "%d", i);
}
else
{
dsubimage(x, y, &img_levels, 11,0,10,10, DIMAGE_NONE);
}
}[/code]
Rien de très inattendu ici, on a 8 niveaux donc notre variable `i` varie de 1 à 8. Les variables `x` et `y` sont les coordonnées du coin haut gauche du carré pour chaque niveau ; la hauteur y=36 est constante et la position horizontale varie de 11 pixels à chaque fois, c'est calculé pour que le résultat soit centré.
Dans la boucle, on continue de dessiner. On utilise une nouvelle fonction `dsubimage()` permettant de dessiner une partie seulement d'une image, c'est comme ça qu'on se débrouille pour avoir uniquement le rectangle ou uniquement le rectangle avec cadenas.
`dsubimage()` est une version plus générale de `dimage()` qui permet de dessiner n'importe quelle partie d'une image et possède quelques options. Comme `dimage()`, on commence par spécifier la position où le résultat doit apparaître à l'écran et l'image source. Ensuite on précise quelle partie de l'image (`img_levels`) on veut dessiner sous la forme d'un quadruplet (x, y, largeur, hauteur). Par exemple le cadenas est à la position (11,0) dans `img_levels` et sa taille est 10x10 pixels. Enfin il y a les options, mais pour l'instant on ne va pas s'y intéresser donc on écrit `DIMAGE_NONE`.
Lorsque le niveau est débloqué, on veut afficher le numéro avec `dtext()`, sauf qu'on n'a le numéro que sous forme d'entier (notre `int i`) et qu'on n'a pas sa représentation textuelle. Si la différence vous paraît douteuse, sachez que les bits de l'entier 4 ne sont pas du tout les mêmes que ceux du texte `"4"`, donc il y a un [i]calcul[/i] à faire pour passer de l'un à l'autre. C'est vrai dans tous les langages, même si beaucoup vous le cachent (Python par exemple fait automatiquement le calcul dans `print()` sans vous le dire). Si vous avez déjà fait du C, vous savez que les fonctions de la famille de `printf()` sont chargées de calculer le texte pour plein de types de variables.
Sans rentrer dans les détails, les fonctions de la famille de `printf()` utilisent un [i]format[/i] qui décrit le texte qu'on veut générer et à quels endroits il faut calculer la représentation textuelle de variables. Ces substitutions s'écrivent `%<lettre>` (dans leur forme simple) avec une lettre différente pour chaque type de données. Par exemple le format "`x=%d`" représente un texte contenant "`x=`" suivi de la valeur d'un entier (`d` est la lettre qui représente les entiers dans une substitution). Sur un ordinateur, si vous appelez `printf("x=%d", 42)`, le `%d` est remplacé par la représentation textuelle de 42 et vous obtenez "`x=42`" dans votre terminal.
Sur la calculatrice on n'a pas de terminal donc on n'a pas `printf()`, mais gint fournit une fonction `dprint()` qui fait quasiment pareil. `dprint(x,y,fg,format,...)` affiche à la position `(x,y)` et avec la couleur `fg` le résultat du calcul du format selon les règles de substitution de la famille de `printf()`.
L'appel à `dprint()` dans la boucle sert donc à écrire la valeur de `i` dans la case de chaque niveau. Il y a pas mal de paramètres, mais vous pouvez vous souvenir que c'est comme `dtext()` à part que les substitution sont autorisées (et du coup pour chaque `%<lettre>` dans le format vous devez ajouter un argument qui indique la valeur à représenter).
C'était un peu gros mais on en reverra. Une fois tout dessiné, on n'oublie pas d'actualiser l'écran avec `dupdate()` (ce que `getkey()` ne fait pas pour nous). Et c'est gagné ! :D
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-01-menu.png[/img]
[i]Image 7 : Le menu principal prend forme ![/i][/center]
Cet état d'avancement correspond au commit [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/commit/5955e0472d77e7fbb9602336e8ec84fa5e30cd6e]`5955e04`[/url] sur le dépôt du projet.
[brown][b]Le curseur interactif[/b][/brown]
Il nous reste encore à rendre ce menu interactif. On va créer un curseur simple pour sélectionner les niveaux en inversant les contenus des rectangles, et ensuite on va la faire bouger avec les flèches gauche et droite. Appuyer sur EXE validera la sélection et on quittera l'add-in. Lorsqu'il y aura des sauvegardes, on pourra afficher correctement les numéros des niveaux débloqués mais pour l'instant on va se contenter de tous les débloquer sauf le dernier.
La première chose qu'on va faire, est qui devrait devenir un réflexe pour vous, c'est [i]séparer le code de dessin de la logique du menu[/i]. Pour ça, on va se créer un petite fonction `draw_menu()`, et notre boucle principale pourra se concentrer sur la gestion du clavier.
[code]static void draw_menu(int selected)
{
extern bopti_image_t img_title;
extern bopti_image_t img_levels;
dclear(C_WHITE);
dimage(0, 2, &img_title);
for(int i = 1; i <= 8; i++)
{
int x = 20 + 11*(i-1);
int y = 36;
if(i != 8)
{
dsubimage(x, y, &img_levels, 0,0,10,10, DIMAGE_NONE);
dprint(x+3, y+2, C_BLACK, "%d", i);
}
else
{
dsubimage(x, y, &img_levels, 11,0,10,10, DIMAGE_NONE);
}
if(i == selected)
{
drect(x+1, y+1, x+8, y+8, C_INVERT);
}
}
}[/code]
Si vous ne maîtrisez pas encore les [i]prototypes de fonctions[/i] ou les fichiers d'en-tête, prenez soin de mettre cette nouvelle fonction avant `main()`, parce que le compilateur lit les fichiers de haut en bas et ne serait pas très content de voir `main()` appeler une hypothétique fonction `draw_menu()` qu'il n'a encore jamais rencontrée.
Il n'y a pas grand-chose de nouveau à voir dans cette fonction, c'est quasiment la même qu'avant. Je l'ai qualifiée de `static`, cela veut dire qu'elle ne sera visible que dans `main.c` quand on ajoutera d'autres fichiers. Après tout, aucune autre partie de l'add-in n'en aura besoin, donc ça ne sert à rien de leur montrer qu'elle existe. Je n'ai pas mis le `dfont()` ici car il suffit de l'appeler une seule fois au début de l'add-in pour changer la police de façon permanente.
On rencontre une nouvelle fonction, qu'on utilise ici pour inverser la couleur des rectangles. `drect(x1,y1,x2,y2,color)` remplit le rectangle allant de `(x1,y1)` jusqu'à `(x2,y2)` (tous les deux inclus) avec la couleur spécifiée. Ici on utilise la couleur spéciale `C_INVERT` qui inverse le noir et le blanc.
Regardons donc plutôt les choses intéressantes qui se passent dans `main()`.
[code]int main(void)
{
extern font_t font_mystere;
dfont(&font_mystere);
int selected = 1;
int key = 0;
while(key != KEY_EXE)
{
draw_menu(selected);
dupdate();
key = getkey().key;
if(key == KEY_LEFT && selected > 1)
selected--;
if(key == KEY_RIGHT && selected < 8)
selected++;
}
return 1;
}[/code]
Il y a pas mal de choses nouvelles à regarder ici.
• D'abord on a commencé à s'intéresser aux touches ! Pour ça, j'ai stocké la valeur de `getkey().key` dans une variable. Comme je l'ai mentionné tout à l'heure, `getkey()` renvoie un [i]événement[/i] qui contient plusieurs informations. Il vous dit notamment quelle touche est concernée, si c'est un nouvel appui ou une répétition (les touchées fléchées sont répétées si vous appuyez longtemps dessus), si `SHIFT` ou `ALPHA` ont été activés avant l'événement, le moment où l'événement s'est produit... ici j'ignore tout sauf la touche exacte. On reviendra sur les informations utiles renvoyées par `getkey()` et les options disponibles.
• Ensuite on voit apparaître la logique du menu. La boucle `while` continue jusqu'à ce qu'on essaie d'entrer dans un niveau avec `EXE`. On peut se déplacer horizontalement mais bien sûr on vérifie qu'on ne se déplace pas avant le niveau 1 ou après le niveau 8.
• L'écran est redessiné même si on presse une touche inutile comme `DEL`. C'est pas top, mais c'est loin d'être dramatique comme en Basic Casio.
• Même si la boucle est infinie, on peut toujours quitter l'add-in en appuyant sur `MENU` puis revenir ensuite, comme avec le `GetKey()` de fxlib. Par contre si on quitte en sortant de `main()`, c'est définitif !
Et voilà ! :bounce:
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-01-menu-animated.gif[/img]
[i]Image 8 : C'était pas si difficile ![/i][/center]
[brown][b]Conclusion[/b][/brown]
C'est un menu très schématique et qu'on pourrait facilement améliorer... quelques animations, un peu de décoration, utiliser la touche `SHIFT` pour valider, et plus encore. Ce tutoriel n'est qu'une brève introduction sur les fonctions fournies par gint mais décrit la majorité de la gestion routinière d'un projet. J'en parlerai moins dans la suite, et ça devrait être plus fun. :)
Voilà un petit récapitulatif de ce qu'on a vu.
:here: Créer et compiler un projet avec le fxSDK
:here: Ajouter des assets et spécifier leurs paramètres pour [brown][b]fxconv[/b][/brown] dans `project.cfg`
:here: Dessin général (`<gint/display.h>`) : `dclear()`, `drect()`, `dupdate()`
:here: Dessin d'images (`<gint/display.h>`) : `dimage()`, `dsubimage()`
:here: Dessin de texte (`<gint/display.h>`) : `dtext()`, `dprint()`, `dfont()`
:here: Gestion du clavier (`<gint/keyboard.h>`) : `getkey()`

View File

@ -0,0 +1,281 @@
[center][big][brown][b]02 : Un peu de logique temporelle ![/b][/brown][/big][/center]
Maintenant qu'on s'est un peu échauffés avec le menu principal, il est temps de regarder comment on va coder le moteur de notre jeu. Voilà ce qu'on va faire dans ce tutoriel :
• Créer un plateau de jeu vide de la taille de l'écran (sans scrolling) ;
• Ajouter un personnage et des animations ;
• Prendre des entrées clavier pendant que les animations tournent.
Pour l'instant la map sera juste un rectangle vide avec des bords, le minimum nécessaire pour déplacer le joueur sans sortir des bornes du niveau. On verra dans le prochain tutoriel comment encoder proprement la map. Notre objectif est d'atteindre le résultat suivant ! :D
[center][img][/img]
[i]Image 1 : Full graphismes et zéro gameplay. Que demande le peuple ?[/i][/center]
Ce tutoriel contient pas mal de code spécifique au jeu qu'on est en train de créer et un peu moins de code gint que le précédent. Cependant, je pense qu'il est utile de réfléchir ensemble à comment séparer les différentes parties du jeu pour rendre le code flexible et élégant à l'échelle du projet complet. Ça peut vous sembler superflu à ce stade, mais il faut réaliser que vous transformez sans cesse le code de votre jeu pour ajouter ou modifier des fonctions, et s'il est mal codé la structure ne [i]résistera[/i] pas. Et donc autant commencer tout de suite à faire les choses bien. ;)
On arrivera vite au point où le code et les fonctions de gint se complexifient par rapport au programme simple de la dernière fois. Je continuerai à expliquer les aspects du langage C au fur et à mesure, mais ce tutoriel ne peut pas être un cours complet de C, donc n'hésitez pas à en consulter un (par exemple [url=https://zestedesavoir.com/tutoriels/755/le-langage-c-1/]celui de Zestes de Savoir[/url]) ou à poser des questions [url=http://www.planet-casio.com/Fr/forums/lecture_sujet.php?id=14915&page=1]sur le topic des commentaires[/url] si quelque chose vous échappe. ^^
[brown][b]Les bases du moteur : joueurs, niveaux et parties[/b][/brown]
On va tout de suite commencer par définir les types fondamentaux du moteur. Je vais le faire dans deux nouveaux fichiers, `engine.h` et `engine.c`. Lorsqu'on programe avec plusieurs fichiers, il est important que chaque fichier source (`.c`) possède un en-tête associé (`.h`) qui récapitule les fonctions et types qui y sont définis. C'est parce que le compilateur compile tous les fichiers sources indépendamment les uns des autres, et c'est cette information qui lui permet de se faire une vision d'ensemble. Vous pouvez trouver de plus amples détails dans le [url=https://www.planet-casio.com/Fr/forums/topic15378-1-tdm-n07-ecrire-des-programmes-c-avec-plusieurs-fichiers.html]TDM n°7[/url] sur ce sujet.
En pratique, le fichier d'en-tête contient une description des types de données manipulés par le fichier source associé et les prototypes de ses fonctions publiques. C'est une sorte de « résumé » si vous voulez. Ici, je vais commencer par ajouter dans `engine.h` la définition d'une structure `player` qui décrit l'information complète d'un joueur présent sur la map. J'en profite aussi pour choisir des valeurs pour désigner les quatre directions donc j'aurai souvent besoin.
[code]#ifndef _MYSTNB_ENGINE_H
#define _MYSTNB_ENGINE_H
/* Directions */
#define DIR_DOWN 0
#define DIR_RIGHT 1
#define DIR_UP 2
#define DIR_LEFT 3
/* struct player: A player on the map, triggering actions as they move */
struct player
{
/* Position in map */
int x, y;
/* Direction currently facing */
int dir;
/* Animation and frame */
struct animation const *anim;
int frame;
};
#endif /* _MYSTNB_ENGINE_H */[/code]
Vous noterez que le fichier commence et se termine par des instructions préprocesseur (lignes qui commencent par `#`). L'objectif de ces lignes est de faire en sorte que les contenus de `engine.h` ne soient lus qu'une seule fois par le compilateur, même si le fichier est inclus plusieurs fois (ce qui arrive souvent quand des en-têtes incluent d'autres en-têtes). Ça marche de la façon suivante : le contenu du fichier n'est lu que si la macro `_MYSTNB_ENGINE_H` n'est pas définie (ça c'est le `#ifndef`), et si c'est le cas alors on lit le fichier et on la définit immédiatement (ça c'est le `#define`). Du coup si on réinclut l'en-tête le `#ifndef` ne passera plus et tout sera ignoré jusqu'à `#endif` tout à la fin.
On fait ça est parce que le compilateur serait très mécontent de voir apparaître deux `struct player`, même si les contenus sont identiques. C'est comme si vous écrivez `int x = 42` deux fois : c'est pas parce que c'est deux fois le même nom et la même valeur que le compilateur va considérer que c'est la même variable. Si tout ça vous échappe un peu, pas d'inquiétude : vous verrez cette construction dans tous les fichiers d'en-tête et vous la connaîtrez par coeur avant d'avoir pu dire « préprocesseur ». :p
Quant à la définition de la structure à proprement parler, rien de bien impressionnant. On y trouve :
• La position du joueur sur la map ;
• La direction où il regarde (pour choisir le bon sprite à afficher) ;
• Et l'animation en cours et le frame où on en est. J'y reviendrai plus tard. Je n'ai pas défini `struct animation` mais c'est pas grave, on a le droit de créer un pointeur vers un type même s'il n'est pas encore bien défini (on dit « incomplet »).
Si vous n'êtes pas encore tout à fait familier·ère avec les structures, c'est le bon moment de se rappeler que cette définition ne crée aucune variable, elle explique simplement ce que les variables de type `struct player` contiendront quand on créera.
On a également besoin de la spritesheet pour pouvoir faire notre affichage. Voici à quoi elle ressemble ; comme d'habitude, ne récupérez pas cette image (qui est agrandie), prenez plutôt la version originale sur le dépôt. Enregistrez-la sous le nom `assets-fx/img/spritesheet.png`.
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-02-spritesheet.png[/img]
[i]Image 2 : Pour les diagonales, on repassera.[/i][/center]
Le joueur se situe bien sûr à l'intérieur d'un niveau que j'appelle [i]map[/i] par pure commodité. C'est simplement une grille avec des espaces libres, des murs, des portes, et quelques items importants placés au sol. Pour l'instant on ne vas pas trop se demander comment les représenter et les convertir (ce sera dans le tutoriel suivant), on va juste donner une version vague de la structure avec les dimensions de la map histoire de pouvoir déplacer le joueur sans sortir des bords.
[code]/* struct map: A map with moving doors and collectibles */
struct map
{
/* Width and height */
int w, h;
/* Raw data */
uint8_t *data;
};[/code]
En fait tous les niveaux que j'ai prévus ont la même taille (sensiblement la taille de l'écran), mais ce serait dommage de ne pas se donner un peu de latitude au cas où le jeu évolue dans le futur. Les données brutes ce sera un tableau avec l'état de chaque cellule de la map, stay tuned. Dans l'immédiat, cette structure nous sera utile pour vérifier que le joueur ne sort pas de la map, c'est-à-dire qu'on veut assurer à tout instant :
• `player.x >= 0 && player.x < map.w`
• `player.y >= 0 && player.y < map.h`
Et comme j'aime bien tout formaliser (c'est un défaut de théoricien), je ne vais pas juste me balader deux variables `player` et `map`, je vais mettre tout ça dans une structure représentant la partie en cours. Ce qui m'incite d'ailleurs à faire un [i]tableau[/i] de joueurs, parce que qui sait, le jeu pourrait évoluer dans le futur. :whistle:
[code]#define PLAYER_COUNT 1
/* struct game: A running game with a map and some players */
struct game
{
/* Current map */
struct map *map;
/* Players */
struct player *players[PLAYER_COUNT + 1];
};[/code]
Comme le nombre de joueurs n'est pas fixe a priori, j'ai juste donné un maximum (`PLAYER_COUNT`) et le tableau se terminera par un `NULL` après le dernier joueur.
Puisque les animations viendront après, on a sensiblement toutes les informations nécessaires pour jouer une partie de mystère noir et blanc... ou plus précisément de mystère blanc étant donné qu'on n'a rien sur la map ! :E
[brown][b]Structure du programme : affichage, logique, et entrées[/b][/brown]
Un point sur lequel je veux m'attarder dès maintenant est la structure du code. Le jeu a pas mal de choses à faire pour fonctionner correctement, et il est important de ne pas les mélanger pour conserver un code extensible qui résistera à tous les tests et modifications qu'on fera pendant le développement.
Ici, on va séparer les tâches en trois catégories :
1. Affichage de l'état du jeu à l'écran (map, joueur et animations)
2. Logique interne et physique (déplacement du joueur, collisions, ouverture des portes)
3. Acquisition des entrées (saisie au clavier et traduction en « instructions »)
Ces trois parties sont sensiblement indépendantes et il est utile de les coder comme telles pour se donner de la flexibilité. Par exemple, il n'y a pas de raison que la vitesse des animations soit liées à la détection des touches. De même, la vitesse des frames à l'écran n'est pas forcément celle des updates internes (FPS vs UPS). (Ici le jeu est au tour par tour donc c'est facile car les updates sont calées sur les saisies au clavier, mais dans un jeu en temps réel la question se poserait.)
L'indépendance doit donc être explicite dans le code, et dans mon cas je vais donner les règles suivantes :
1. L'affichage ne peut que lire les structures du jeu et dessiner à l'écran.
2. La logique ne peut que lire les instructions du joueur et modifier les structures du jeu (joueur, map, etc).
3. La gestion des entrées ne peut que lire le clavier et produire des instructions.
Tout ça sera lié par la fonction principale du programme qui passera les sorties des uns en entrée aux autres et réglera le timing. ^^
[brown][b]Affichage de l'état du jeu à l'écran[/b][/brown]
Commençons par l'affichage de l'état du jeu. Pour l'instant il n'y a pas grand-chose à faire étant donné qu'on n'a que le joueur et pas de map. Par simplicité, je me donne une macro pour calculer la position d'une cellule de la map à l'écran (en supposant qu'on ne scrolle pas) et ça permet tout de suite d'écrire une fonction pour afficher le joueur depuis la spritesheet.
[code]#define CELL_X(x) (-2 + 10 * (x))
#define CELL_Y(y) (-3 + 10 * (y))
static void engine_draw_player(struct player const *player)
{
extern bopti_image_t img_spritesheet;
dsubimage(CELL_X(player->x) - 1, CELL_Y(player->y) - 5,
&img_spritesheet, player->dir * 12, 0, 12, 16, DIMAGE_NONE);
}[/code]
Rien de bien inattendu ici, juste une utilisation un peu maline de `dsubimage()`. Comme vous pouvez le voir, je prends un rectangle de taille 12x16 à la position `player->dir * 12` horizontalement et `0` verticalement : c'est donc le premier frame de l'animation [i]"idle"[/i] pour la direction actuelle du joueur. Dans la deuxième partie de ce tutoriel on ajoutera des vraies animations et donc on viendra modifier cette fonction en conséquence.
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-02-spritesheet-frame1.png[/img]
[i]Image 3 : Tout ce travail et j'utilise qu'un seul frame ![/i][/center]
Pour information, le choix de `CELL_X()` et `CELL_Y()` donne une grille qui a la tête suivante à l'écran. Une partie des cellules est masquée par le bord de l'écran pour gagner de la place.
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-02-mapbounds.png[/img]
[i]Image 4 : C'est asymétrique spécifiquement pour vous embêter.[/i][/center]
Et tout cela suffit pour afficher le jeu complet puisqu'on n'a pas de map.
[code]void engine_draw(struct game const *game)
{
dclear(C_WHITE);
for(int p = 0; game->players[p]; p++)
{
engine_draw_player(game->players[p]);
}
}[/code]
Dans la boucle `for`, la condition de sortie `game->players[p]` est équivalente à `game->players[p] != NULL`, donc j'itère sur tous les joueurs présents sur la map pour les afficher.
[brown][b]Logique interne et physique[/b][/brown]
Voyons maintenant comment on peut se déplacer. Comme mentionné tout à l'heure, on ne veut pas que le joueur puisse sortir de la map ni marcher dans les murs. Du coup j'ajoute une fonction `map_walkable()`, qui doit vérifier qu'une case est accessible (ie. pas un mur) et hardcode actuellement des murs sur les bords, comme sur l'image ci-dessous.
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-02-mapgrid.png[/img]
[i]Image 5 : Contrairement à Portal, il n'y a pas vraiment de sortie.[/i][/center]
[code]/* Check whether a cell of the map is walkable */
static int map_walkable(struct map const *map, int x, int y)
{
return (x >= 1) && (y >= 1) && (x < map->w - 1) && (y < map->h - 1);
}[/code]
À partir de là, on peut écrire une fonction qui tente de déplacer un joueur dans une direction choisie. La fonction renvoie 0 si le déplacement échoue et 1 s'il réussit, car cette information est nécessaire pour décider si le tour est terminé (il se passera des choses avec les portes entre chaque tour de jeu).
[code]int engine_move(struct game *game, struct player *player, int dir)
{
int dx = (dir == DIR_RIGHT) - (dir == DIR_LEFT);
int dy = (dir == DIR_DOWN) - (dir == DIR_UP);
/* Always update the direction */
player->dir = dir;
/* Only move the player if the destination is walkable */
if(!map_walkable(game->map, player->x + dx, player->y + dy)) return 0;
player->x += dx;
player->y += dy;
return 1;
}[/code]
Notez l'astuce consistant à calculer le déplacement en x et en y avec une différence de booléens pour ne pas s'embêter à faire des `if/else` ou un `switch`.
[brown][b]Acquisition des entrées[/b][/brown]
La gestion des entrées sera très basique pour l'instant donc je l'ai laissée dans `main.c`. Le but de cette fonction est simplement de lire les entrées clavier et de renvoyer la direction dans laquelle le joueur demande à se déplacer. On va donc juste chercher `KEY_DOWN`, `KEY_RIGHT`, `KEY_UP` et `KEY_LEFT` et les renvoyer vers leurs `DIR_*` associés. La paragraphe suivant est long simplement parce que c'est une bonne occasion de parler de la fonction `getkey_opt()`. :)
[code]/* Returns a direction to move in */
static int get_inputs(void)
{
int opt = GETKEY_DEFAULT & ~GETKEY_REP_ARROWS;
while(1)
{
int key = getkey_opt(opt, NULL).key;
if(key == KEY_DOWN) return DIR_DOWN;
if(key == KEY_RIGHT) return DIR_RIGHT;
if(key == KEY_UP) return DIR_UP;
if(key == KEY_LEFT) return DIR_LEFT;
}
}[/code]
Une des fonctionnalités assurées par `getkey()` est la répétition des touches. Si vous gardez une touche appuyée, `getkey()` est susceptible de la renvoyer plusieurs fois. Par défaut, seules les touches fléchées sont répétées (comme `GetKey()`), ce qui permet par exemple de se déplacer dans des listes ou des menus. Mais ici, comme le jeu est un puzzle nécessitant de bien réfléchir à chaque coup, on veut que chaque pression déplace le joueur exactement une fois (sinon on risque un faux mouvement qui gâcherait le niveau).
C'est ici que la fonction `getkey_opt()` entre en jeu. `getkey_opt()` est une version générale de `getkey()` (définie dans `<gint/keyboard.h>`) qui possède plein d'options pour personnaliser la façon dont vous lisez le clavier. Il y a deux arguments : d'abord les options et ensuite le timeout. Expliquer tout le comportement de la fonction serait long, donc je vais juste présenter rapidement les options.
• `GETKEY_MOD_SHIFT` et `GETKEY_MOD_ALPHA` activent l'utilisation de SHIFT et ALPHA comme des modifieurs, pour former des combinaisons comme SHIFT+sin → asin. Lorsque ces options sont activées, `getkey_opt()` ne renvoie jamais d'événement ayant `.key == KEY_SHIFT` ou `.key == KEY_ALPHA` et attend à la place que vous appuyiez sur une autre touche avant de s'arrêter. Dans ce cas, l'événement renvoyé contient `.shift == 1` ou `.alpha == 1` pour indiquer l'état des modifieurs. Une application peut par exemple interpréter un événement ayant `.key == KEY_SIN` et `.shift == 1` comme asin. Activé dans `getkey()`.
• `GETKEY_BACKLIGHT` active la combinaison SHIFT+OPTN pour allumer et éteindre le rétroéclairage sur les modèles monochromes qui le supportent (essentiellement la Graph 75+E et ses prédécesseurs). Activé dans `getkey()`.
• `GETKEY_MENU` autorise le retour au menu en appuyant sur la touche `MENU`. Activé dans `getkey()`.
• `GETKEY_REP_ARROWS` et `GETKEY_REP_ALL` activent la répétition des touches directionnelles et de toutes les touches, respectivement. Le délai de répétition est contrôlé par la fonction `getkey_repeat()`. Par défaut, la première répétition se produit après 400 et les suivantes toutes les 40 ms. Le premier est activé dans `getkey()`, pas le second.
• `GETKEY_REP_FILTER` active le filtre de répétitions, un outil puissant qui vous permet de contrôler la répétition des touches avec finesse. Vous pouvez contrôler quelles touches se répétent, combien de fois, sous quel délai, et même changer les délais d'une fois sur l'autre. Activé dans `getkey()`, mais n'a aucun effet tant que vous n'appelez pas `getkey_repeat_filter()` pour configurer tout ça. J'aurai peut-être l'occasion d'en reparler.
Le premier argument de `getkey_opt()` est une combinaison de ces options (qu'on peut écrire avec `+` ou `|` selon votre préférence). Le second argument que j'ai appelé « timeout » est un pointeur sur un entier : si l'entier est ou devient autre chose que 0 pendant que `getkey_opt()` attend, la fonction s'interrompt et s'arrête immédiatement en renvoyant un événement de type `KEYEV_NONE`. Ça permet de contrôler la durée d'attente voire même d'interrompre à l'improviste.
La fonction `getkey()` en elle-même ne fait qu'appeler `getkey_opt()` avec les options que j'ai mentionnées ci-dessus (la combinaison s'appelle `GETKEY_DEFAULT`) et un délai illimité.
Dans `get_inputs()`, on donne comme options `GETKEY_DEFAULT & ~GETKEY_REP_ARROWS`, ce qui reprend les options par défaut de `getkey()` à l'exception de `GETKEY_REP_ARROWS`, et garde un délai illimité. En deux mots : on désactive la répétition des touches fléchées. ^^
Le reste est direct, comme annoncé on cherche les touches directionnelles et on renvoie les `DIR_*` correspondants. Voyons voir maintenant comment implémenter la logique du jeu avec tout ça !
[brown][b]Logique du jeu avec les déplacements[/b][/brown]
Vous allez voir qu'en fait c'est plus court que ça en a l'air.
[code]int main(void)
{
int level = main_menu();
struct player singleplayer = {
.x = 2,
.y = 3
};
struct map map = {
.w = 13,
.h = 7
};
struct game game = {
.map = &map,
.players = { &singleplayer, NULL }
};
int level_finished = 0;
while(!level_finished)
{
int turn_finished = 0;
while(!turn_finished)
{
engine_draw(&game);
dupdate();
int dir = get_inputs();
turn_finished = engine_move(&game, &singleplayer, dir);
}
/* Update doors, etc */
}
return 1;
}[/code]
On commence par appeler le menu principal que j'ai déplacé dans une fonction `main_menu()`, et ensuite on prépare la partie. Pour cela, on crée :
• Un joueur, dont la position initiale est (2,3) ;
• Une map de taille 13x7 (la taille de l'écran présenté précédemment) ;
• Une partie sur ladite map avec ledit joueur.
Ensuite, la boucle principale tourne tant que le niveau n'est pas terminé. À l'intérieur de la boucle, on fait le lien entre les trois parties qu'on a développées jusqu'à présent : d'abord on affiche l'état du jeu, ensuite on prend des entrées, et enfin on déplace le joueur selon les entrées saisies.
Le jeu est au tour par tour et les portes changent d'état entre les tours (action symbolisée par le commentaire "Update doors, etc"), donc j'ai besoin de savoir précisément quand un tour se termine. Ce n'est pas évident car si le joueur demande une direction dans laquelle il/elle ne peut pas se déplacer, le tour ne sera pas fini (et s'il y a plusieurs joueurs la condition n'est pas non plus évidente). Donc j'ai une variable `turn_finished` pour contrôler ça.
Vous noterez que `level_finished` n'est jamais mis à 1 donc la boucle ne se termine jamais ; ce n'est pas grave parce qu'on peut toujours fuir vers le menu principal durant les appels à `getkey_opt()` en appuyant sur MENU. ^^
Et voilà le résultat.
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-02-basic-movement.gif[/img]
[i]Image 6 : ... ouais en fait non, c'est tout nul.[/i][/center]
Vous comprenez pourquoi j'ai pas mis ça tout en haut du tutoriel. Allez, on va rajouter des animations, ça aidera. ^^"
[brown][b]Animations structurées[/b][/brown]

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB