|
|||||||||||||||||||||||||||||||||||||||||||
|
La perfection L'objectif du programmeur de jeux est de tirer le meilleur parti de l'ordinateur en écrivant le code le plus rapide, afin d'obtenir les meilleurs résultats possibles. Naturellement, le code doit faire ce que nous voulons, mais, et cela va peut-être vous surprendre, nous n'y parvenons généralement pas du premier coup. J'ai l'habitude de dire que le développement d'un programme est un processus qui consiste à se tromper à plusieurs reprises jusqu'au moment où on y parvienne. Nous passons donc 99,999% de notre temps à nous tromper et à chercher pourquoi. L'ordinateur exige une précision de 100% et punit impitoyablement les erreurs. La punition peut prendre différentes formes. L'ordinateur peut simplement dire "non" si la syntaxe du langage est incorrecte ou si le nom d'une variable est mal orthographié, ce qu'on appelle une erreur de compilation. Si nous parvenons à compiler correctement le code source et à exécuter le programme, il se peut qu'il mette un graphisme deux pixels trop à gauche ou qu'il s'autodétruise. Nous devons apprendre à repérer les erreurs, et nous ne manquons jamais de nouvelles façons de nous tromper. Semi-compilé Les langages informatiques sont comme le lait, ils peuvent être entièrement compilés, assemblés, semi-compilés ou interprétés. Les langages comme le BASIC peuvent être vérifiés au niveau de la syntaxe au fur et à mesure que vous les saisissez, et ils sont compilés et exécutés au fur et à mesure que le programme s'exécute. Cela permet aux programmes de fonctionner sur différentes plates-formes, mais n'est pas particulièrement rapide. Assemblé L'assembleur est très proche du code machine natif de l'ordinateur, donc complètement spécifique à la plate-forme, aussi rapide que possible, mais de nos jours, les processeurs étant de plus en plus sophistiqués, ce n'est plus vraiment pratique. Tous mes jeux 8 et 16 bits ont été écrits en assembleur. J'ai écrit une routine d'affichage de films pour PC en 8086, mais ce n'était pas amusant, autant expliquer pourquoi. Le processeur lit le code, détermine quelles sont les instructions, puis les exécute. Il met en place un pipeline d'instructions et peut travailler sur deux instructions consécutives, voire plus. Le problème est que certaines instructions prennent plus de temps que d'autres pour être décodées et exécutées, et une instruction courte et rapide suivant une instruction longue et lente peut être exécutée en premier. Ce n'est pas très malin si la seconde instruction dépend de la première. Nous devons écrire le code de manière à éviter de telles dépendances consécutives. Nous pouvons simplement placer des instructions sans opération (NOP) entre toutes les autres, si nous en avons le temps, ce qui n'était pas le cas à l'époque. Binaire multiarchitecture ("full-fat") L'autre type de langage que nous utilisons est un langage entièrement compilé, tel que le C ou le C++. COBOL était un autre langage de ce type. Cela signifie que nous écrivons notre programme, puis que nous lançons un compilateur et un éditeur de liens pour transformer le programme en code machine, ou plus probablement pour signaler un tas d'erreurs de syntaxe. De nos jours, l'éditeur peut également effectuer un contrôle syntaxique au fur et à mesure, ce qui permet de réduire les erreurs de frappe et les noms erronés. À l'époque, nous devions exécuter le compilateur sur nos programmes COBOL et attendre que l'impression des erreurs de compilation arrive sur un chariot toutes les heures. Nous reviendrons plus tard sur ce chariot. Il existe de nombreux autres types de langages, y compris les langages à balises basés pour Internet qui sont généralement envoyés sur votre ordinateur, puis exécutés sur votre ordinateur pour afficher des pages Web. De nombreux langages sont en écriture seule, c'est-à-dire qu'une fois que vous avez écrit le programme, vous ne pouvez pas dire ce qu'il fait ni comment il le fait. L'époque du COBOL Mon histoire en matière de programmation remonte à 1979, lorsque je travaillais sur un ordinateur central. Cet ordinateur occupait une pièce entière, les lecteurs de disquettes étaient aussi gros que des machines à laver et les données étaient stockées sur des bandes massives ainsi que sur des disquettes. L'ordinateur disposait de 8 Mo de mémoire vive. Les bases de données relationnelles étaient encore en cours de conception et nous devions partager six terminaux pour une équipe de quarante personnes. Il n'y avait pas d'ordinateur sur mon bureau, juste un crayon et un bloc-note de programmation. La plupart du temps, nous écrivions l'intégralité du programme sur des feuilles de programmation, que nous confiions aux dactylographes pour qu'elles l'introduisent dans la machine. Celles-ci pouvaient être réintroduites et stockées sur disquette. Nous réservions alors une session sur le terminal, lancions le compilateur COBOL sur le programme et attendions que l'impression arrive avec une liste du programme et toutes les erreurs du compilateur. Comme nos dactylographes rapides et sympathiques n'étaient pas des programmeuses, elles n'allaient pas vérifier la syntaxe de nos programmes, de sorte qu'il y avait beaucoup d'erreurs la première fois. Nous devions travailler sur trois ou quatre programmes par jour pour gagner du temps. Une fois que nous avions corrigé toutes les erreurs de compilation, étant donné que cela pouvait prendre plusieurs passages et donc quelques jours, nous pouvions alors essayer d'exécuter le programme. Là encore, le programme n'allait jamais fonctionner du premier coup, et généralement il se "plantait", c'est-à-dire qu'il s'arrêtait à une ligne de code particulière parce que nous étions en train de faire quelque chose de ridicule. L'une des erreurs les plus courantes consistait à essayer d'effectuer des calculs sur une variable décimale compressée que nous avions oublié d'initialiser. Lorsque le programme se bloquait, nous devions alors attendre que le chariot d'impression nous livre une impression de tout le contenu de la mémoire de notre programme, en hexadécimal et en ASCII, ainsi que l'adresse de l'endroit où il s'est arrêté. Plus tard, ils ont optimisé cette opération pour réduire la taille de l'impression. Il ne fallait que quelques secondes pour déterminer quelle ligne du programme avait échoué et pourquoi, puis nous jetions 800 mètres de papier en accordéon dans la poubelle et faisions la correction sur une session de terminal et recommencions. Traçage Bien que nous n'ayons pas de débogueur en temps réel (nous en reparlerons plus tard), nous avions quelques fonctions pratiques dans le compilateur. Nous pouvions utiliser la commande "READY TRACE", et le programme affichait les noms de toutes les routines qu'il rencontrait. Si vous aviez beaucoup de boucles, cela pouvait produire une montagne de papier, aussi ne devrions-nous pas choisir d'activer cette fonction au début du programme. Le seul autre outil dont vous disposiez était d'imprimer toutes les variables utiles et autres informations pendant l'exécution du programme. Comme nous passions beaucoup de temps assis à notre bureau, nous passions beaucoup de temps à vérifier le code à l'oeil. Le COBOL est un peu plus lisible que le C, ce qui n'est pas si mal. Il permet d'aiguiser les capacités d'observation et de logique. N'oubliez pas que nous n'écrivions pas de programmes graphiques, mais que nous faisions du calcul, de la mise à jour de fichiers et de la production de rapports. L'échec n'est pas une option Lors de l'écriture d'un programme, il est essentiel de s'assurer que les routines donnent les bons résultats, mais aussi qu'elles ne font rien d'autre par inadvertance. Cela signifie qu'il faut rechercher les lignes de code "inutiles" que vous avez oublié de supprimer. Il est également essentiel de s'assurer que toutes les vérifications d'erreurs sont testées. Cela signifie parfois qu'il faut simuler des erreurs afin de vérifier que le programme gère les situations d'erreur. La plupart du temps, cela n'arriverait pas dans un jeu 8 bits parce que nous étions totalement en contrôle, qu'aucune routine tierce n'était appelée et que nous n'avions rien qui puisse produire une erreur. De toute façon, que pouvions-nous faire en cas d'erreur ? Débogage en temps réel Pour expliquer ce qu'est un débogueur en temps réel, je vais remonter le temps jusqu'à l'époque des ordinateurs multitâches 32 et 64 bits, vous comprendrez pourquoi dans un instant. Au fur et à mesure que les ordinateurs sont devenus plus complexes, le besoin d'une meilleure visibilité et d'un meilleur contrôle s'est accru. Il n'était plus acceptable qu'un programme s'arrête et affiche la ligne de code à laquelle il était parvenu. Nous voulions être en mesure d'examiner la mémoire vive, les variables et éventuellement de les corriger temporairement afin de pouvoir poursuivre le test. Tout cela était possible grâce à la conception du processeur qui permettait d'arrêter un programme à n'importe quel moment. Cela pouvait se faire à partir d'un autre programme, appelé débogueur en temps réel, qui surveillait votre programme. Vous pouviez même surveiller un programme qui s'exécutait sur un autre ordinateur, nous y reviendrons plus tard. Points d'arrêt Comme votre programme était séparé du système d'exploitation et du programme qui le surveille, ainsi que de tous les autres programmes en cours d'exécution, il était peu probable que vous bloquiez toute la machine si vous faisiez une erreur. Votre programme pensait qu'il était le seul programme en cours d'exécution, mais ce n'était pas le cas. Vous pouviez regarder votre programme s'exécuter dans l'éditeur afin de voir votre code source plutôt que le code machine que l'ordinateur exécute réellement, et vous pouviez placer des points d'arrêt dans le programme, c'est-à-dire des marqueurs qui arrêteront le programme s'il exécute la ligne de code que vous aviez marquée. Généralement, le débogueur modifie votre programme pour lui passer le contrôle en remplaçant votre code par une seule instruction. Il doit alors se souvenir de votre code original afin de pouvoir exécuter le bon code ultérieurement. Vous pouviez répandre généreusement ces marqueurs de points d'arrêt pour, par exemple, vérifier que chaque itinéraire d'une fonction était testé. Il suffisait de supprimer les points d'arrêt une fois qu'ils étaient testés avec succès. Parfois, vous deviez programmer temporairement des tests supplémentaires afin que les points d'arrêt ne se produisent que lorsque vous le souhaitiez. Les débogueurs en temps réel sont intégrés aux éditeurs de code et à la sortie du compilateur, de sorte qu'il suffisait de taper les noms des variables pour qu'ils indiquent leur type et leur valeur. Vous pouviez examiner des zones de mémoire pour vous assurer que vos données n'étaient pas corrompues et vous pouviez voir tous les noms de routine et de fonction dans le code. On se demande comment on a pu s'en passer. Retour à l'Amiga Retour en arrière, à l'époque de l'Amiga. Le processeur 68000 était en effet équipé d'instructions permettant aux débogueurs d'intercepter le code et de voir ce qui se passait. Comme nous écrivions des jeux et que nous avions pris le contrôle de l'écran, que nous l'avions formaté comme nous le voulions et que nous contrôlions les couleurs, il n'aurait pas été possible pour un programme tiers de fonctionner de manière réaliste sur l'Amiga et d'écrire sur l'écran de l'Amiga. Nous compilions donc nos programmes sur un PC, puis nous faisions passer le programme et les graphismes par un câble de connexion (je ne me souviens plus s'il était sériel ou parallèle) vers un boîtier d'extension branché sur le côté de l'Amiga. Cela nous permettait de voir à l'intérieur de l'Amiga sans risquer d'être affectés par les problèmes de l'Amiga. Sur l'Amiga, nous écrivions en assembleur 68000 plutôt que dans un langage de plus haut niveau. Il n'y avait pas d'alternative à l'époque, et nous n'en cherchions pas. Le processeur tournait à 7,14 MHz (aujourd'hui, les fréquences des processeurs des PC sont de 2500 MHz, et en multicoeur !). L'écriture en assembleur donne les résultats les plus rapides, et nous avions 16 registres 32 bits, ce qui n'est pas sans rappeler l'architecture des gros ordinateurs centraux d'autrefois, ni l'architecture 32 bits des PC. À cette époque, nous ne pouvions pas corriger le code, il suffisait de tout réassembler, mais au moins vous pouviez éditer le programme et le regarder côte à côte avec le débogueur. La taille totale du programme devait être inférieure à 512 ko, car c'était la taille minimale de mémoire que nous gérions. L'une des complications liées à la taille différente des Amiga est que nous avons dû écrire un gestionnaire de mémoire capable d'allouer de la mémoire Chip ou de la mémoire Fast pour les caches de disque, et de pousser le code du programme vers la mémoire Fast si elle était disponible. Il était donc important de surveiller si la mémoire était allouée et libérée correctement au cours de plusieurs parties, faute de quoi le programme dériverait et se bloquerait. Il fallait également veiller à ce que la mémoire allouée ne se fragmente pas au fil du temps, sous peine de se retrouver à nouveau avec des zones de mémoire libre contiguës trop petites. Couleurs des bordures Une technique de contrôle de la mémoire consiste à stocker des variables de débogage supplémentaires dans votre programme afin de contrôler le nombre de blocs de mémoire alloués, ce qui vous permet d'être facilement alerté en cas de problème. Nous avions l'habitude d'utiliser la méthode plutôt rudimentaire consistant à régler la couleur de la bordure sur une couleur différente selon les routines, de sorte que si le programme s'arrêtait et que la couleur était, disons, bleu clair, nous savions qu'il s'était planté dans une routine de tracé. Cela nous a également permis de voir en temps réel quelles routines prenaient plus de temps, car la couleur de la bordure était une masse de couleurs différentes, et comme le jeu était synchronisé avec la trame ("raster") de l'écran pour ne pas aller trop vite, les couleurs étaient cohérentes d'une image à l'autre. Il était facile de voir combien de temps il restait avant l'image suivante, et vous pouviez facilement voir quand le jeu s'emballait. Bien sûr, il n'y a pas de couleur de bordure sur un PC. "Ce n'est pas la peine, monsieur, vous êtes la 83e personne à qui je le dis aujourd'hui !". Atari ST Avant cela, sur Atari ST, je crois me souvenir que nous avions deux Atari ST sur le bureau et que nous transférions une disquette de la machine qui compilait vers celle qui fonctionnait. Ce n'était pas aussi bien qu'un câble de transfert, mais comme nous ne déboguions pas le code source, ça allait. Nous disposions du HiSoft Devpac et cela nous permettait de déboguer en temps réel sur l'Atari ST, tant que notre programme ne mettait pas la machine hors service. Dominic Robinson avait écrit un système d'exploitation pour les jeux et l'avait pratiquement débogué lorsque je suis arrivé, la transition s'est donc faite en douceur, même si je déteste toujours la méthodologie de conception orientée objet, qui était entièrement basée sur l'exécution parce que nous travaillions toujours en assembleur. Oui, c'était intelligent, mais il n'y a pas tant d'endroits où l'on peut utiliser l'héritage, et si on le fait, on ne comprendra jamais où se trouve le code. Il y avait également une grille carrée de types d'objets et de méthodes, qui devenait clairement incontrôlable et prenait de plus en plus de mémoire vive. Commodore 64 Pour mon dernier jeu, j'ai utilisé une trousse de développement fonctionnant sur PC, toujours avec le C64 connecté. Le programme était compilé sur le PC et transféré sur le C64, avec les graphismes. C'était en 1989 et j'ai du mal à me souvenir de ce que c'était, je ne l'ai utilisé que pour Intensity. Il m'a permis d'écrire le plus gros programme 8 bits que j'aie jamais écrit, il y avait 29 ko de code sur les 64 ko de mémoire, soit environ 15 000 lignes de code source. Il aurait eu 15 ko de graphismes, et j'ai utilisé le dernier quart de la mémoire comme zone vidéo et j'avais tous les 64 ko de mémoire, aucune ROM n'a été connectée. Le reste de l'espace était réservé aux données des niveaux et aux tampons mémoire. Je crois que cette trousse de développement me permettait de voir dans la mémoire du C64, ce qui est utile, mais il n'y avait pas de capacité de traçage de code. Mes autres jeux Morpheus et Alleykat ont été codés sur un PC et j'ai utilisé un assembleur croisé 6502 pour créer le code machine et nous avions un câble parallèle propriétaire pour envoyer le code sur le C64. Le PC était l'un de ceux de la première génération, avec deux disques durs de 5,25 pouces, 1 Mo de mémoire vive et un écran monochrome ambré. Comme nous ne pouvions pas voir la mémoire du C64 depuis le PC et que nous n'avions aucun moyen d'arrêter le programme, nous avons dû utiliser la fonction Pause du jeu. Lorsque vous développez votre boucle de jeu principale, vous devez pouvoir tenir le jeu "en l'air" pour voir ce qui se passe. Tout le monde pense que la fonction Pause du jeu sert à faire des pauses toilettes ou à passer un important coup de fil, mais en fait, c'était une aide au développement vitale. ABMon Le seul outil de débogage dont nous disposions était mon propre "ABMon". Il s'agissait d'une routine qui était appelée à chaque image (que le jeu soit en mode Pause ou non). Elle affichait simplement une adresse hexadécimale à quatre chiffres et la valeur à deux chiffres de l'octet à cette adresse sur la ligne supérieure de l'écran. J'avais de toute façon besoin de chiffres pour l'affichage du score, j'ai donc simplement marqué les lettres A à F à la fin. J'ai branché quelques touches de fonction pour me permettre de modifier les quatre premiers chiffres vers le haut ou vers le bas afin de pouvoir composer n'importe quelle adresse. Page zéro Pour des raisons d'espace et de rapidité sur une puce 6502, toutes les variables doivent se trouver dans les 256 premiers octets, ce que l'on appelle la "page zéro". Je notais toutes les variables que j'avais assignées dans cette zone et je gardais ce papier à portée de main. Toutes les variables commençaient donc par "00" et étaient regroupées. Gardez à l'esprit que l'espace est compté et qu'il n'y a donc pas de place pour de jolis noms de variables dans le C64. En utilisant cette fonction, je pouvais observer n'importe quelle variable du jeu pendant qu'il était en cours ou en pause. La plupart des variables n'étaient que des valeurs d'un seul octet. Je crois que Steve Turner a fait une version sur le ZX Spectrum qui montrait un nombre d'octets. La première version d'ABMon était en fait sur Dragon 32 lorsque je convertissais la trilogie de jeux Seiddab Trilogy de Steve Turner sur ZX Spectrum. Je souffrais de ne pas pouvoir voir ce qui se passait à l'intérieur. Après avoir écrit la première version, j'ai réalisé qu'on pouvait aussi l'utiliser pour modifier la valeur d'un octet à un endroit particulier. Ainsi, si vous trouviez une valeur inattendue, vous pouviez la corriger et poursuivre les tests. Il fallait être prudent, car la plupart du temps, il fallait faire une pause pour modifier les valeurs, sinon le programme les rétablissait, et il ne fallait pas pointer ABMon sur le code machine et jouer avec les valeurs ! Écrivez-le À cette époque, je gardais également un bloc A4 à portée de main pour noter les bogues. Je les corrigeais par lots, surtout pour les premiers jeux sur C64, jusqu'à Uridium inclus. En effet, nous n'avions pas encore de PC et j'avais deux C64 sur le bureau. L'un d'eux servait à exécuter le Macro Assembleur C64, en utilisant le lecteur de disquettes "briques" 1541. Les assemblages prenaient 30 minutes, je ne pouvais donc pas justifier le réassemblage pour la correction d'un bogue à la fois. J'attendais d'avoir une feuille pleine de bogues, ou de ne plus trouver d'erreurs. Pendant l'assemblage, j'utilisais un deuxième C64 pour travailler sur les sprites, les caractères ou les cartes du jeu. Nous utilisions SpriteMagic et Ultrafont, que j'avais achetés au magasin d'informatique local, à l'époque où nous disposions d'endroits aussi fascinants. Pour revenir brièvement à l'époque du Dragon 32, j'ai acheté un programme d'assemblage multi-passes dans le même magasin d'informatique local, et je l'ai fait tourner sur le lecteur de disquette du Dragon 32, qui utilisait une ancienne variante de MS-DOS, si ma mémoire est bonne, appelée DragonDOS. Le temps d'assemblage était assez rapide, et j'ai codé dans plusieurs blocs de mémoire pour que le code reste modulaire, et aussi pour pouvoir lancer l'assembleur à partir de différents endroits afin d'assembler tous les blocs qui devaient être chargés à travers la machine. Les blocs comportaient de petites tables de saut au sommet pour accéder aux fonctions inférieures, un peu comme les DLL aujourd'hui. Ce n'est pas tout à fait efficace, mais ce n'est pas mal. Cela fonctionnait assez bien pour me permettre de travailler sur de petits fichiers de code source. Lorsque vous travaillez sur le code de la machine cible, tout est détruit lorsque vous lancez le jeu, une réinitialisation s'imposait donc. Heureusement, le système d'exploitation était terminé et se trouvait dans des ROM, de sorte qu'il suffisait d'appuyer sur l'interrupteur marche/arrêt pour le réinitialiser. Imaginez que vous deviez faire un redémarrage complet entre deux tests. Je sais qu'Azumi prend moins d'une minute, mais les ordinateurs 8 bits étaient prêts en une seconde. Du zéro au héros Voilà où nous en sommes, si vous avez suivi tout cela. Lorsque nous avons commencé, nous ne disposions d'aucune capacité de débogage, ni même d'impressions arrivant sur le chariot. Juste une impression du jeu fini, que nous commentions après l'impression parce qu'il n'y avait pas d'espace dans l'éditeur pour stocker des commentaires. J'ai écrit le programme ABMon pour me permettre de voir ce qui se passait à l'intérieur de la machine, et nous comptions sur le fait que les programmes étaient suffisamment petits pour mémoriser toutes les routines. L'assembleur est suffisamment impitoyable pour que si vous faites une erreur de programmation, vous puissiez dire adieu à votre programme, et vous deviez être capable de connaître votre programme de fond en comble pour pouvoir naviguer dans le code, il n'y avait pas d'éditeurs fantaisistes. Le débogage de votre code pouvait se résumer à regarder le code jusqu'à ce que vous repériez l'erreur. Nametara Ikan Zeyo Il ne fait aucun doute que les débogueurs ont beaucoup évolué. Je dirais toujours qu'apprendre à utiliser le débogueur et à écrire du code entièrement débogué est l'un des arts de la magie noire. Il ne faut jamais le sous-estimer. Aujourd'hui J'utilise maintenant Visual Studio 2013 ; et l'exécution en mode fenêtré est gérable sur un système à écran unique. En plein écran, je devrais pouvoir faire tourner le débogueur sur un second moniteur. Si tout le reste échoue, je vais devoir brancher deux PC ensemble. Cela risque de ralentir les choses car ils sont dans des pièces différentes !
|