Contextes
Un jeu enchaîne des parties bien distinctes : l'écran titre, les niveaux,
l'écran de fin. Ce chapitre montre comment découper un jeu en contextes,
c'est-à-dire en morceaux chargés en mémoire seulement quand on en a besoin. Nous
construirons pas à pas un programme qui passe d'un écran titre à un jeu. En chemin, nous
rencontrerons deux outils indispensables : initContext() et les
interruptions.
Le problème : un jeu trop grand pour la mémoire
Les machines rétro ont très peu de mémoire. La PC Engine, par exemple, ne voit que 64 Ko à la fois, dont 40 Ko pour le programme. Or une cartouche peut contenir jusqu'à 1 Mo : bien plus que ce que le processeur peut voir d'un coup.
Heureusement, un jeu n'a jamais besoin de tout en même temps. Pendant l'écran titre, le code du niveau 3 ne sert à rien. Pendant le niveau 3, c'est l'écran titre qui ne sert à rien. D'où l'idée des contextes :
Un contexte, c'est une partie du jeu (le titre, un niveau, la fin), rangée à part et chargée en mémoire seulement quand on en a besoin. Un seul contexte est chargé à la fois. Le code qui sert partout reste toujours en mémoire.
Selon la machine
Les contextes ont été conçus pour les machines à mémoire segmentée, comme la PC Engine. Sa cartouche est découpée en banques de 8 Ko, et le programme choisit lesquelles sont visibles. Gérer ces banques à la main serait pénible. Simple8 fait donc comme si la cartouche était un système de fichiers : chaque contexte est un ensemble de banques, rangé par le compilateur.
Sur les micro-ordinateurs, comme le Commodore 64 ou l'Amstrad CPC, un contexte est tout simplement un fichier auxiliaire sur la disquette, que le programme principal charge en mémoire. Le programme s'écrit de la même façon partout. Seule change ce que la machine fait en coulisse.
| Machine | Où est rangé un contexte | Le charger prend… |
|---|---|---|
| PC Engine | des banques de 8 Ko de la cartouche | un instant : les banques sont rendues visibles |
| NES | des banques de 8 Ko de la cartouche | un instant |
| Game Boy | une banque de 16 Ko de la cartouche | un instant |
| Commodore 64 | un fichier .dat sur la disquette | le temps de lire le fichier |
| Amstrad CPC | un fichier sur la disquette | le temps de lire le fichier |
Étape 1 : un premier contexte
Commençons par un seul contexte : l'écran titre. Il faut faire deux choses : le déclarer, puis écrire sa unit.
Déclarer le contexte
Dans main.s8p, juste après les addlib, on déclare le contexte.
On lui donne un nom et le fichier de sa unit :
main.s8p
// Étape 1 : un seul contexte, l'écran titre (Commodore 64)
architecture commodore.c64;
addlib vic.base;
addlib vic.screentext;
context Titre {
addunit("Titre.s8u");
}
unit {
main() {
sys.loadContext(Titre); // charge le contexte Titre, et ne revient jamais
}
}
La unit principale ne fait plus qu'une chose : charger le contexte Titre
avec sys.loadContext(Titre). Cet appel ne revient jamais.
À partir de là, c'est le contexte qui a la main.
Écrire la unit du contexte
La unit d'un contexte ressemble à une unit ordinaire. Elle doit simplement contenir deux
fonctions obligatoires, initContext() et main() :
Titre.s8u
// Titre.s8u : le contexte de l'écran titre
unit Titre {
// Appelée à chaque chargement du contexte, avant main() : on prépare l'écran
initContext() {
vic.initScreen();
vic.setBorderColor(0);
vic.setBackgroundColor(6);
vic.clearScreen();
vic.setFontColor(1);
vic.setCursor(16, 6);
vic.print("TITRE");
}
// Le programme du contexte
main() {
while (1) {
}
}
}
Pourquoi initContext() ?
Un contexte peut être chargé plusieurs fois : on revient à l'écran titre après chaque
partie. À chaque chargement, il faut remettre l'écran en place : effacer, choisir les
couleurs, charger les graphismes. C'est le rôle d'initContext(). Les deux
fonctions ont chacune un métier bien précis :
initContext()prépare : elle est appelée à chaque chargement, avantmain();main()joue : c'est le programme du contexte.
On pourrait tout mettre dans main(). Mais séparer les préparatifs rend le
contexte plus lisible. Et nous verrons plus bas qu'initContext() a un
avantage de plus : rien ne peut l'interrompre.
Titre est chargé et s'affiche.Étape 2 : passer à un deuxième contexte
Ajoutons le jeu. On déclare un deuxième contexte, et l'écran titre le charge à son tour :
// main.s8p : un contexte de plus
context Jeu {
addunit("Jeu.s8u");
}
// Titre.s8u : main() passe au jeu
main() {
sys.loadContext(Jeu);
}
Le programme compile, et le jeu s'affiche bien. Mais l'écran titre, lui, n'apparaît jamais : il passe au jeu à l'instant où il commence. Il faudrait qu'il attende un peu, deux secondes par exemple.
Comment attendre deux secondes ? Une boucle qui compte jusqu'à un grand nombre attendrait plus ou moins longtemps selon la machine. Il nous faut une vraie horloge.
Étape 3 : mesurer le temps avec une interruption
Qu'est-ce qu'une interruption ?
Imaginez que vous lisez un livre. Une sonnerie retentit à intervalles réguliers. À chaque sonnerie, vous notez un trait sur une feuille, puis vous reprenez votre lecture. Pour savoir combien de temps a passé, il suffit de compter les traits.
Une interruption fonctionne ainsi. Le circuit vidéo envoie un signal à chaque image qu'il affiche : 50 fois par seconde sur un C64 européen, 60 fois sur la PC Engine. À chaque signal, le processeur met son programme en pause, exécute un petit morceau de code (le gestionnaire d'interruption), puis reprend exactement où il en était.
main() tourne en continu ; à chaque image, l'interruption le
coupe un court instant pour compter.Écrire un gestionnaire
Un gestionnaire s'écrit dans la unit avec interrupt ->, suivi du nom de
l'interruption. Celle de l'image s'appelle irq sur C64. Pour qu'elle soit
active, main.s8p charge sa bibliothèque : addlib event.irq;.
Titre.s8u
// Titre.s8u : le contexte de l'écran titre
unit Titre {
ubyte images; // le nombre d'images depuis le chargement
// Appelée à chaque chargement du contexte, avant main() : on prépare l'écran
initContext() {
vic.initScreen();
vic.setBorderColor(0);
vic.setBackgroundColor(6);
vic.clearScreen();
vic.setFontColor(1);
vic.setCursor(16, 6);
vic.print("TITRE");
}
// Le programme du contexte : attendre 100 images (2 secondes), puis passer au jeu
main() {
while (.images < 100) {
}
sys.loadContext(Jeu);
}
// À chaque image, 50 fois par seconde, le programme est interrompu le temps de compter
interrupt -> irq {
.images++;
}
}
Le travail est partagé en deux :
- le gestionnaire compte : à chaque image,
.imagesaugmente de 1 ; main()attend : sa boucle tourne tant que.imagesest inférieur à 100, puis il charge le jeu.
100 images à 50 par seconde, cela fait 2 secondes. Cette fois, l'écran titre reste affiché deux secondes, puis le jeu apparaît.
| Machine | Interruption de l'image | Bibliothèque à charger |
|---|---|---|
| Commodore 64 | interrupt -> irq, 50 fois par seconde | addlib event.irq; |
| PC Engine | interrupt -> irq1, 60 fois par seconde | addlib event.irq1; |
Il existe d'autres interruptions (un minuteur, timer, par exemple), et les
autres machines ont leurs propres noms. Leur documentation les décrit.
Et initContext() dans tout ça ?
Pendant qu'initContext() s'exécute, les interruptions sont
masquées : le gestionnaire n'est pas encore branché. Les préparatifs se
font donc d'un seul tenant, sans risque d'être coupés au milieu. C'est important, par
exemple, quand on copie une police en mémoire vidéo. Les interruptions ne démarrent
qu'après, juste avant main().
Sur C64, changez de contexte depuis main(). On pourrait
appeler sys.loadContext directement dans le gestionnaire. Sur PC Engine,
cela fonctionne. Mais sur C64, pour l'instant, le contexte suivant ne reçoit alors plus
d'interruptions, et le programme reste bloqué. Le partage ci-dessus — le gestionnaire
compte, main() décide — fonctionne partout.
Récapitulatif : le cycle de vie
Nous pouvons maintenant compléter le schéma du chapitre 2. Au démarrage, comme à chaque chargement d'un contexte, les étapes sont les mêmes :
main().- la machine est préparée : par
boot.asmau démarrage, parsys.loadContextpour un contexte, qui met aussi le contexte en mémoire ; initContext()fait les préparatifs, sans être interrompue ;- les gestionnaires d'interruption sont branchés : ceux du contexte, à défaut ceux de la unit principale ;
main()commence.
La unit principale de main.s8p suit le même chemin : elle aussi peut avoir
son initContext() et ses gestionnaires. sys.loadContext(Main) y
revient. Rien n'est chargé, puisqu'elle est toujours en mémoire, mais son
initContext() et son main() sont rappelés.
Ce qu'un contexte peut voir
Comme un seul contexte est chargé à la fois, un contexte ne peut utiliser que ce qui se trouve en mémoire avec lui :
| Depuis… | On peut utiliser | On ne peut pas utiliser |
|---|---|---|
| une unit d'un contexte | les units de son contexte, et tout le code résident | les units d'un autre contexte |
| le code résident | le code résident | les units des contextes |
Le code résident, c'est la unit principale et les units ajoutées par
addunit en dehors de tout contexte. Ce qui doit servir à plusieurs
contextes — par exemple un texte à afficher — va donc dans une unit résidente.
addunit "Commun.s8u"; // hors contexte : résidente, visible de tous
context Jeu {
addunit("Jeu.s8u"); // la unit principale du contexte : initContext() et main()
addunit("Ennemis.s8u"); // une autre unit du même contexte, sans initContext() ni main()
}
Utiliser une unit d'un autre contexte est refusé. Le compilateur affiche alors un message qui rappelle la règle :
Jeu.essais: unit Jeu belongs to context Jeu, not reachable from context Titre.
Contexts are isolated: switch with sys.loadContext(Jeu)
Les variables d'un contexte
À chaque chargement, les variables d'un contexte reprennent leur valeur de départ : un niveau rechargé recommence au début. Certaines valeurs doivent pourtant être conservées, comme le nombre de parties jouées. Selon l'endroit où l'on déclare une variable, elle est remise à zéro ou conservée :
| Variable | Où la déclarer | À chaque chargement du contexte |
|---|---|---|
| du contexte | dans une unit du contexte | remise à sa valeur de départ |
shared | dans un bloc shared { } d'une unit du contexte | gardée, et réservée à ce contexte |
| résidente | dans la unit principale, ou une unit hors contexte | gardée, et visible de tous les contextes |
unit Jeu {
ubyte essais; // remis à zéro à chaque chargement du jeu
shared {
ubyte parties; // gardé d'un chargement à l'autre
}
}
Le programme complet
Voici le programme des trois étapes, terminé. Le titre et le jeu se passent la main
toutes les deux secondes. Une unit résidente, Commun, affiche les écrans pour
les deux contextes. Trois compteurs montrent le sort des trois sortes de variables.
main.s8p
// Contextes : un écran titre et un jeu, chargés tour à tour (Commodore 64)
architecture commodore.c64;
addlib event.irq;
addlib vic.base;
addlib vic.screentext;
addunit "Commun.s8u"; // résidente : toujours en mémoire
context Titre {
addunit("Titre.s8u"); // unit principale du contexte Titre
}
context Jeu {
addunit("Jeu.s8u"); // unit principale du contexte Jeu
}
unit {
// Au démarrage, avant main() : l'écran en mode texte
initContext() {
vic.initScreen();
vic.setBorderColor(0);
}
main() {
sys.loadContext(Titre); // ne revient jamais
}
}
Commun.s8u
// Commun.s8u : résidente, utilisée par tous les contextes
unit Commun {
ubyte chargements; // contextes chargés depuis le démarrage : jamais remis à zéro
// Un écran : sa couleur de fond et son titre
void ecran(ubyte fond, uint titre) {
vic.setBackgroundColor(fond);
vic.clearScreen();
vic.setFontColor(1);
vic.setCursor(16, 6);
vic.print(titre);
.chargements++;
}
// Un texte puis un chiffre, à la ligne y
void ligne(ubyte y, uint texte, ubyte valeur) {
vic.setCursor(10, y);
vic.print(texte);
vic.putChar((ubyte)('0' + valeur));
}
}
Titre.s8u
// Titre.s8u : le contexte de l'écran titre
unit Titre {
ubyte images; // remis à zéro à chaque chargement du contexte
initContext() {
Commun.ecran(6, "TITRE");
Commun.ligne(10, "CHARGEMENTS ", Commun.chargements);
}
// attendre 100 images (2 secondes), puis passer au jeu
main() {
while (.images < 100) {
}
sys.loadContext(Jeu);
}
// 50 fois par seconde
interrupt -> irq {
.images++;
}
}
Jeu.s8u
// Jeu.s8u : le contexte du jeu
unit Jeu {
ubyte images; // remis à zéro à chaque chargement
ubyte essais; // remis à zéro à chaque chargement : vaut toujours 1 ici
shared {
ubyte parties; // gardé d'un chargement à l'autre
}
initContext() {
.essais++;
.parties++;
Commun.ecran(2, "JEU");
Commun.ligne(10, "CHARGEMENTS ", Commun.chargements);
Commun.ligne(12, "PARTIES ", .parties);
Commun.ligne(14, "ESSAIS ", .essais);
}
// attendre 100 images, puis revenir à l'écran titre
main() {
while (.images < 100) {
}
sys.loadContext(Titre);
}
interrupt -> irq {
.images++;
}
}
| Compteur | Rangé dans | Valeurs à chaque passage dans le jeu |
|---|---|---|
CHARGEMENTS | une unit résidente | 2, 4, 6, 8… : il compte aussi les passages par le titre |
PARTIES | un bloc shared | 1, 2, 3, 4… : gardé d'un chargement du jeu à l'autre |
ESSAIS | une variable du contexte | 1, 1, 1, 1… : remis à zéro à chaque chargement |
Titre.
Jeu, au quatrième passage.Ce que produit le compilateur
Sur C64, la compilation produit un fichier par contexte, et les réunit sur une disquette :
context Titre: titre.dat, 175 bytes at $6000
context Jeu: jeu.dat, 229 bytes at $6000
| Fichier | Contenu |
|---|---|
main.prg | le code résident : la unit principale, Commun, les bibliothèques |
titre.dat, jeu.dat | un fichier par contexte, chargé à l'adresse $6000 |
main.d64 | l'image de la disquette, qui contient tous ces fichiers : c'est elle qu'il faut lancer |
Sur PC Engine, tout est dans une seule ROM, main.pce : les contextes y
occupent chacun leurs banques.
Le programme vu par Simple8 IDE
Simple8 IDE montre l'organisation d'un programme dans son diagramme du projet (Projet → Diagramme du projet, ⌥⌘D). Après la compilation de l'exemple, il se présente ainsi :
Ressources 0 o · Système 2 142 o ROM, 135 o RAM (démarrage, runtime, chaînes)
- En haut,
main.s8p: la unit principale, et le socle résident, c'est-à-dire tout ce qui reste en mémoire en permanence. Ses jauges comparent la place prise à la place disponible dans sa zone ($0801-$5FFFsur C64). - En dessous, ce qu'il contient : ses bibliothèques, la unit résidente
Commun, et chaque contexte. - Chaque contexte indique où il est rangé (ici, un fichier chargé à
$6000), la place qu'il occupe dans sa zone de 16 Ko, et, en dessous, ses units.
Les tailles sont en octets (o). Elles viennent du fichier asm/main_units.json,
que le compilateur écrit à chaque compilation réussie.
La version PC Engine
La structure est la même. La unit principale charge la police dans son
initContext(), le fond change par la palette, et l'interruption de l'image
s'appelle irq1. Elle se déclenche 60 fois par seconde : il faut donc 120
images pour deux secondes.
main.s8p
// Contextes : un écran titre et un jeu, chargés tour à tour (PC Engine)
architecture nec.pce;
addlib event.irq1;
addlib sys.resource;
addlib vce.base;
addlib vdc.base;
addlib vdc.screentext;
addunit "Commun.s8u"; // résidente : toujours en mémoire
context Titre {
addunit("Titre.s8u"); // unit principale du contexte Titre
}
context Jeu {
addunit("Jeu.s8u"); // unit principale du contexte Jeu
}
unit {
const int font = addResource("font.bin");
// Au démarrage, avant main() : l'écran, la police, le texte en blanc
initContext() {
int source;
int length;
vdc.setBATSize(vdc.BAT_32x32);
vdc.initScreen();
resource.mapToMPR2(.font);
source = 0x4000 + resource.getOffset(.font);
length = resource.getLength(.font);
vdc.writeToVRAM(source, 0x1000, length);
vce.setPalette(0, 1, RGB(7, 7, 7));
}
main() {
sys.loadContext(Titre); // ne revient jamais
}
}
Commun.s8u
// Commun.s8u : résidente, utilisée par tous les contextes
unit Commun {
ubyte chargements; // contextes chargés depuis le démarrage : jamais remis à zéro
// Un écran : sa couleur de fond et son titre
void ecran(uint fond, uint titre) {
vce.setPalette(0, 0, fond);
vdc.clearScreen();
vdc.setFontColor(0);
vdc.setCursor(13, 7);
vdc.print(titre);
.chargements++;
}
// Un texte puis un chiffre, à la ligne y
void ligne(ubyte y, uint texte, ubyte valeur) {
vdc.setCursor(8, y);
vdc.print(texte);
vdc.putChar((ubyte)('0' + valeur));
}
}
Titre.s8u
// Titre.s8u : le contexte de l'écran titre
unit Titre {
ubyte images; // remis à zéro à chaque chargement du contexte
initContext() {
Commun.ecran(RGB(0, 0, 7), "TITRE");
Commun.ligne(11, "CHARGEMENTS ", Commun.chargements);
}
// attendre 120 images (2 secondes), puis passer au jeu
main() {
while (.images < 120) {
}
sys.loadContext(Jeu);
}
// 60 fois par seconde
interrupt -> irq1 {
.images++;
}
}
Jeu.s8u
// Jeu.s8u : le contexte du jeu
unit Jeu {
ubyte images; // remis à zéro à chaque chargement
ubyte essais; // remis à zéro à chaque chargement : vaut toujours 1 ici
shared {
ubyte parties; // gardé d'un chargement à l'autre
}
initContext() {
.essais++;
.parties++;
Commun.ecran(RGB(4, 0, 1), "JEU");
Commun.ligne(11, "CHARGEMENTS ", Commun.chargements);
Commun.ligne(13, "PARTIES ", .parties);
Commun.ligne(15, "ESSAIS ", .essais);
}
// attendre 120 images, puis revenir à l'écran titre
main() {
while (.images < 120) {
}
sys.loadContext(Titre);
}
interrupt -> irq1 {
.images++;
}
}
Titre.
Jeu.À retenir
- Un contexte est une partie du jeu chargée en mémoire seulement quand on en a besoin : des banques de la cartouche sur PC Engine, un fichier sur la disquette sur C64. Un seul est chargé à la fois.
- On le déclare dans
main.s8p:context Nom { addunit(…); }, et on le charge avecsys.loadContext(Nom), qui ne revient jamais. - Sa unit principale a deux fonctions :
initContext()prépare, sans être interrompue, etmain()joue. - Une interruption appelle un gestionnaire à intervalles réguliers :
celle de l'image sert d'horloge. Le gestionnaire compte,
main()décide. - Un contexte ne voit que ses units et le code résident.
- Ses variables repartent de zéro à chaque chargement, sauf celles de
shared { }; les variables résidentes sont toujours gardées.
À vous de jouer
- Faites durer l'écran titre cinq secondes au lieu de deux.
Voir la solutionMasquer la solution
Cinq secondes, c'est 5 × 50 = 250 images sur C64, et 5 × 60 = 300 images sur PC Engine. Sur C64, il suffit de changer le nombre dans
Titre.s8u: 250 tient dansimages, unubyte(255 au plus).Sur Commodore 64
// attendre 250 images (5 secondes), puis passer au jeu main() { while (.images < 250) { } sys.loadContext(Jeu); }Dans l'émulateur de test, le titre apparaît à 0,21 s et le jeu à 5,16 s : cinq secondes, comme prévu.
Sur PC Engine
300 ne tient pas dans un
ubyte. Le compilateur le remarque, et refusewhile (.images < 300):Comparison of Titre.images (ubyte, 0..255) with 300 is always true or always falseL'idée qui vient ensuite est de déclarer
imagesenuint. Le programme compile, mais nous avons mesuré que le premier écran titre ne dure que 256 images (4,3 s) au lieu de 300. La cause est un piège classique des interruptions : unuintoccupe deux octets, et le processeur 8 bits les lit l'un après l'autre. Quand le compteur passe de 255 à 256, si l'interruption arrive entre les deux lectures,main()lit l'octet bas d'avant (255) et l'octet haut d'après (1) : il croit voir 511, et sort de la boucle trop tôt. Cela ne se produit pas à chaque fois, ce qui rend l'erreur difficile à repérer.Une solution simple : ne partager entre l'interruption et
main()que desubyte, qui se lisent d'un seul coup. Le gestionnaire compte les images, et toutes les 60 images, ajoute une seconde ;main()attend 5 secondes.Titre.s8u// Titre.s8u : le contexte de l'écran titre unit Titre { ubyte images; // les images de la seconde en cours, de 0 à 59 ubyte secondes; // les secondes écoulées depuis le chargement initContext() { Commun.ecran(RGB(0, 0, 7), "TITRE"); Commun.ligne(11, "CHARGEMENTS ", Commun.chargements); } // attendre 5 secondes, puis passer au jeu main() { while (.secondes < 5) { } sys.loadContext(Jeu); } // 60 fois par seconde : toutes les 60 images, une seconde de plus interrupt -> irq1 { .images++; if (.images == 60) { .images = 0; .secondes++; } } }Mesuré dans l'émulateur de test : l'écran titre dure 298 images, soit environ 5 secondes, à chaque passage. Le gestionnaire compte toujours, et
main()décide toujours : le partage du chapitre est respecté. - Déplacez
essaisdans le blocshared: comment évolue-t-il ?Voir la solutionMasquer la solution
Il suffit de déplacer la déclaration dans le bloc
shared, à côté departies:unit Jeu { ubyte images; // remis à zéro à chaque chargement shared { ubyte parties; // gardé d'un chargement à l'autre ubyte essais; // gardé lui aussi, désormais }essaisn'est plus remis à zéro à chaque chargement du contexteJeu: il évolue maintenant exactement commeparties. Nous l'avons vérifié sur les deux machines, en relevant l'écran à chaque passage dans le jeu :Passage dans le jeu 1er 2e 3e 4e ESSAISavant1 1 1 1 ESSAISdansshared1 2 3 4 
Au quatrième passage, ESSAIS vaut 4, comme PARTIES. La différence avec
Commun.chargementsreste la même : une variablesharedest gardée, mais elle appartient au contexteJeu; le titre ne peut pas la lire. Remarquez aussi qu'au dixième passage, le chiffre devient::Commun.lignen'écrit qu'un chiffre, avec'0' + valeur. - Ajoutez un contexte
Fin, chargé parJeuau bout de trois parties, qui affiche « FIN » et ne change plus.Voir la solutionMasquer la solution
Trois choses à faire : déclarer le contexte dans
main.s8p, écrire sa unit, et décider dansJeuquand le charger. Le nombre de parties est déjà compté par.parties, qui est dans le blocsharedet survit donc aux chargements.main.s8pcontext Fin { addunit("Fin.s8u"); // unit principale du contexte Fin }Le contexte
Finaffiche son écran dansinitContext(), avec la unit résidenteCommun, puis sonmain()tourne sans fin, sans rien faire. Il n'a pas besoin d'interruption : il n'attend rien.Fin.s8u// Fin.s8u : le contexte de fin, qui ne change plus unit Fin { initContext() { Commun.ecran(0, "FIN"); } // plus rien à faire : on reste sur cet écran main() { while (1) { } } }Dans
Jeu.s8u, la décision se prend dansmain(), après l'attente, et non dans le gestionnaire d'interruption (on se souvient de l'avertissement du chapitre pour le C64). Commesys.loadContextne revient jamais, leelseest inutile : si la fin est chargée, la ligne suivante n'est jamais exécutée.Jeu.s8umain() { while (.images < 100) { } if (.parties == 3) { sys.loadContext(Fin); // trois parties jouées : c'est la fin } sys.loadContext(Titre); }Sur PC Engine, c'est le même code, avec
while (.images < 120)dansJeuetCommun.ecran(RGB(0, 0, 0), "FIN");dansFin, puisque le fond s'y donne par une couleur.Ce que nous avons observé : le titre et le jeu alternent trois fois, puis l'écran « FIN » apparaît vers 12 s et ne change plus. Sur C64, le compilateur crée un fichier de plus,
fin.dat(25 octets), et les fichiers lus sur la disquette sont, dans l'ordre :TITRE.DAT, JEU.DAT, TITRE.DAT, JEU.DAT, TITRE.DAT, JEU.DAT, FIN.DAT. Nous avons aussi lancé la disquette dans VICE : l'écran « FIN » y est bien affiché après les trois parties. - Remplacez l'attente de l'écran titre par l'appui sur un bouton de la manette
(
io.readJoysticks(),io.getButtons(0)).Voir la solutionMasquer la solution
La bibliothèque des manettes s'ajoute dans
main.s8p, avec les autres :addlib io.base;.io.readJoysticks()lit l'état des manettes, puisio.getButtons(0)donne les boutons de la première : 0 si aucun n'est appuyé. Sur C64, c'est le bouton de tir du joystick du port 2 ; sur PC Engine, n'importe quel bouton (I, II, SELECT ou RUN).L'écran titre n'a plus besoin de compter les images : la variable
imageset le gestionnaire d'interruption disparaissent.main()attend en deux temps :Titre.s8u// Titre.s8u : le contexte de l'écran titre unit Titre { initContext() { Commun.ecran(6, "TITRE"); Commun.ligne(10, "CHARGEMENTS ", Commun.chargements); } // attendre un appui sur un bouton de la manette, puis passer au jeu main() { // d'abord, que le bouton soit relâché (il est peut-être encore enfoncé) io.readJoysticks(); while (io.getButtons(0) != 0) { io.readJoysticks(); } // puis, qu'il soit appuyé while (io.getButtons(0) == 0) { io.readJoysticks(); } sys.loadContext(Jeu); } }Pourquoi attendre d'abord que le bouton soit relâché ? Parce qu'on arrive sur le titre en venant du jeu, et le joueur a peut-être encore le doigt sur le bouton. Nous l'avons vérifié en simulant les appuis dans l'émulateur de test. Sans la première boucle, un bouton tenu enfoncé fait sauter l'écran titre : il est remplacé par le jeu dans la même image. Avec elle, le titre reste affiché tant que le bouton n'a pas été relâché puis appuyé de nouveau. Sans aucun appui, il reste affiché indéfiniment, comme on le souhaite.
Le code est le même sur les deux machines (seuls les arguments de
Commun.ecranetCommun.ligne, dansinitContext(), diffèrent, comme dans l'exemple du chapitre). Le contexteJeune change pas : il garde son interruption pour revenir au titre au bout de deux secondes.
