tutorial-03: final take

This commit is contained in:
Lephenixnoir 2021-05-04 18:30:19 +02:00
parent 761dbd51d4
commit b3bc41fc39
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
1 changed files with 115 additions and 34 deletions

View File

@ -1,43 +1,47 @@
[center][big][brown][b]03 : Des portes... [i]beaucoup[/i] de portes.[/b][/brown][/big][/center]
Dans ce tutoriel, on va passer le jeu au niveau supérieur. Jusqu'ici, on a codé un menu principal et un petit moteur de déplacement avec des animations. Pour l'instant, on a laissé la map de côté et on a hardcodé les positions des murs pour éviter que le joueur ne sorte de la map. Tout cela change aujourd'hui, parce qu'on va ajouter une map animée et les premières mécaniques de jeu ! Plus précisément :
Dans ce tutoriel, on va passer le jeu au niveau supérieur. Jusqu'ici, on a codé un menu principal et un petit moteur de déplacement avec des animations. Pour l'instant, on a laissé la map de côté et on a hardcodé les positions des murs pour éviter que le joueur ne sorte de l'écran. Tout cela change aujourd'hui, parce qu'on va ajouter une map animée et les premières mécaniques de jeu ! Plus précisément :
• On va voir comment convertir des objets personnalisés (ici une map) avec [brown][b]fxconv[/b][/brown] ;
• Et on va étendre le moteur de jeu pour intégrer des changements sur la map.
• On va étendre le moteur de jeu pour intégrer des éléments dynamiques sur la map ;
• Et je vais coder suffisamment de mécaniques pour faire marcher le premier niveau.
Le final ressemblera à ça !
TODO
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-03-level-1.gif[/img]
Il y a un peu moins de gint ici, et un peu plus de code du jeu. Si vous programmez votre propre jeu en lisant ce tutoriel, faites bien attention à la structure du code. Comme j'en ai parlé dans le tutoriel précédent, il est [i]très important[/i] de séparer le rendu, la simulation du jeu et la gestion des entrées avant que le programme ne devienne monstrueux et impossible à maintenir (... parce que c'est ce qui arrivera si vous ne faites pas attention ^^).
[i]Image 1 : Si je vous spoile la solution y'a plus vraiment de mystère ![/i][/center]
Il y a un peu moins de gint ici, et un peu plus de code du jeu. Si vous programmez votre propre jeu en lisant ce tutoriel, faites bien attention à la structure du code. Comme j'en ai parlé dans le tutoriel précédent, il est [i]très important[/i] de séparer le rendu, la simulation du jeu et la gestion des entrées avant que le programme ne devienne monstrueux et impossible à maintenir (... ce qui arrivera [i]tout seul[/i] sauf si vous y portez une attention particulière ^^).
[brown][b]Format de la map en tant qu'asset[/b][/brown]
Pour l'instant, c'est parti ! La map du premier niveau ressemblera à ça. Ça n'inclut pas toutes les mécaniques, mais les grandes idées y sont. On a la zone de départ en bas à gauche, la zone d'arrivée en bas à droite, et le but c'est de passer de l'un à l'autre en utilisant l'ouverture et la fermeture automatique des portes, dont il existe deux types : horizontaux et verticaux.
Pour l'instant, c'est parti ! La map du premier niveau ressemblera à ça. Ça n'inclut pas toutes les mécaniques, mais les grandes idées y sont. On a la zone de départ en bas à gauche, la zone d'arrivée en bas à droite, et le but c'est de passer de l'un à l'autre en utilisant l'ouverture et la fermeture automatique des portes, dont il existe deux types : les horizontales et les verticales.
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-03-map1.png[/img]
[i]Image 1 : Le titre a pas menti, y'a [b]beaucoup[/b] de portes.[/i][/center]
[i]Image 2 : Le titre a pas menti, y'a [b]beaucoup[/b] de portes.[/i][/center]
Je pourrais tenter de générer la map directement à partir de l'image, mais ça serait assez compliqué. Pour ce projet tout simple qui n'a que 8 maps toutes de la même taille, je vais simplement fournir une image « squelette » du niveau (juste les murs en gros) et le programme affichera les points de départ et d'arrivée, les portes, et autres items par-dessus. Le rôle et la position des objets seront décrits dans un fichier texte qu'on va convertir avec fxconv parce que ce serait quand même dommage d'avoir à décrire une map en C.
Un vrai projet super ambitieux éditerait certainement les maps dans un outil spécialisé comme [url=https://www.mapeditor.org/]Tiled[/url], auquel cas le fichier texte serait remplacé par le fichier de sauvegarde de Tiled. Le principe serait le même, je vais simplement me contenter du fichier texte par simplicité pour ce tutoriel. Il ressemblera à ça :
[code]##### #####
# ##### #
# a a #
# b a #
##A### ###A##
# a a #
# a b #
# ~ ##### @ #
##### #####
a: #.
b: .#
A: #.[/code]
Les `#` représentent des murs, les lettres minuscules représentent des portes verticales et les lettres minuscules des portes horizontales. `~` et `@` sont le point de départ et le point d'arrivée, respectivement.
À la fin, il y a des informations qui décrivent le cycle d'ouverture/fermeture de chaque porte (ici les portes sont synchronisées donc j'ai utilisé qu'une seule lettre). Je ne voudrais pas vous gâcher les mécaniques donc pour l'instant je n'en dis pas plus. :p
À la fin, il y a des informations qui décrivent le cycle d'ouverture/fermeture de chaque porte (certaines portes ont le même cycle et donc utilisent la même lettre). Je ne voudrais pas vous gâcher les mécaniques donc pour l'instant je n'en dis pas plus. :p
[brown][b]Format de la map en tant qu'objet du programme[/b][/brown]
Pour obtenir ces informations dans le jeu, on va commencer par ajouter dans le code (plus précisément dans `engine.h`) la structure qu'on veut obtenir à la fin. La conversion de la map devra prendre en entrée le fichier texte ci-dessus et produire en sortie structure de map, donc c'est important de bien définir le format tout de suite. J'avais donné un début dans le tutoriel précédent, qu'on peut maintenant compléter avec les détails en plus.
Pour obtenir ces informations dans le jeu, on va commencer par ajouter dans le code (plus précisément dans `engine.h`) la structure qu'on veut obtenir à la fin. La conversion de la map devra prendre en entrée le fichier texte ci-dessus et produire en sortie une structure de map, donc c'est important de bien définir le format tout de suite. J'avais donné un début dans le tutoriel précédent, qu'on peut maintenant compléter avec les détails en plus.
[code]/* struct map: A map with moving doors, collectibles, and fog */
struct map
@ -76,7 +80,7 @@ Je numérote l'air, les murs, les points de départ et d'arrivée de 0 à 3. Les
Moralement `tiles` dans la `struct map` c'est un tableau de `enum map_tile`. Mais comme les énumérations sont des `int` par défaut ça voudrait dire que chaque case de la map occupe 4 octets en mémoire, et jeux avoir un type qui occupe juste 1 octet. Donc j'ai mis `uint8_t` à la place ; ça revient au même vu que tout ça sont des entiers. ;)
Donc voilà ce qu'on doit produire : une structure comprenant 156 octets de données plus un pointeur de 4 octets pointan vers l'image de fond, et un autre pointeur de 4 octets pointant vers `w*h` octets de données. Voyons voir comment faire ça avec fxconv. ;)
Donc voilà ce qu'on doit produire : une structure comprenant 156 octets de données plus un pointeur (de 4 octets) pointant vers l'image de fond, et un autre pointeur (de 4 octets) pointant vers `w*h` octets de données que l'on va générer en même temps que la structure. Voyons voir comment faire ça avec fxconv. ;)
[brown][b]Conversion de la map avec fxconv[/b][/brown]
@ -106,9 +110,9 @@ def convert_map(input, output, params, target):
fxconv.elf(data, output, "_" + params["name"], **target)[/code]
Il y a plusieurs choses à noter.
• D'abord il y a un module `fxconv` qui fournit quelques fonctions utilitaires et l'indispensable fonction `elf` dont je reparle dans un instant. Votre boulot se réduit (essentiellement) à lire le fichier d'entrée et à produire un objets `bytes()` avec les octets de la variable finale, ici la `struct map`.
• D'abord le fxSDK contient un module Python `fxconv` qui fournit quelques fonctions utilitaires et l'indispensable fonction `elf` dont je reparle dans un instant. Votre boulot se réduit (essentiellement) à lire le fichier d'entrée et à produire des objets `bytes()` avec les octets de la variable finale, ici la `struct map`.
• La fonction `convert` est appelée avec 4 paramètres : `input` et `output` sont les noms du fichier d'entrée et du fichier de sortie, respectivement. `params` est un dictionnaire avec les paramètres indiqués dans `fxconv-metadata.txt` (plus quelques paramètres toujours présents), et `target` indique la calculatrice cible (Graph mono ou Graph 90+E), le compilateur à utiliser, et d'autres détails. `params["name"]` est toujours défini même si vous avez mis `name_regex` (le nom est calculé plus tôt).
• La fonction `convert` est appelée avec 4 paramètres : `input` et `output` sont les noms du fichier d'entrée et du fichier de sortie, respectivement. `params` est un dictionnaire avec les paramètres indiqués dans `fxconv-metadata.txt` (ainsi que `"type"` indiquant le type de la conversion, et `"name"` indiquant le nom de la variable à créer), et `target` indique la calculatrice cible (Graph mono ou Graph 90+E), le compilateur à utiliser, et d'autres détails. `params["name"]` est toujours défini même si vous avez mis `name_regex` (la regex est appliquée plus tôt).
• `params["custom-type"]` est le type personnalisé spécifié dans `fxconv-metadata.txt`. J'aime bien faire un if/else ici et avoir une sous-fonction par type de données pour ne pas tout mélanger. Si vous reconnaissez le type, vous devez renvoyer 0 ; sinon, vous devez renvoyer 1 auquel cas fxconv tentera le convertisseur personnalisé suivant (et si tous renvoient 1, il indiquera que le type est inconnu).
@ -198,17 +202,17 @@ C'est un peu long, mais il y a de tout dans cette conversion donc c'est une bonn
On commence par lire le fichier source `input`. Ici c'est un fichier texte, je le lis directement avec `open()`. Quand c'est une image, vous pouvez utiliser `PIL` pour la charger directement et avoir un joli objet idiomatique Python pour faire toutes les opérations dont vous avez besoin. Si vous voulez des exemples de ça, il en y en a plusieurs dans [url=https://gitea.planet-casio.com/Lephenixnoir/fxsdk/src/branch/master/fxconv/fxconv.py]les sources de fxconv[/url] (le plus simple étant `convert_libimg_fx()`).
La suite est spécifique à mon format de map. Un ligne blanche ("`\n\n"`) sépare les contenus de la map (`tiles`) des infos sur les cycles de portes (`cycle_texts`), et je sépare encore les deux en lignes. Je devine la taille de la map à partir des tiles qui sont saisies, et pour l'instant je fixe `fog` à 0. Je récupère dans le nom de fichier (`"lv1.txt"`) la partie sans extension ("`lv1"`) comme ça je pourrai automatiquement mettre un pointeur vers l'image du même nom (qui sera "`img_lv1`").
La suite est spécifique à mon format de map. Un ligne blanche ("`\n\n"`) sépare les contenus de la map (`tiles`) des infos sur les cycles de portes (`cycle_texts`), et je sépare encore les deux en lignes. Je devine la taille de la map à partir du texte saisi, et pour l'instant je fixe `fog` à 0. Je récupère dans le nom de fichier (`"lv1.txt"`) la partie sans extension ("`lv1"`) comme ça je pourrai automatiquement mettre un pointeur vers l'image du même nom (qui sera "`img_lv1`").
Enfin arrive la conversion de la map à proprement parler. Vous pouvez voir que je commence avec un tableau de `w*h` octets, qui sont tous 0 (`TILE_AIR`) initialement. Ensuite je remplis en lisant les caractères de chaque ligne. Je note les clés 0...9, les portes verticales a...z et les portes horizontales A...Z.
Enfin arrive la conversion de la map à proprement parler. Vous pouvez voir que je commence avec un tableau de `w*h` octets, qui sont tous 0 (`TILE_AIR`) initialement. Ensuite je remplis en lisant les caractères de chaque ligne. Je note les clés 0...9, les portes verticales a...h et les portes horizontales A...H.
Si je tombe sur quelque chose d'imprévu, je lève une exception de type `fxconv.FxconvError`, qui s'affiche à l'écran et arrête la compilation. Je vous conseille de vraiment blinder vos conversions contre les imprévus. Si une erreur se glisse mais n'est pas détectée c'est comme si votre programme contient une erreur mais le compilateur ne s'en rend pas compte : le résultat n'aura pas de sens et il sera très difficile de réaliser que le problème vient de là.
Si je tombe sur quelque chose d'imprévu, je lève une exception de type `fxconv.FxconvError`, que fxconv affiche à l'écran avant de renvoyer une erreur au Makefile. Je vous conseille de vraiment blinder vos conversions contre les imprévus. Si une erreur se glisse mais n'est pas détectée, l'objet converti sera inutilisable mais vous n'en serez pas averti·e. C'est comme si votre programme contient une erreur mais le compilateur le laisse passer sans rien dire : le résultat n'aura pas de sens et il sera très difficile de réaliser que le problème vient de là.
L'étape suivante consiste à analyser le texte des cycles pour obtenir une représentation structurée. Si vous ne connaissez pas les regex ce n'est pas grave, vous pouvez admettre qu'à la fin `cycles` est un dictionnaire du type `{"a": b"#.", "A": b"#."}`. Tout ou presque est en `bytes` au lieu de chaînes de caractères parce que tout doit être des `bytes` lors de la génération du fichier converti. ^^
L'étape suivante consiste à analyser le texte des cycles pour obtenir une représentation structurée. Si vous ne connaissez pas les regex ce n'est pas grave, vous pouvez admettre qu'à la fin `cycles` est un dictionnaire du type `{"a": b"#.", "A": b"#."}`. Tout ou presque est en `bytes` au lieu de chaînes de caractères parce que c'est ce dont on aura besoi lors de la génération du fichier de sortie à la fin de la fonction. ^^
La troisième étape consiste à encoder ces informations en une paire de variables `door_cycle` et `door_cycle_index` de la structure. En gros `door_cycle` c'est la concaténation de tous les cycles ensemble avec un espace entre chaque (ici `b"#. #. "`), et `door_cycle_index` indique où commence chaque cycle (ici celui de `"a"` commence à la position 0 et celui de `"A"` commence à la position 3). Ce format est pratique parce que toutes les informations tiennent dans deux variables de taille fixe.
Remarquez que dans la structure, `door_cycle` fait 128 octets et `door_cycle_index` fait 16 octets donc je suis obligé de respecter ces tailles. Dans le premier cas, j'ai une ligne qui rajoute des 0 à `door_cycle` pour amener la taille à 128. Dans le second cas, je crée le tableau directement avec 16 octets dans le constructeur `bytearray(16)` et ensuite je ne touche plus à la taille.
Remarquez que dans la structure, j'ai annoncé que `door_cycle` ferait 128 octets et `door_cycle_index` 16 octets, donc je suis obligé de tenir parole et respecter ces tailles. Dans le premier cas, j'ai une ligne qui rajoute des 0 à `door_cycle` pour amener la taille à 128. Dans le second cas, je crée le tableau directement avec 16 octets dans le constructeur `bytearray(16)` et ensuite je ne touche plus à la taille.
Ensuite, on génère la map. Ce code-là est super important donc je vous le remets ici. ;)
@ -231,17 +235,17 @@ Le problème c'est que notre structure contient des pointeurs :
• Il y a un pointeur `img` vers une variable externe.
• Il y a un pointeur `tiles` vers un tableau qu'on veut générer en même temps que la structure.
Et ça on ne peut pas connaître leur valeur durant la conversion (l'explication complète dans la prochaine section). Il y a un mécanisme pour délayer leur calcul, mais c'est un peu compliqué et donc fxconv fournit une classe `fxconv.ObjectData()` qui fait tout ça automatiquement. Pour créer une structure qui contient des pointeurs, vous commencez par créer un objet vide type `ObjectData()`, et ensuite vous pouvez lui ajouter successivement des composants.
Et ça on ne peut pas connaître leur valeur durant la conversion (l'explication complète de ce problème est détaillée dans la prochaine section). Il y a un mécanisme pour délayer leur calcul, mais c'est un peu compliqué et donc fxconv fournit une classe `fxconv.ObjectData()` qui fait tout ça automatiquement. Pour créer une structure qui contient des pointeurs, vous commencez par créer un objet vide type `ObjectData()`, et ensuite vous pouvez lui ajouter successivement des composants de trois types avec `+` ou `+=` :
[b]Octets fixes.[/b] Si vous ajoutez un objet de type `bytes` ou `bytearray`, les octets sont ajoutés directement au résultat de la conversion. Par exemple cette ligne crée les trois premiers `int` de la `struct map`.
[code]o += fxconv.u32(w) + fxconv.u32(h) + fxconv.u32(fog)[/code]
Il y a trois fonctions `fxconv.u8`, `fxconv.u16` et `fxconv.u32` pour créer des entiers non-signés de 1, 2 et 4 octets (ce qui correspond aux types `char`, `short` et `int`, ou si vous préférez les types de `<stdint.h>`, `uint8_t`, `uint16_t` et `uint32_t`). Si vous n'êtes pas familier·e avec cets types entiers et leurs tailles, vous pouvez voir le [url=https://www.planet-casio.com/Fr/forums/topic16574-1-tdm-18-comprendre-les-donnees-et-leurs-representations.html]TDM 18[/url] qui aborde précisément ce sujet.
Pour créer des entiers non-signés, le module `fxconv` fournit trois fonctions `fxconv.u8`, `fxconv.u16` et `fxconv.u32`. Elles génèrent respectivement des entiers de 8 bits (1 octet, aka. `char` ou `uint8_t`), 16 bits (2 octets, aka. `short` ou `uint16_t`) et 32 bits (4 octets, aka. `int` ou `uint32_t`). Si vous n'êtes pas familier·ère avec ces histoires de types entiers de taille variable, vous pouvez voir le [url=https://www.planet-casio.com/Fr/forums/topic16574-1-tdm-18-comprendre-les-donnees-et-leurs-representations.html]TDM 18[/url] qui aborde précisément ce sujet.
[b]Pointeurs vers des variables externes.[/b] Si vous ajoutez `ref("variable")`, un pointeur vers la variable est ajouté au résultat de la conversion. Un pointeur fait toujours 4 octets. Par exemple cette ligne ajoute un pointeurs vers l'image de fond (ici c'est `"img_lv1"`).
[b]Pointeurs vers des variables externes.[/b] Si vous ajoutez `ref("variable")`, un pointeur vers la variable est ajouté au résultat de la conversion. Un pointeur fait toujours 4 octets. Par exemple cette ligne ajoute un pointeur vers l'image de fond (ici c'est `"img_lv1"`).
[code]o += fxconv.ref(f"img_{filename}")[/code]
La commande exacte est `ref(<variable> [,offset=<offset>])`, ça ajoute un pointeur vers `(void*)&variable + offset`. L'offset est en octets (et pas en taille d'objets comme en C !).
La commande exacte est `ref(<variable> [,offset=<offset>])`, ça ajoute un pointeur vers `(void*)&variable + offset`. Attention l'offset est en octets et pas en taille d'objets comme en C.
[b]Pointeurs vers des données annexes.[/b] Si vous ajoutez `ref(octets)` où `octets` est soit un `bytes` soit un `bytearray`, alors les octets sont ajoutés en annexe du résultat et un pointeur vers le résultat est ajouté à la structure. Par exemple, cette ligne ajoute les octets de `encoded_tiles` en annexe de la `struct map` et un pointeur vers cette annexe à la structure.
@ -250,6 +254,15 @@ La commande complète est `ref(<bytes_like> [,padding=<padding>])`. Si vous spé
Et ensuite, on termine avec `fxconv.elf()` qui déplie tout, génère les octets et les références pour les pointeurs, et produit le fichier de sortie avec la structure nouvellement convertie. :D
Remarquez que du coup la map référence `img_lv1` donc il faut fournir `assets-fx/img/lv1.png` sinon l'édition des liens échoue puisqu'il lui manque des variables. ;)
Ce système de conversion est largement supérieur à simplement déclarer des variables avec les contenus dans le code, pour plusieurs raisons ! ^^
• Si vous éditez à la main, vous avez toute la liberté de choisir un format agréable à saisir sans compromettre l'utilisation d'une format optimisé dans le code. Par exemple mon stockage des cycles de portes serait horrible à faire à la main.
• Si vous utilisez un éditeur de niveaux maison, il est très dur (sinon impossible) de lire un niveau à partir de sa `struct map` écrite en C, donc en fait il est probable qui vous ayez déjà un format à vous. Dans ce cas fxconv s'intègre plus facilement à la chaîne de compilation qu'un générateur de code C, et le convertisseur Python est généralement plus simple ou plus court.
• Vous pouvez utiliser des éditeurs externes comme [url=https://www.mapeditor.org/]Tiled[/url] ou [url=https://www.aseprite.org/]Aseprite[/url] si vous prenez le temps d'analyser leur format de fichier. (Est-ce que fxconv supportera directement ces formats un jour ? Peut-être. ;) )
• Pour toute modification, il suffit d'enregistrer le fichier texte décrivant la map, recompiler, et tout est automatique. Si vous aviez l'habitude de Sprite Coder ça élimine totalement ce genre d'étape intermédiaire.
[brown][b]Parenthèse technique : pourquoi gérer les pointeurs différemment ?[/b][/brown]
On vient de voir que les pointeurs doivent être gérés spécialement, et qu'on ne peut pas connaître leur valeur du tout pendant la conversion.
@ -267,17 +280,19 @@ Lorsque ce fichier est compilé, le compilateur génère les octets qui serviron
Pour résoudre ce problème, le compilateur utilise un mécanisme de [i]références[/i] conçu spécifiquement pour ça. Le compilateur génère les octets 00 00 00 00 pour `px` et 00 00 00 08 pour `px8`, et note quelque part dans le fichier `.o` (le résultat de la compilation) que ces deux valeurs sont [i]relatives à `&x`[/i]. L'éditeur de liens, lorsqu'il choisit l'adresse dans la RAM où `x` sera chargé, ajoute `&x` aux deux valeurs, qui deviennent alors correctes.
La capacité de l'éditeur de liens de modifier à la volée des valeurs [i]relatives à des adresses de variables[/i] est très puissante et permet sur un ordinateur de compiler efficacement des programmes immenses, d'avoir des bibliothèques dynamiques, et même de charger du code à l'exécution (par exemple des mods pour votre jeu préféré). ^^
La capacité de l'éditeur de liens à modifier à la volée des valeurs [i]relatives à des adresses de variables[/i] est très puissante et permet sur un ordinateur de compiler efficacement des programmes immenses, d'avoir des bibliothèques dynamiques, et même de charger du code à l'exécution (par exemple des mods pour votre jeu préféré). ^^
Je pense que vous voyez où je veux en venir. Durant notre conversion de map, on ne spécifie pas nous-mêmes les valeurs des pointeurs `img` et `tiles`, on se contente de dire que c'est des valeurs relatives à des adresses de variables, et l'éditeur de liens se charge de déterminer la valeur pour nous bien plus tard juste avant de produire le fichier g1a/g3a. :)
J'ai brièvement mentionné que le compilateur ajoute à son fichier de sortie (un fichier objet `.o`) une sorte d'« annotation » indiquant quelles valeurs sont relatives et relatives à qui. fxconv produit aussi des fichiers `.o` (durant l'appel à `fxconv.elf` ; ELF est le nom du format des fichiers `.o`), et bénéficie donc de ce mécanismes.
J'ai brièvement mentionné que le compilateur ajoute à son fichier de sortie (un fichier objet `.o`) une sorte d'« annotation » indiquant quelles valeurs sont relatives et relatives à qui. fxconv produit aussi des fichiers `.o` (durant l'appel à `fxconv.elf` ; ELF est le nom du format des fichiers `.o`), et bénéficie donc de ce mécanisme.
Par contre, créer les annotations c'est laborieux surtout à la main. Du coup fxconv génère un morceau de code assembleur et l'assemble immédiatement, parce que l'assembleur `as` sait très bien produire ce genre d'annotations. Donc en fait ce que `ObjectData()` vous cachez c'est qu'il génère du texte assembleur pour référencer les symboles, et ensuite `fxconv.elf()` invoque l'assembleur pour produire les annotations. ^^
Par contre, créer les annotations c'est laborieux surtout à la main. Du coup fxconv produit une description en assembleur de la structure et la fait assembler sur-le-champ, puisque `as` sait très bien produire toutes les annotations. Donc en fait ce que `ObjectData()` vous cache c'est qu'il génère du texte assembleur pour référencer les symboles, et ensuite `fxconv.elf()` invoque l'assembleur pour produire les annotations. ^^
Notez que vous pouvez toujours joindre du code assembleur au résultat de la conversion vous-même (notamment pour générer des symboles annexes ou véritablement du code exécutable) en donnant une valeur au paramètre optionnel `assembly` de `fxconv.elf()`.
[brown][b]Utilisation de la map dans le programme[/b][/brown]
On peut tout de suite intégrer les nouvelles informations dans le moteur du programme. Au début de la fonction `engine_draw()`, je rajoute un `dimage()` pour afficher l'image de fond du niveau :
Maintenant qu'on a des `struct map` complètes et un premier niveau disponible dans le programme, on peut modifier le moteur pour exploiter toutes les nouvelles données. Au début de la fonction `engine_draw()`, je rajoute un `dimage()` pour afficher l'image de fond du niveau :
[code]void engine_draw(struct game const *game)
{
@ -308,26 +323,92 @@ struct game game = {
Et voilà, on peut maintenant se déplacer sur la map et les collisions sont fonctionnelles ! :D
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-03-colliding-map.gif[/img]
[i]Image 2 : Toujours pas de gameplay mais pour ma défense on s'en approche.[/i][/center]
[i]Image 3 : Toujours pas de gameplay mais pour ma défense on s'en approche.[/i][/center]
Vous pouvez consulter le code à ce stade au niveau du [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/src/commit/80167969fe39de1536af094e6db8e642a8b3718e]commit `80167969f` sur le dépôt[/url].
[brown][b]Animation des tiles sur la map[/b][/brown]
Là j'ai surtout codé le jeu sans apporter beaucoup de modifications. Le système d'animations que j'ai construit n'est pas lié spécifiquement au joueur et peut être utilisé pour à peu près n'importe quoi.
Dans cette partie j'ai surtout codé le jeu sans apporter beaucoup de modifications spécifiques à gint. La principale modification a été d'ajouter dans la `struct game` décrivant la partie en cours un tableau d'informations dynamiques de tiles. Comme la `struct map` qui est issue de la conversion est en lecture seule (comme tout objet issu de la conversion, par défaut), il est hors de question de la modifier. Donc toutes les infos dynamiques sont dans la `struct game`.
Pour coder les mécaniques et gérer les animations, j'ai ajoutés dans la `struct game` un tableau d'informations dynamiques de tiles ; chaque tile qui peut changer d'état pendant le jeu a une entrée avec sa position, son état actuel et son animation. Au chargement du niveau, des infos dynamiques de tiles sont créées pour l'entrée, la sortie, les portes et les clés, avec les animations qui vont bien. ;)
Pour implémenter les mécaniques et gérer les animations, j'ai défini un concept de « tile dynamique ». Ce sont des tiles qui peuvent changer d'état pendant le jeu. Les données d'une tile dynamique comprennent leur position sur le map, leur état actuel, et une animation.
Les animations pour l'entrée et la sortie n'ont qu'une phase et émettent des sortes de particules régulièrement dans un sens qui suggère le rôle de la case.
[code]/* struct dynamic_tile: Dynamic tile information */
struct dynamic_tile
{
/* Position */
int x, y;
/* Current state */
int state;
int state2;
/* Whether animation is idle */
int idle;
/* Current animation */
struct anim_data anim;
};[/code]
Le système d'animations que j'utilise pour le joueur fonctionne tout à fait pour les tiles aussi, en fait il décrit simplement des transitions entre des images de différentes durées. Pour ceux que ça intéresse, voilà comment ça se passe :
Les animations pour les portes sont un peu plus élaborées. Il y a quatre phases : porte fermée, porte en cours d'ouverture, porte ouverte, porte en cours de fermeture (et pareil pour les portes horizontales). La première et la troisième sont statiques (elles bouclent sur un unique frame), les deux autres affichent une succession de frames puis transitionnent vers leur phase statique naturelle.
[code]/* struct anim_data: Data for currently-running animations */
struct anim_data
{
/* Animation update function */
anim_function_t *function;
/* Frame to draw */
struct anim_frame img;
/* On-screen entity displacement */
int dx, dy;
/* Animation direction */
int dir;
/* Current frame */
int frame;
/* Duration left until next frame; updated by the engine. Animation
function is called when it becomes negative or null */
int duration;
};[/code]
Une animation contient essentiellement une référence à l'image en cours (`img`) qui est une sous-image d'une spritesheet ou tilesheet, des paramètres d'affichage et de direction, le numéro du frame actuel de l'animation (`frame`) et la durée restante avant le prochain frame (`duration`).
L'état dynamique est utilisé pour stocker la progression de chaque porte dans son cycle d'ouverture/fermetur et indiquer le cas particulier où une porte est bloquée par un joueur (si le joueur entre juste avant qu'elle n'essaie de se fermer).
À chaque update le moteur du jeu réduit `duration` de la durée écoulée depuis l'update précédente (dans mon cas c'est fixé à 25 ms) et quand `duration` tombe à 0 ou en-dessous de 0, la fonction d'animation `function` est appelée pour passer au frame suivant.
On peut regarder par exemple la fonction d'animation de la tile de début de niveau.
[code]int anim_tile_start(struct anim_data *data, int init)
{
data->function = anim_tile_start;
data->frame = init ? 0 : (data->frame + 1) % 5;
data->duration = (data->frame == 0 ? 1000 : 150);
data->img = anim_frame(&anim_tiles, data->frame, 0);
return 0;
}[/code]
Cette fonction commence par indiquer qui devra être appelé pour la prochaine update dans `data->function` (elle-même) et le prochain frame à afficher (le premier quand on initialise, le suivant modulo 5 sinon). On en déduit la durée (qui ici est 1 seconde pour le frame initial est 150 ms pour les autres) et enfin l'image à afficher (`anim_frame()` sert juste à extraire une sous-image dans la tilesheet).
Dans le cas d'une animation comme l'ouverture de porte, lorsqu'on atteint le frame final on veut faire une transition vers l'animation pour les portes qui sont ouvertes et à l'arrêt, et dans ce cas [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/src/commit/c2178fd2b03041ff7535d446ecbb4c37ff2be2e4/src/animation.c#L150]il y a une condition[/url] qui teste si on a atteint la fin de l'animation d'ouverture et appelle la fonction d'animation des portes ouvertes à l'arrêt quand ça se produit.
Tout ça donc marche aussi bien pour le joueur que pour les tiles. Ce n'est probablement pas la meilleure manière d'implémenter des animations, mais c'est relativement flexible et ça évite au moteur d'animations de s'inscruter dans le moteur de jeu. ^^
Revenons aux tiles dynamiques du coup. Au chargement du niveau, des tiles dynamiques sont crées pour l'entrée, la sortie, les portes et le clés. Les animations pour l'entrée et la sortie tournent en boucle. Les animations des portes sont séparées en deux fixes (porte ouverte et porte fermée) et deux transitoires (porte en cours d'ouverture et portes en court de fermeture).
L'état des tiles dynamiques est utilisé pour stocker la progression de chaque porte dans son cycle d'ouverture/fermeture, et indiquer le cas particulier où une porte est bloquée en position ouverte par un joueur (si le joueur entre juste avant qu'elle n'essaie de se fermer).
[brown][b]Premiers éléments de gameplay[/b][/brown]
À ce stade je pense que le gameplay est clair pour vous. On veut traverser chaque niveau de son entrée vers sa sortie, mais il y a des portes partout et elles passent leur temps à s'ouvrir et se fermer. Et donc on tourne en rond en essayant de comprendre les cycles. :E
Pour implémenter réellement le gameplay il suffit de faire avancer chaque porte d'une étape dans son cycle à la fin de chaque tour, et d'empêcher le jour de se déplacer sur une case où il y a une porte fermée.
Pour implémenter réellement le gameplay il suffit de faire avancer chaque porte d'une étape dans son cycle à la fin de chaque tour, et d'empêcher le joueur de se déplacer sur une case où il y a une porte fermée.
Et voici, chers lecteurs, le premier niveau de [b]Mystère noir et blanc[/b] ! :D
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-03-level-1.gif[/img]
[i]Image 3 : Si je vous spoile la solution y'a plus vraiment de mystère ![/i][/center]
[i]Image 4 : Gamer focus activated![/i][/center]
Vous pouvez jouer à ce niveau sur toutes les plateformes supportées par gint (pour un add-in de cette taille, normalement toutes les Graph mono) en [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/commit/c2178fd2b03041ff7535d446ecbb4c37ff2be2e4/MystNB.g1a]téléchargeant `MystNB.g1a` sur le dépôt[/url] (lien direct) au niveau du [url=https://gitea.planet-casio.com/Lephenixnoir/mystnb/src/commit/c2178fd2b03041ff7535d446ecbb4c37ff2be2e4] commit `c2178fd2b`[/url]. :)
[brown][b]Conclusion[/b][/brown]
Ce tutoriel présente toutes les notions nécessaires à la conversion d'assets spécifiques pour un jeu avec fxconv. En plus de la map qu'on a convertie ici, on peut convertir des descriptions d'objets, des séquences de dialogues, des cinématiques, des listes de quêtes, des traductions de textes, et tous autres assets spécifiques à des applications et qui peuvent être représentés et édités dans des formes plus agréables que du code.
Voilà ce qu'on a abordé dans cet épisode. :)
• Comment étendre fxconv avec des conversions personnalisées en Python.
• Comment convertir un objet en une structure définie en C.
• L'utilisation de `fxconv.ObjectData()` pour générer des structures contenant des pointeurs.
• Et une petite discussion sur l'implémentation des éléments dynamiques de Mystère noir et blanc.