Site logo

Triceraprog
La programmation depuis le Crétacé

La Maison dans la colline, partie 4 ()

Dans ce quatrième article concernant le développement de « La maison dans la colline », je vais aborder quelques points de programmation. Deux points en particulier : la structure générale du programme, puis les listes d'affichage.

Structure générale

Un jeu vidéo, c'est un programme qui ne s'arrête pas. Enfin si... quand on a fini de jouer. Mais il s'oppose aux programmes en « batch » qui doivent résoudre une fonction à partir de données en entrée. Un jeu vidéo se situe donc dans la classe des applications qui font évoluer un état en fonction des entrées de l'utilisateur.

Ainsi, un tel programme peut se résumer à cette structure :

int main()
{
    while(running())
    {
        read_input();
        update_state();
        display_state();
    }
}

Autrement dit : tant que le logiciel tourne, on lit les entrées, on met à jour les états du jeu, on affiche l'état du jeu (on peut aussi diffuser le son, mais ce jeu n'en n'a pas) et on recommence.

Voilà la base.

Au deuxième étage, le jeu est constitué de « pages ». Il y a la page d'accueil, avec le titre, la page d'introduction, qui donne le texte de début, le jeu en lui-même, et le texte de fin. Comme le jeu est à tout moment dans une seule de ces pages, j'utilise pour les représenter une paire de fonctions. L'une est appelée lorsque l'on entre dans la page, et l'autre à chaque mise à jour, en boucle, tant que cette page est active.

Fonction d'entrée

La fonction d'entrée permet de changer le contexte du jeu, de mettre l'écran dans les bonnes conditions. Par exemple, voici la fonction d'entrée de la page de titre :

void page_title_enter(PageContext* context) __z88dk_fastcall
{
    initialize_40_long();       // Initialisation du mode 40 colonne format long de l'EF9345
    clear_40_long();            // Effacement de l'écran dans ce mode

    // Affichage du titre
    set_print_attributes(M40LONG_COLOR_FG_WHITE, M40LONG_DOUBLE_HEIGHT);
    const char* title = extract_string_from_id(TEXT(0));
    print_at(2, 12, title);
    print_at(2, 13, title);

    // Affichage de l'auteur
    set_print_attributes(M40LONG_COLOR_FG_WHITE, 0);
    const char* author = extract_string_from_id(TEXT(71));
    print_at(20, 17, author);

    // Affichage du PRESS START
    set_print_attributes(M40LONG_COLOR_FG_WHITE | M40LONG_FLASH, 0);

    const char* press = extract_string_from_id(TEXT(1));
    print_at(2, 22 + 8, press);
}

Quelques commentaires :

  • le paramètre context n'est pas utilisé ici. Nous verrons dans la fonction suivante son utilité. À vrai dire, je ne l'ai jamais utilisé dans les fonctions d'entrée et il devrait probablement être enlevé.
  • __z88dk_fastcall est une annotation pour la suite z88dk qui indique au compilateur que le pointeur passé en argument devra être passé dans HL, plutôt que par la pile. Nous verrons plus tard que c'est bien pratique lorsque l'on mélange C et assembleur.
  • les chaînes de caractères sont appelées via un système d'accès à des ressources. J'en parlerai probablement dans un autre article plus tard.

Fonction de mise à jour

L'autre fonction qui décrit une page est celle de la mise à jour. Elle sera appelée en boucle tant que la page est active. Contrairement à ce que j'indiquais au tout début de l'article, il n'y a pas de séparation entre la mise à jour et l'affichage. Dans ce programme, la fonction de mise à jour s'occupe des deux étapes.

Voici par exemple la fonction de mise à jour pour l'écran de titre :

void page_title_update(PageContext* context) __z88dk_fastcall
{
    if (context->just_pressed_key != 0)
    {
        context->command = CHANGE;
        context->command_param = INTRODUCTION_PAGE;
    }
}

Commentaires :

  • ici, le context est utilisé. On peut voir qu'il sert de communication avec l'état extérieur à la page.
  • en lecture, le context fourni une information sur une touche du clavier éventuellement appuyée.
  • en écriture, le context permet de donner une information à transmettre à l'extérieur. Ici, l'information est celle d'un changement de page.

En effet, au-dessus des pages, il y a un petit superviseur, qui n'est en fait rien d'autre que la boucle principale du jeu. Celle-ci va s'occuper de peupler le context avec les informations systèmes, dont les touches du clavier, puis va vérifier si la page en cours a donné une commande. Il y a trois commandes possibles :

  • CHANGE : qui indique une page de destination, et qui provoquera donc un changement de page. Ici, lorsqu'une touche est appuyée, on passe à la page d'introduction du jeu.
  • RUN : qui indique que la page actuelle doit continuer à être appelée, c'est la page active.
  • STOP : qui demande l'arrêt du programme. C'est une commande que j'ai utilisée en début de développement pour certains tests, mais que j'ai arrêté d'utiliser. Le programme ne s'arrête pas.

Voilà pour la structure générale du jeu. Passons maintenant aux listes d'affichage.

Les listes d'affichage

Comme je l'avais mentionné dans un article précédent, je voulais dans ce jeu avoir un affichage rapide avec lequel le scintillement de mise à jour était, au moins dans la majorité des cas, invisible. Et cela signifie une chose : il faut afficher vite, et au bon moment.

