Triceraprog
La programmation depuis le Crétacé

  • La Maison dans la colline, partie 5 ()

    Dans ce cinquième article, je vais aborder la méthodologie que j'ai appliquée pour le développement de « La maison dans la colline ».

    La planification

    Dans un premier temps, j'avais jeté sur papier (électronique) la liste des fonctionnalités que je voulais implémenter, en partant de l'idée générale du jeu et en descendant successivement sur ce dont je pensais avoir besoin. Puis j'ai segmenté cette liste en thèmes, comme par exemple « mouvements du personnage » ou bien « gestion de l'inventaire ».

    Ces fonctionnalités ont besoin les unes des autres, je suis descendu jusqu'aux briques de bases, comme « afficher quelque chose à l'écran » ou bien « lire une touche du clavier ». Entre toutes ces fonctionnalités, j'ai créé des dépendances : afficher un personnage nécessaire de savoir afficher quelque chose à l'écran. L'animer nécessite de savoir l'afficher. Et ainsi de suite.

    Mes dépendances ne sont pas complètes. J'ai celles qui concernent les premières tâches à effectuer, mais c'est tout. Inutile de travailler sur celles qui viendront bien plus tard, et ce pour une raison très simple : il est très probable qu'elles changent au fur et à mesure que j'avance dans le développement. Voire pour certaines, qu'elles disparaissent.

    Liste terminée des fonctions du jeu.

    La qualité progressive

    Dernière étape, pour chaque fonctionnalités, séparer des niveaux de qualité. Par exemple, je sais que je vais un personnage avec un affichage fin, et animé. Mais je ne sais pas encore trop ce que ça donne d'un point de vue jeu, est-ce qu'il me faut un sprite de 8 pixels de large ou de 16 pixels de large ? J'ai même joué avec l'idée à un moment qu'il fasse 8 de large de côté, mais 16 de large de face.

    Je sais que dessiner un sprite va me prendre du temps, surtout que ça n'est pas mon domaine. Je ne veux pas avoir à le refaire trop de fois. Donc dans les premiers niveaux de qualité, je note que je vais afficher des caractères prédéfinis de l'ordinateur.

    Version de développement de MDLC

    Je note ensuite que je pourrais tenter avec un personnage fixe. Puis enfin un personnage animé. Cela me donne trois étapes de qualité pour cette fonctionnalité. Et celles-ci ne seront pas forcément exécutées consécutivement. C'est important car le temps de la jam est limité et je fais ça sur mon temps libre, qui est très variable. Si la fin arrive avant qu'un niveau de qualité soit atteint, ce n'est pas grave, j'aurais tout de même quelque chose de moins joli que prévu, mais quelque chose quand même.

    Je fais de même avec les fonctionnalités : je les classe par importance, mêlée de difficulté. Un personnage qui se déplace, c'est essentiel. Parmi les interactions avec les objets, j'en avais initialement prévus beaucoup plus que ce qui est dans le jeu à la fin. Quant à l'audio... je n'ai pas eu le temps et j'ai laissé de côté.

    De même j'avais prévu quelques scènes graphiques d'illustration en « haute résolution ». C'est passé à la trappe.

    Sur le long terme

    À chaque fois que je termine une fonctionnalité, je reviens sur le document et j'ajuste. En voyant le jeu progresser, je comprends qu'il y a des choses que je voulais faire qui ne vont pas avec le reste. Ou parfois, je vois qu'il me manque un morceau, que j'avais oublié quelque chose.

    Ce document va souvent être modifié. Je garde l'idée générale du jeu, les grandes lignes. Mais je me laisse aussi porter par ce qu'il devient. Ça m'évite de me bloquer sur quelque chose trop longtemps alors que ça ne fonctionne pas, que ce soit techniquement ou en game design.

    Il ne faut pas hésiter à couper par manque de temps, ou parce qu'on a vu trop gros pour la machine. À modifier parce que ça ne convient plus. Ou parce qu'une nouvelle idée arrive. Ce dernier cas est à évaluer avec prudence ceci dit : il faut l'intégrer correctement à l'existant, et à l'état actuel du projet. Les idées arrivent toujours plus nombreuses et plus rapidement que leur temps de développement nécessaire.

    La programmation

    Niveau programmation, j'applique là aussi une démarche itérative « par petits pas ». En reprenant l'affiche, par exemple, je commence par m'adresser directement à l'EF9345, à travers une liste d'affichage, avec une position et un caractère fixes. Puis j'extrais la position pour qu'elle devienne variable, mais toujours localement. Puis je la passe par paramètre de la liste d'affichage. Et enfin je l'injecte depuis le programme principal, avant de répéter la même démarche pour le caractère affiché.

    L'idée ici est de vérifier que le code fonctionne sur un cas simple, s'assurer qu'on a bien compris le problème. Dans le cas de l'EF9345, être certain qu'on a bien compris son fonctionnement par exemple. Puis petit à petit, on généralise, si nécessaire, avant d'extraire les données variables.

    Cela permet de valider chaque étape individuellement et d'éviter les bugs dus à un développement avec beaucoup de changements différents. Cela permet aussi de s'arrêter en chemin. Soit pour passer à autre chose que l'on développe en parallèle car en lien. Ou tout simplement parce que c'est l'heure de manger, et qu'il est toujours préférable de laisser le programme dans un état fonctionnel sur lequel on peut reprendre plus tard.

    Et enfin, parce que parfois, on s'aperçoit qu'il n'est pas nécessaire de généraliser plus avant. Ou alors pas tout de suite. Et peut-être jamais. La fonctionnalité arrive à un point satisfaisant.

    Conclusion

    S'il faut retenir une chose de cette méthodologie, c'est le concept d'avancée graduelle. Faire des « petits pas », monter petit à petit en qualité et savoir s'arrêter.


  • 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 !


  • Récréation 3D, Micral N ()

    Je l'avais brièvement mentionné il y a presque un an : j'ai eu la chance de pouvoir travailler avec l'association M05.COM à l'analyse et restauration d'un exemplaire de « Micral N ».

    Entre deux analyses, j'ai reproduis cet exemplaire en modèle 3D, que je présente ici dans un rendu assez simple.

    J'y voyais deux intérêts. Le premier est que c'est pour moi une façon d'étudier l'aspect extérieur d'une machine. Dans le cas de celle-ci, est-ce que j'ai bien vu tous les détails ? Est-ce que j'ai bien vu toutes les LEDs et tous les interrupteurs. L'exercice de modélisation force à se pencher sur les détails.

    En comparant les photos, on peut aussi voir les différences entre différents exemplaires. Par exemple, l'exemplaire de l'association a un interrupteur ajouté sur la droite du panneau de contrôle par rapport aux autres exemplaires dont les photos sont disponibles.

    Le deuxième intérêt est que cette modélisation pourra servir pour l'émulateur « grand public » (toute proportion gardée), afin de manipuler le clone virtuel de cette machine.

    Reproduction en image de synthèse de l'exemplaire Micral N de l'association MO5

    L'exemplaire original, lui, peut-être vu dans cet article de blog sur le site de l'association.


  • La Maison dans la colline, partie 3 ()

    Suite de la série sur le développement du jeu La maison dans la colline sur VG5000µ. Après, un exposé du contexte dans la première partie, puis un aperçu des outils utilisés dans la seconde partie, cet article aborde l'idée du jeu et de son évolution au fur et à mesure du développement.

    La première idée

    LE VG5000µ est une machine que je commence à bien connaître, et, comme expliqué dans le premier article, j'étais en train d'approfondir les capacités du VDP lorsque ce petit défi a commencé. Je voulais me servir des mes nouvelles connaissances et je suis parti sur l'idée de faire un affichage « fin » et sans clignotement.

    L'idée de faire un jeu d'aventure graphique me trottait dans la tête depuis quelques temps, et c'est un genre qui collait bien à mes objectifs. Peu de mouvements à l'écran, donc l'affichage pourrait être maîtrisé.

    Initialement, le jeu se passe dans un manoir, c'est-à-dire un lieu avec de nombreuses pièces. Et puis, c'est le classique des jeux graphiques ou textuels, repris aussi par des standards comme « Alone in the Dark » en jeu vidéo, ou « Mansions of Madness » en jeu de plateau. Les jeux où le but est de sortir d'une maison fermée ne manquent pas.

    Rapidement, je fais quelques croquis pour me fixer une idée de ce à quoi le jeu ressemblerait. Un peu de place pour l'affichage de la pièce courante vue de dessus, de la place pour un texte et un inventaire.

    Premières notes manuscrites du jeu.

    Creuser les idées

    Dans ce genre de jeux, il s'agit de résoudre des énigmes qui prennent la forme de trouver les bons objets pour les amener aux bons endroits. Il peut y avoir quelques énigmes plus complexes à résoudre, mais je veux m'arrêter là. Le temps va passer vite, et il faut rester sur du faisable, et sachant que ça sera déjà probablement trop.

    Je lance quelques idées d'objets. Une torche électrique. Une boite d'allumettes. Ah, donc autant remplacer la torche pas une lanterne à allumer. Des livres. Des clés. C'est vague, mais cela me permet de définir quels types d'interactions le personnage du jeu peut avoir. Ah, et des portes aussi.

    Au tout début, les interactions sont nombreuses : prendre, lire, utiliser, combiner, poser, ouvrir... Mais je sens que cela va faire beaucoup. Je voudrais aussi essayer que le jeu soit jouable avec la manette, à un seul bouton (ce ne sera pas le cas au final). Cette contrainte me fait réfléchir à réduire le nombre d'interactions, ou en tout cas de faire en sorte qu'elles soient automatiques : si je fais une action sur un livre, c'est pour le lire. Si je fais une action devant une porte en ayant une clé, c'est pour l'ouvrir. Et ainsi de suite. Mais pour le moment, je ne creuse pas plus avant. Je laisse cette idée mûrir.

    Je veux aussi me détacher des jeux d'époque dont une partie de la difficulté est de comprendre comment manipuler le jeu. De ce côté-ci, je veux apporter de la modernité (toute relative) : en jouant, les objets sur lesquels ont peut agir doivent être facilement identifiables, et les actions possibles indiquées en fonction du contexte. Ça va probablement raboter la durée du jeu, mais cela sera beaucoup plus agréable.

    Concernant la durée d'ailleurs, je ne vise pas quelque chose de très long. Les quelques personnes susceptibles de jouer à ce jeu ne cherchent probablement pas une expérience de douze heures. Ni même d'une heure. Trente minutes pour quelqu'un qui ne connaît pas la solution, ça semble bien. Même si je cherche à ce que le jeu soit agréable à jouer et un bon moment à passer, c'est avant tout une preuve de concept d'affichage sur VG5000µ. Et c'est un hobby à mes heures perdues.

    Ci-dessous, une maquette préliminaire du jeu avec des essais de rendu. On y voit un essaie de personnage plus large. Les portes sont différentes aussi.

    Maquette préliminaire du jeu.

    Évolutions

    Toutes ces idées, je les notes sur un carnet (électronique), et je n'hésite pas à les annoter, les modifier, les corriger, en fonction des développements, du temps qui passe, des choses plus faciles à programmer que d'autres, des simplifications,... Cette espèce de document de design est un document vivant, que j'adapte au fur et à mesure.

    Je commence à coller un scénario sur mes éléments de gameplay, afin d'expliquer les contraintes. Pourquoi est-ce que le personnage ne peut pas sortir de la maison une fois qu'il y est entrée. Que s'est-il passé dans les différentes pièces.

    En retour, le scénario influe sur le design. Comment est-ce que j'agence les pièces, quels objets vont dans quelles pièces, qu'est-ce que je fais faire aux gens qui jouent ?

    C'est allers et retours entre conception du jeu, de niveaux, du scénario et du programme nourrissent peu à peu ces différentes parties et les font avancer de concert.

    Le manoir initial se transforme en une maison à moitié troglodyte, car c'est un moyen assez « simple » d'avoir un environnement d'intérieur de maison et de grotte dans un espace assez restreint. C'est un peu tiré par les cheveux mais bon, on est dans du jeu vidéo rétro. Je me trouve même assez sobre par rapport à certaines productions.

    Petit à petit se construit aussi un double objectif afin de pouvoir terminer le jeu. Sortir de la maison, mais aussi pouvoir reprendre la voiture qui nous a amené sur les lieux.

    J'ajoute aussi un peu de « lore » avec quelques objets qui ne sont pas nécessaires, mais qui font transparaître une histoire assez simple, mais je pense suffisante pour l'ambition toute relative de ce jeu. J'ai donc écrit un petit background sur les évènements passés de cette maison. Qui sait ? Peut-être que je compléterai l'histoire grâce à d'autres jeux dans le futur.

    Léger

    La constante, c'est de rester léger. Comme je l'ai évoqué, c'est un petit défi de programmation hobbyiste. Il ne faut surtout pas hésiter à couper, et donc prévoir des choses de manières à ce qu'elles puissent facilement être enlevées en modifiées.

    Je suis assez satisfait du résultat, même si j'y vois plein de petites erreurs, ou maladresses, en y jouant.

    Diagramme montrant les étapes du jeu.


  • Mattel Aquarius, scan des touches en assembleur ()

    Le scan du clavier, sur Mattel Aquarius, à lieu dans la ROM à l'adresse $1e80. Cette fonction traite les minuscules, les majuscules mais aussi les raccourcis BASIC, en injectant au fur et à mesure les touches nécessaires comme si elles avaient été tapées au clavier.

    C'est beaucoup trop pour un scan de clavier dans un jeu, et peut même poser quelques soucis. Mais c'est une bonne base pour écrire une routine de lecture de clavier, car la lecture des valeurs des touches n'est pas forcément très simples.

    Ce que nous apprends la lecture de la routine en ROM est qu'il semble falloir attendre une stabilité dans les valeurs avant d'accepter la touche. En effet, la routine fait plusieurs lectures et ne considère la touche appuyée que si cette lecture est stable.

    Voici une version de la routine, où j'ai enlever ce qui était traitement de raccourcis BASIC, ainsi que le traitement des touches de modifications. Ainsi, la traduction ne se fait qu'avec les touches minuscules.

    Attention, je n'ai pas encore testé cette routine sur du matériel réel !

    La routine

    Dans cette version, le décodage de la touche est faite par rapport aux minuscules. En changeant vers la fin la valeur de HL de lower_key_table-1 vers upper_key_table-1, il est possible d'obtenir un décodage avec des majuscules.

    Il est aussi possible de ne traiter que des codes de touches, sans traductions. Dans ce cas là, il suffit de remplacer les trois instructions après accept_key par un simple LD A, E.

    Les deux emplacements en RAM, qui commencent ce code source, doivent être en RAM et être adaptés à votre programme.

    latest_keycode_pressed: EQU     0380eh
    keyword_delay_value:    EQU     0380fh
    
    lower_key_table:        EQU     01f38h
    upper_key_table:        EQU     01f66h
    
    check_key:
        EXX
    
        LD         BC,0xff
        IN         A,(C)
        CPL
        AND        0x3f
        LD         HL,latest_keycode_pressed
        JR         Z,no_key_scanned            ; ?No row were selected, quick skip?
        LD         B,0x7f                      ; Input row 7
        IN         A,(C)
        CPL
        AND        0xf                         ; Only 4 keys in lower bits
        JR         NZ,decode_key_scan
        LD         B,0xbf                      ; Input rows 6 to 0
    
    loop_input_rows:
        IN         A,(C)
        CPL
        AND        0x3f                        ; 5 keys in lower bits
        JR         NZ,decode_key_scan
        RRC        B
        JR         C,loop_input_rows
    
    no_key_scanned:
        INC        HL
        LD         A,0x46
        CP         (HL)                        ; Compare with delay
        JR         C,return_no_key_value
        JR         Z,reset_latest_key
        INC        (HL)                        ; Increment delay
    
    return_no_key_value:
        XOR        A
        EXX
        RET
    
    reset_latest_key:
        INC        (HL)
        DEC        HL
        LD         (HL),0x0
        JR         return_no_key_value
    
    decode_key_scan:
        LD         DE,0x0
    
    count_row:
        INC        E
        RRA
        JR         NC,count_row
        LD         A,E                         ; E is the active row
    
    count_add_column:
        RR         B
        JR         NC,keycode_found
        ADD        A,0x6                       ; Add 6 for each column
        JR         count_add_column
    
    keycode_found:
        LD         E,A
        CP         (HL)                        ; HL = 0380eh
        LD         (HL),A
        INC        HL
        JR         NZ,new_key_delay            ; That's a new key, we need a bit of selay
        LD         A,0x4
        CP         (HL)
        JR         C,commit_key_delay_continued
        JR         Z,accept_key                ; Wait if over, accept key!
        INC        (HL)                        ; Increment delay from 4 to 6 iteratively
        JR         return_no_key_value
    
    commit_key_delay_continued:
        LD         (HL),0x6                    ; Set the delay for the second pass
        JR         return_no_key_value
    
    new_key_delay:
        LD         (HL),0x0
        JR         return_no_key_value
    
    accept_key:
        LD         IX,lower_key_table-1
        ADD        IX,DE
        LD         A,(IX+0x0)                  ; Read decoded Key value
    
    return_key_value
        EXX
        RET
    

Page 1 / 19 (suivant) »