Site logo

Triceraprog
La programmation depuis le Crétacé

La Maison dans la colline, partie 6 ()

Dans ce sixième article de la série sur le jeu « La maison dans la colline », il va être question des structures du jeu, de portage et de « binarisation ».

Les structures

« La maison dans la colline » est un jeu programmé en grande partie en C. L'idée derrière est de pouvoir porter assez facilement sur une autre machine qui n'aurait potentiellement pas le même processeur, c'est aussi une manière de faciliter les itérations. Le jeu manipulant des objets, des pièces pour circuler, un personnage, il est intéressant de pouvoir se reposer sur des structures de données et de les manipuler, de les faire évoluer, sans avoir à adapter un code assembleur en parallèle (même s'il existe des assembleurs qui peuvent faciliter ces opérations).

Les pièces

La première structure que je présente est celle des pièces de la maison et des portes qui les relient. Ces données sont fixes et pourraient se situer en ROM si je jeu était sur ROM.

typedef struct Door {
    unsigned char position[2];
    unsigned char destination_room;
    unsigned char destination_position[2];
} Door;

Une porte a une position dans la pièce et amène vers une pièce de destination (identifiée par un octet) à une position donnée dans cette pièce de destination.

typedef struct Room {
    unsigned char id;
    unsigned short shift_to_next;
    unsigned short shift_to_doors;
    unsigned char position[2];
    unsigned char size[2];
    unsigned char enter_text;
    unsigned char door_count;
    Door* doors;
    unsigned char data[];
} Room;

Une pièce est un peu plus complexe. Elle est identifiée par un octet (id) qui est suivi par deux nombres de 16 bits qui sont en fait des déplacements en mémoire. shift_to_next est un offset de chaînage vers la pièce suivante. Toutes les données des pièces sont contiguës en mémoire formant une liste chaînée unidirectionnelle. Ainsi, avec un pointeur vers une structure Room, si on avance de shift_to_next octets, on arrivera sur la pièce suivante dans les données.

shift_to_doors est un peu plus complexe. C'est une indication qui permet de construire le pointeur doors un peu plus loin dans la structure.

Comme on peut le voir, la structure Room se termine par un tableau de taille non spécifiée d'octets. Dans ce tableau se trouvent les données graphiques de la pièce suivies par les données des portes présentes dans la pièce. Ces deux données sont de taille variable et s'il est facile de connaître l'emplacement des données graphiques (c'est data), il est plus compliqué de connaître le début des portes qui suivent. Surtout que les données graphiques sont compressées.

Il y aurait plusieurs autres manières de faire. J'aurais pu mettre les portes (dont les données ne sont pas compressées) en premier et calculer le déplacement à partir du nombre de portes qui est une donnée connue. Mais les portes ont connu différentes implémentations et se sont finalement retrouvées là. Puis la fin du projet est arrivée et elles y sont restées.

La position et la taille (size) de la pièce indiquent la façon dont elle doit-être affichée à l'écran. enter_text est un identifiant vers le texte qui apparaît à l'écran en entrant. Et door_count comme son nom l'indique, précise le nombre de portes présentes dans la pièce.

Les données graphiques d'une pièce sont compressées selon un schéma RLE. Lorsqu'on entre dans un pièce ces données sont décompressées dans une zone temporaire et envoyées à l'affichage.

Les objets

La structure qui décrit les objets est la suivante. Là encore, ce sont des données fixes.

typedef struct Object {
    unsigned char name_id;       // the resource id for the text in the inventory. Used also to designate the object.
    unsigned char char_mode;     // what mode for the display
    unsigned char character;     // what char to display
    unsigned char properties;    // object properties
    unsigned char room_id;       // initial room
    unsigned char position[2];   // initial position in the room
    unsigned char action_text_id;// text id when the action is done on the object
} Object;

Magnifique, le code est commenté.

Un objet est donc identifié par un identifiant name_id qui est aussi l'identifiant du texte qui y est associé. C'est un choix que j'ai regretté, il aurait été bien plus pratique d'avoir un identifiant pour l'objet lui-même séparé du texte qui le décrit. Plus loin, on voit un autre identifiant du texte écrit lorsque l'on effectue une action. Là encore, c'est assez peu flexible et cela m'a obligé à ne considérer qu'une seule action par objet. Je m'en suis sorti et on dit que les contraintes amènent de la créativité.

