mystnb/tutorials/03-des-portes-beaucoup-de-p...

415 lines
33 KiB
Plaintext

[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 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] ;
• 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 !
[center][img]https://gitea.planet-casio.com/Lephenixnoir/mystnb/raw/branch/master/tutorials/gint-tuto-03-level-1.gif[/img]
[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 : 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 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]##### #####
# ##### #
# b 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 (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 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
{
/* Width and height */
int w, h;
/* Whether fog is enabled */
int fog;
/* Door cycle string */
char door_cycle[128];
/* Mapping of door types to cycle string index */
uint8_t door_cycle_index[16];
/* Background image */
bopti_image_t *img;
/* Array of tiles in row-major order */
uint8_t *tiles;
};[/code]
Je stocke les cycles à la suite les uns des autres dans une seule chaîne `door_cycle`, et je référence chaque portion dans le tableau `door_cycle_index` (il y a un exemple plus bas). Le plus important ici reste la map, et pour la map on a besoin de donner une valeur à chaque case :
[code]/* enum map_tile: Single cell in the map */
enum map_tile
{
TILE_AIR = 0,
TILE_WALL = 1,
TILE_START = 2,
TILE_END = 3,
/* 8 keys in interval 16..23 */
TILE_KEY = 16,
/* 8 vertical doors in interval 24..31 */
TILE_VDOOR = 24,
/* 8 horizontal doors in interval 32..39 */
TILE_HDOOR = 32,
};[/code]
Je numérote l'air, les murs, les points de départ et d'arrivée de 0 à 3. Les clés et les portes sont pas toutes identiques donc je me donne 8 numéros de clés et 8 numéros de chaque direction de porte pour avoir de la variété.
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) 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]
On ne dirait pas de loin, mais fxconv n'est pas juste un outil en ligne de commande. En fait, il est programmable. On peut ajouter dans chaque projet des conversions personnalisées programmées en Python. ;)
D'abord, on va enregistrer la map dans un autre sous-dossier de `assets-fx` pour ne pas trop mélanger nos assets. Prenons par exemple `assets-fx/map/lv1.txt`. Pour les métadonnées, on va utiliser `custom-type` au lieu de `type` pour indiquer à fxconv qu'on utilise une conversion personnalisée. Comme j'ai mis un sous-dossier, je crée bien un nouveau `fxconv-metadata.txt` dans `assets-fx/map`.
[code]*.txt:
custom-type: map
name_regex: (.*)\.txt map_\1[/code]
Le type est donc un type personnalisé "`map`" ; la regex est la même que dans le tutoriel précédent, mais avec l'extension `.txt` pour les noms de fichiers et le préfixe `map_` pour les noms de variables.
Maintenant, on peut coder le convertisseur. Je vais le mettre dans `assets-fx/converters.py` ; vous pouvez le mettre où vous voulez. Voilà à quoi doit ressembler le fichier :
[code]import fxconv
def convert(input, output, params, target):
if params["custom-type"] == "map":
convert_map(input, output, params, target)
return 0
else:
return 1
def convert_map(input, output, params, target):
# Generate the data here...
data = b"<Placeholder>"
fxconv.elf(data, output, "_" + params["name"], **target)[/code]
Il y a plusieurs choses à noter.
• 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` (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).
• À la fin de la génération, la fonction `fxconv.elf()` est utilisée pour générer le fichier de sortie. Sauf si vous faites des trucs très extravagants, les trois derniers paramètres ne changeront jamais, et vous n'aurez besoin de `output` et `target` que là. Avec ces simplifications en tête, finalement le procédé de conversion prend en entrée `input` (fichier d'entrée) et `params` (infos de `fxconv-metadata.txt)` et doit générer `data`. ;)
Ici vous pouvez voir que les octets que je génère sont juste ceux du texte `"<Placeholder>"` encodé en ASCII. Ça n'a rien à voir avec une `struct map`, c'était juste pour garder le code court pendant l'explication. Maintenant au passe à la conversion à proprement parler ! ^^
[code]def convert_map(input, output, params, target):
TILE_AIR = 0
TILE_WALL = 1
TILE_START = 2
TILE_END = 3
TILE_KEY = 16
TILE_VDOOR = 24
TILE_HDOOR = 32
# Read input file
with open(input, "r") as fp:
tiles, cycles = fp.read().split("\n\n")
tiles = tiles.split("\n")
cycle_texts = [c for c in cycles.split("\n") if c]
w = max(len(t) for t in tiles)
h = len(tiles)
fog = 0
filename = os.path.splitext(os.path.basename(input))[0]
# Generate map contents
encoded_tiles = bytearray(w * h)
for (y, t) in enumerate(tiles):
for (x, c) in enumerate(t):
if c == " ":
tile = TILE_AIR
elif c == "#":
tile = TILE_WALL
elif c == "~":
tile = TILE_START
elif c == "@":
tile = TILE_END
elif ord("0") <= ord(c) <= ord("9"):
tile = TILE_KEY + int(c)
elif ord("a") <= ord(c) <= ord("z"):
tile = TILE_VDOOR + (ord(c) - ord("a"))
elif ord("A") <= ord(c) <= ord("Z"):
tile = TILE_HDOOR + (ord(c) - ord("A"))
else:
raise fxconv.FxconvError(f"unknown tile character {c}")
encoded_tiles[y*w + x] = tile
# Parse door cycles
RE_CYCLE = re.compile(r'^([a-zA-Z]):\s*([#.]+)$')
cycles = dict()
for c in cycle_texts:
m = re.match(RE_CYCLE, c)
if not m:
raise fxconv.FxconvError(f"cannot parse door cycle '{c}'")
cycles[m[1]] = m[2].encode("utf-8")
# Generate door cycle data
door_cycle = b""
door_cycle_index = bytearray(16)
for index, letter in enumerate("abcdefghABCDEFGH"):
door_cycle_index[index] = len(door_cycle)
if letter in cycles:
door_cycle += cycles[letter] + b" "
door_cycle = door_cycle + bytes(128 - len(door_cycle))
# Generate the structure
o = fxconv.ObjectData()
o += fxconv.u32(w) + fxconv.u32(h) + fxconv.u32(fog)
o += door_cycle + door_cycle_index
o += fxconv.ref(f"img_{filename}")
o += fxconv.ref(encoded_tiles)
fxconv.elf(o, output, "_" + params["name"], **target)[/code]
C'est un peu long, mais il y a de tout dans cette conversion donc c'est une bonne référence. On va donc prendre le temps de tout bien détailler. :)
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 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...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`, 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 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, 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. ;)
[code]o = fxconv.ObjectData()
o += fxconv.u32(w) + fxconv.u32(h) + fxconv.u32(fog)
o += door_cycle + door_cycle_index
o += fxconv.ref(f"img_{filename}")
o += fxconv.ref(encoded_tiles)
fxconv.elf(o, output, "_" + params["name"], **target)[/code]
La génération du fichier final c'est la toute dernière ligne qui appelle `fxconv.elf()`. Les trois derniers paramètres sont toujours les mêmes, le seul qui importe vraiment c'est le premier, qui représente les données à mettre dans le fichier (c'est-à-dire les octets de la `struct map` qu'on est en train de convertir).
Si tous les octets de la structure sont connus au moment de la conversion, vous pouvez passer un objet `bytes` à `fxconv.elf()`, comme ceci :
[code]# N'importe quoi de type bytes
data = b"..." + bytes([0x00, 0x10, 0x42, ...]) + ...
# On génère exactement ces octets-là
fxconv.elf(data, output, "_" + params["name"], **target)[/code]
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 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]
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 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`. 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.
[code]o += fxconv.ref(encoded_tiles)[/code]
La commande complète est `ref(<bytes_like> [,padding=<padding>])`. Si vous spécifiez `padding` alors `ref()` rajoutera quelques octets nuls à la fin pour s'assurer que la taille du résultat est un multiple de `padding`. C'est utile pour préserver l'alignement !
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.
La raison de tout ça c'est que les pointeurs ce sont des adresses dans la mémoire (voir [url=https://www.planet-casio.com/Fr/forums/topic16588-1-tdm-19-apprehender-la-memoire-pour-eclairer-le-bas-niveau.html#adresses]cette section du TDM 19[/url] pour les détails), et donc pour connaître leur valeur il faut savoir où seront chargées les données pointées (l'image de fond et le tableau qui accompagne la `struct map`) dans la mémoire.
Sauf que cette information n'existe pas du tout à ce stade ! Choisir des adresses de chargement est le rôle de l'éditeur de liens, qui n'intervient que plus tard dans le processus (qui est détaillé dans le [url=https://www.planet-casio.com/Fr/forums/topic15930-1-tdm-n016-grands-principes-de-compilation.html]TDM 16[/url]). Il est impossible de connaître les adresses durant la conversion et c'est parfaitement normal.
En fait, le même problème se pose lorsque vous écrivez du code C avec des variables globales de ce genre :
[code]char x = 2;
char *px = &x;
char *px8 = &x + 8;[/code]
Lorsque ce fichier est compilé, le compilateur génère les octets qui serviront à initialiser ces trois variables. Pour `x` c'est facile, il y a un seul octet qui est 02. Mais pour `px` et `px8` le compilateur ne peut pas déterminer la valeur initiale, puisqu'elle dépend de `&x` et que c'est l'éditeur de liens qui fixe `&x` bien plus tard.
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 à 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écanisme.
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]
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)
{
dclear(C_WHITE);
dimage(0, 0, game->map->img);
/* ... */
}[/code]
Dans `map_walkable()`, on peut retirer le test qui hardcodait les bords de l'écran comme murs et lire les données qu'on vient de générer dans la map.
[code]/* Check whether a cell of the map is walkable */
static int map_walkable(struct map const *map, int x, int y)
{
int tile = map->tiles[y * map->w + x];
return (tile != TILE_WALL);
}[/code]
Pour l'instant j'ignore les portes, on verra ça après !
Et enfin, on peut arrêter d'utiliser un map factice dans `main()` et véritablement charger la map du niveau 1.
[code]extern struct map map_lv1;
struct game game = {
.map = &map_lv1,
.players = { &singleplayer, NULL },
.time = 0,
};[/code]
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 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]
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 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.
[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 :
[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`).
À 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 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 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.