Si la mise à jour de l'écran se fait au fil de la mise à jour de l'état du jeu, il y a de bonnes chances qu'une mise à jour de l'affichage arrive au mauvais moment. De plus, l'EF9345 est pleinement efficace en début de traçage de l'écran, dans la zone de bord haute. Par la suite, il commence à être occupé avec la génération de l'image et a moins de temps à consacrer aux données qui arrivent depuis le Z80.

Il y a deux outils classiques pour cela :

  • la synchronisation verticale, qui permet de savoir quand le processeur vidéo commence une nouvelle image,
  • les listes d'affichage (Display Lists en anglais), qui sont des listes de commandes préparées pour être exécutées le plus vite possible. Ou en tout cas, « assez vite »

Mise en place

Par bonheur, le VG5000µ est conçu de manière à être mis au courant d'une synchronisation verticale. En effet, le signal de synchronisation, envoyé par l'EF9345, est branché sur l'interruption (IRQ) du Z80. Comme je l'avais abordé dans l'article sur les hooks de la ROM du VG5000µ, le système appel une emplacement en RAM avant d'effectuer son affichage. Il est possible remplacer l'instruction RET initialement positionné à cette adresse par un saut (JMP xxxx) vers l'adresse que l'on veut.

Et c'est une des premières opérations que fait le programme : une mise en place d'un routage vers l'exécution des listes d'affichage en cours. La routine dépile aussi l'adresse de retour sur la pile, afin de désactiver complètement l'affichage géré par la ROM.

On est donc en contrôle complet de l'affichage du VG5000µ. Une lourde responsabilité !

Gestion de listes

Il y a de nombreuses manières différentes d'implémenter des listes d'affichage, en fonction des besoins en vitesse balancés avec la place prise et probablement d'autres paramètres encore.

Dans ce programme, les listes d'affichage sont formées par une suite d'appels, au sens machine (CALL) vers de fonctions spécialisées. Ce sont des listes chaînées qui ont le format suivant :

  • l'adresse du lien vers l'élément suivant, ou zéro pour la fin de liste,
  • l'adresse d'appel,
  • les paramètres de l'appel.

Ajouter un élément à la liste d'affichage est donc ajouter à la liste une nouvelle adresse d'appel ainsi que des arguments, puis ajuste le lien de la chaîne.

Rejouer une liste consiste à parcourir la liste et appeler les fonctions spécialisées en fournissant les paramètres.

Tout cela est stocké dans l'espace de la RAM que la ROM utilise normalement pour traiter l'affichage. Il y a là plein de place inutilisée.

J'ai considéré un temps utiliser aussi l'espace des variables internes du BASIC, mais z88dk en utilise une partie, par exemple pour la lecture des touches, que je n'ai pas réécrite. Il est cependant possible d'utiliser cette partie avec un peu plus d'efforts.

Un exemple

Dans la fonction d'affichage de l'écran titre plus haut, il est cette ligne :

    set_print_attributes(M40LONG_COLOR_FG_WHITE, M40LONG_DOUBLE_HEIGHT);

Cette fonction set_print_attributes n'agit pas directement sur l'EF9345. Si on regarde son implémentation, on peut voir qu'elle se contente d'ajouter à la liste d'affichage un appel différé vers la routine spécialisée :

void set_print_attributes(unsigned char a, unsigned char b)
{
    params[0] = a;
    params[1] = b;
    add_to_display_list(dl_set_attributes, 2, (void*) params);
}

Les trois paramètres indiquent :

  • l'adresse de la routine spécialisée,
  • la taille des paramètres, en octets,
  • les paramètres, sous forme d'un buffer qui sera copié dans la liste d'affichage.

Au prochain traçage de l'écran, la routine sera alors appelée. Dans ce cas précis, c'est une routine en assembleur.

    ; Sets the A and B attributes to used for KRF
    ; Parameters are A then B
_dl_set_attributes:
    ef9345_addr EF9345_SEL|REG_R3_CMDST ; Loads A
    ld          a,(hl)
    ef9345_data a
    call        wait_impl
    ef9345_addr EF9345_SEL|REG_R2_CMDST ; Loads B
    inc         hl
    ld          a,(hl)
    ef9345_data a
    call        wait_impl
    ret

Mais il est possible d'appeler une fonction C annotée avec l'attribut __z88dk_fastcall vu plus haut. Ainsi, la fonction C recevra le buffer de paramètre naturellement. Il n'existe qu'une seule fonction dans le programme qui utilise cette méthode, car j'ai progressivement réécrit les autres en assembleur. C'était cependant bien pratique pendant le développement.

Cette fonction s'occupe de l'affichage des pièces, et commence comme ceci :

void dl_display_room(const RoomNew** p_room_param) __z88dk_fastcall
{

Conclusion

Le système de page est simple et efficace dans le cadre de ce jeu. Et le système de liste d'affichage a bien joué son rôle : l'affichage du jeu est rapide et agréable. Il a fallu quelques ajustements sur matériel réel sur quelques synchronisation, car j'envoyais parfois les commandes trop vite, ce qui est accepté par les émulateurs, mais pas par un vrai VG5000µ.

Dans le prochain article, je compte parler de la méthodologie de développement que j'ai utilisé sur le jeu. À la prochaine !