char_mode et character donnent les informations d'affichage. Il n'y a pas de couleur car les objets ont une couleur fixe dans ce jeu, pour indiquer que des actions peuvent être faites dessus.

properties indique ce que l'on peut faire de l'objet : est-ce qu'on peut le prendre, est-ce qu'on peut le lire, est-ce que c'est un déclencheur d'évènement, est-ce que l'on peut marcher dessus, est-ce qu'il est transformable en un autre objet et enfin, est-ce que c'est un téléporteur.

Les téléporteurs sont en fait les portes. Initialement, j'avais un système spécifique de traitement des portes. Je l'ai plus tard unifié avec le traitement des objets de manière générale.

L'objet a aussi un pièce (room_id) et un emplacement (position) qui désignent l'endroit où se trouve l'objet en début de jeu. Cette information est immuable et servira lorsque l'on relance le jeu à tout remettre en place. Au début du jeu, un tableau des localisations réelles des objets est créé en mémoire et ce tableau qui sera modifié en fonction des actions.

Il existe deux pièces spéciales dans le jeu. Un pièce « nulle part » dans laquelle sont déplacés les objets qui ne sont plus valides (par exemple, une clé après avoir été utilisée). La seconde pièce est « l'inventaire ». Cela permet de s'assurer qu'un objet est toujours dans une pièce. Prendre un objet, c'est changer sa pièce courante pour celle de l'inventaire. En échangeant ses informations avec l'objet qu'il remplace dans l'inventaire, ce dernier est naturellement posé dans la pièce.

La binarisation

Partie logique

Si au début du développement il est possible d'indiquer directement dans le code les pièces (non compressées) et les objets, ça se révèle rapidement impraticable. Pour mettre au point le jeu, un éditeur est plus pratique. Cependant je n'avais non beaucoup de temps à consacrer au développement d'un éditeur de jeu.

Dans ces cas là, une manière classique de faire est de travailler sur des fichiers texte que l'on transpose dans le format binaire attendu par le jeu. D'où le terme « binarisation ». Un autre terme existe : « cooking »... et probablement d'autres.

Voici à quoi ressemble la première pièce du jeu :

Room ; Entrée
Id: 1
Position: 4,8
Size: 9,15
EnterText: 42

Description
#########
####D####
#       #
#       #
##      #
#       #
#       #
E       #
E       #
##     ##
##     ##
#       #
#   i   #
####F####
#########

Doors
D:3|E^
E:2|E<
F:255|A>

Objects
i:G'10,68,None,9 ; Apparition

Locks
F:18,56

EndRoom

Tous les # sont des emplacements bloquants : des murs ou des objets de décors. Les autres caractères (souvent des lettres) sont des emplacements spéciaux décrits à la suite de la partie graphique. Ainsi, on voit trois portes, un objet et un verrou.

Les portes sont suivies d'un petit code qui indique la pièce d'arrivée et un emplacement sous la forme d'une lettre (que l'on trouvera dans cette pièce) ainsi qu'une direction naturelle pour le sprite du personnage.

Les objets sont suivis d'informations graphiques (G'10,68 signifie : caractère numéro 68 dans la palette G'10), des propriété et d'un identifiant de texte. Ce qui suit le point virgule est un commentaire, il n'est pas lu.

Toutes ces données sont traitées et envoyées dans un fichier de données qui sera inclus au jeu.

Partie graphique

La parte graphique est elle aussi binarisée. Pour cela, j'utilise Pixelorama avec une palette d'objets graphiques et je dessine la pièce. La binarisation s'occupe de découper cela en morceaux de 10 pixels par 8 afin de construire la liste des caractères à redéfinir.

C'est à moi de m'assurer que les données logiques et graphiques sont cohérentes. Entre autre que les tailles de pièces soient identiques. Il y aurait de la marge pour aller plus loin avec un éditeur mais encore une fois, c'était dans un délai trop court pour cela. Peut-être plus tard ?

La suite ?

En effet, l'idée que j'avais en essayant de construire des structures réutilisables et flexibles étaient de pouvoir les... réutiliser. Et pourquoi pas étendre le jeu ou bien en faire un autre sur le même principe ? Avec cette fois un peu plus de temps à passer sur les outils.

Pourquoi pas. C'est une idée que je garde dans un coin de la tête.