Projet complet pas à pas
Voici le chapitre où tout se rejoint. Nous allons écrire un vrai jeu, un casse-briques, et le faire tourner sur sept machines. Les règles du jeu seront écrites une seule fois. L'affichage, lui, sera écrit pour chaque machine, parce que chacune dessine à sa façon. C'est en faisant ce travail qu'on comprend ce qui fait la personnalité d'une console ou d'un micro-ordinateur, et pourquoi les jeux de l'époque se ressemblaient si peu d'une machine à l'autre.
Un jeu, sept machines
Voici le résultat. Chaque image est une capture du programme de ce chapitre, compilé par Simple8 et lancé en émulation. Les règles sont les mêmes partout, au point près : seule la façon de dessiner change.
Chaque version a sa page de sources, avec tous ses fichiers. Les projets sont aussi dans
le dossier Examples/ du dépôt. Ouvrez-en un dans
Simple8 IDE, compilez, lancez : sans manette, une démo joue toute seule.
Touchez la manette, et la main est à vous.
| Machine | Dossier | Sources |
|---|---|---|
| Les règles, communes | Examples/common/CasseBriques/ | dans chaque page |
| Commodore 64 | Examples/commodore/c64/CasseBriques_C64/ | voir |
| PC Engine | Examples/nec/pce/CasseBriques_PCE/ | voir |
| NES | Examples/nintendo/nes/CasseBriques_NES/ | voir |
| Game Boy | Examples/nintendo/dmg/CasseBriques_DMG/ | voir |
| Mega Drive | Examples/sega/megadrive/CasseBriques_MD/ | voir |
| Amstrad CPC | Examples/amstrad/cpc/CasseBriques_CPC/ | voir |
| Amiga | Examples/commodore/amiga/CasseBriques_Amiga/ | voir |
Un exemple pour apprendre, pas une référence d'optimisation. Ces sept programmes ne sont pas la meilleure façon d'écrire un casse-briques sur chaque machine. Un programmeur qui viserait la performance sur une seule d'entre elles ferait d'autres choix : plus d'assembleur, des routines taillées pour son circuit vidéo, des données préparées à l'avance. Le but ici est pédagogique : montrer comment on aborde la conception d'un jeu selon l'architecture, et comment on le développe avec Simple8. Ces exemples ne sont évidemment pas parfaits, mais ils ont l'avantage d'être courts, lisibles et comparables d'une machine à l'autre.
Les règles du jeu
Avant d'écrire une ligne, on fixe ce que le jeu doit faire. C'est le cœur d'un Arkanoid, sans les bonus :
- un terrain de 160 × 144 points, fermé par des murs à gauche, à droite et en haut ;
- 50 briques : 10 colonnes, 5 rangées. La rangée du haut rapporte 50 points, celle du bas 10 ;
- une raquette de 24 points de large, qui avance de 2 points par image ;
- une balle de 4 × 4 points. Elle attend sur la raquette, et part quand on appuie sur le bouton, ou toute seule au bout d'une seconde ;
- l'angle du rebond dépend de l'endroit où la balle touche la raquette : c'est ce qui permet de viser ;
- 3 vies. Toutes les briques cassées : « BRAVO » et un nouveau mur. Plus de vie : « PERDU » et une nouvelle partie ;
- quand personne ne touche la manette, une démo déplace la raquette.
Tout ce qu'on voudra ajouter plus tard (bonus, niveaux, son) se greffera sur ce cœur.
L'architecture : des règles communes, un écran par machine
Le piège serait d'écrire sept jeux. On sépare plutôt le programme en deux units :
Jeucontient les règles : les positions, les vitesses, les briques, le score, les vies. Pas une ligne n'y dépend de la machine, pas même une lignearchitecture: une unit.s8uest compilée pour la machine du programme qui l'ajoute ;Ecrancontient tout ce qui touche au matériel : dessiner, lire la manette, attendre l'image suivante. Chaque machine a son propre fichierEcran.s8u.
Le contrat de l'unit Ecran
Jeu appelle huit fonctions d'Ecran. Chaque Ecran.s8u doit les définir, avec
les mêmes paramètres. C'est un contrat, comme une interface en Java : s'il en manque une,
le compilateur refuse le programme (« Function 'message' not found »).
| Fonction | Rôle |
|---|---|
init() | préparer l'écran : couleurs, motifs, murs, sprites |
attendreImage() | attendre l'image suivante : c'est elle qui donne le rythme du jeu |
manette() | rendre les touches : Jeu.GAUCHE, Jeu.DROITE, Jeu.BOUTON |
dessinerBriques() | dessiner les 50 briques |
effacerBrique(c, l) | effacer la brique de la colonne c, rangée l |
placer(raquette, x, y) | montrer la raquette et la balle à leur place |
afficherInfos() | écrire le score et les vies |
message(texte) | écrire « BRAVO » ou « PERDU » au milieu, 0 pour l'effacer |
Les coordonnées passées à Ecran sont celles du terrain, de 0 à 159 et de
0 à 143. Chaque machine place ce terrain où elle veut sur son écran : elle ajoute
simplement un décalage (X0, Y0).
Le programme principal
Le main.s8p de chaque machine est minuscule. Voici celui du Commodore 64 :
// Casse-briques — Commodore 64
//
// Les règles sont dans Examples/common/CasseBriques/Jeu.s8u, les mêmes sur toutes les machines.
// Ici, l'affichage du C64 : les briques en caractères, la balle et la raquette en sprites.
// Joystick en port 2 : gauche, droite, FEU pour lancer la balle. Sans joystick, la démo joue.
architecture commodore.c64;
addlib event.irq;
addlib vic.base;
addlib vic.screentext;
addlib vic.sprite;
addlib io.base;
addunit "../../../common/CasseBriques/Jeu.s8u"; // les règles, communes
addunit "Ecran.s8u"; // l'affichage du C64
unit {
main() {
Jeu.jouer();
}
// l'interruption de l'image : elle rythme Ecran.attendreImage()
interrupt -> irq {
}
}
Les bibliothèques changent d'une machine à l'autre, le reste ne change presque pas.
Le chemin ../../../common/ va chercher les règles dans le dossier commun :
il n'en existe qu'un exemplaire.
Pourquoi Jeu est ajouté avant Ecran ? Ecran lit des variables de Jeu :
Jeu.vies, et l'adresse du texte du score, @Jeu.texteScore. Avec
le compilateur actuel, une unit ne peut prendre l'adresse d'une variable que dans une
unit ajoutée avant elle. Les appels de fonctions, eux, se résolvent dans n'importe quel
ordre : Jeu peut appeler Ecran, ajouté après lui.
La boucle de jeu
Un jeu vidéo est une boucle : attendre l'image, lire la manette, faire avancer le monde d'un pas, montrer le résultat. La nôtre tient en quelques lignes :
// La boucle du jeu : appelée une fois par main(), ne revient jamais
void jouer() {
Ecran.init();
nouvellePartie();
while (1) {
Ecran.attendreImage();
image(Ecran.manette());
Ecran.placer((ubyte).raquetteX, (ubyte).balleX, (ubyte).balleY);
}
}
Et voici une image de jeu. La balle a deux états : collée à la raquette, ou en jeu.
// Une image de jeu
void image(ubyte touches) {
bougerRaquette(touches);
if (.collee == 1) {
coller();
if (.attente > 0) {
.attente--;
}
if ((touches & .BOUTON) != 0 || .attente == 0) {
.collee = 0;
.dx = 1;
.dy = -2;
}
return;
}
deplacerBalle();
if (.balleY >= .HAUTEUR) {
ballePerdue();
}
}
Toutes les vitesses sont en points par image : la raquette avance de 2, la balle de 1 ou 2 en x et de 2 en y. Pas de virgule, pas de calcul de temps : le jeu est réglé sur l'image. Conséquence directe : il va un peu plus vite sur les machines à 60 images par seconde (PC Engine, NES, Game Boy, Mega Drive japonaise ou américaine) que sur les machines européennes à 50 (Commodore 64, Amstrad CPC, Amiga). Beaucoup de jeux de l'époque avaient ce défaut.
Attendre l'image
attendreImage() est la fonction la plus importante d'Ecran : elle bloque
jusqu'au retour vertical, le moment où le faisceau remonte en haut de
l'écran (chapitre 12). Chaque machine le fait à sa façon :
| Machine | attendreImage() |
|---|---|
| Commodore 64 | irq.waitFrame(), avec un interrupt -> irq vide dans main.s8p |
| PC Engine | attendre que Main.images change ; interrupt -> irq1 l'augmente à chaque image |
| NES | ppu.waitFrame() : les tuiles changées pendant l'image sont posées à ce moment-là |
| Game Boy | attendre Main.images (interrupt -> vblank), puis recopier les objets par DMA |
| Mega Drive | vdp.waitVBlank() |
| Amstrad CPC | attendre Main.images (interrupt -> int, appelé une fois par image) |
| Amiga | ocs.frame() |
Le terrain et sa grille
Pourquoi 160 × 144 ?
C'est l'écran de la Game Boy, le plus petit des sept. Un terrain de cette taille tient donc partout, et la Game Boy l'affiche sans rien couper.
Les briques font 16 × 8 points. Ce n'est pas un hasard : 16 et 8 sont des puissances de deux (chapitre 13). Diviser par 16, c'est décaler de 4 bits vers la droite ; diviser par 8, décaler de 3. Sur un 6502 ou un Z80, qui n'ont pas d'instruction de division, la différence est énorme. Et une brique fait exactement deux tuiles de 8 × 8 sur les machines à tuiles.
Du point à la brique
Les briques sont rangées dans un tableau de 50 octets, briques : 1 si la
brique est là, 0 si elle est cassée. Pour savoir quelle brique se trouve sous un point
(x, y) du terrain, il suffit d'un petit calcul :
// La brique sous le point (x, y), ou 255 s'il n'y en a pas
ubyte briqueEn(int x, int y) {
ubyte n;
if (y < .HAUT_BRIQUES || y >= .BAS_BRIQUES || x < 0 || x >= .LARGEUR) {
return 255;
}
.colonneTouchee = (ubyte)(x >> 4); // x / 16
.ligneTouchee = (ubyte)((y - .HAUT_BRIQUES) >> 3); // (y - 16) / 8
n = (ubyte)(.DEBUT_LIGNE[.ligneTouchee] + .colonneTouchee);
if (.briques[n] == 0) {
return 255;
}
return n;
}
La multiplication rangée × 10 est remplacée par une table,
DEBUT_LIGNE[] = { 0, 10, 20, 30, 40 } : 10 n'est pas une puissance de deux, et
lire une table est plus rapide que multiplier. C'est une recette du chapitre 13.
La balle
Un axe à la fois
La balle avance de dx en x et de dy en y. Si l'on déplaçait les
deux à la fois, une balle qui arrive dans un coin de brique poserait une question
difficile : a-t-elle touché par le côté (il faut inverser dx) ou par
dessous (il faut inverser dy) ?
On évite la question en avançant d'abord en x, en regardant ce qu'on
touche, puis en y, en regardant encore. Chaque test ne concerne qu'une
direction : un contact en x inverse dx, un contact en y inverse
dy. C'est simple, et dans un casse-briques, ça se voit à peine.
Le bord qui avance
Inutile de tester les 16 points de la balle. Quand elle va vers la droite, seul son bord droit peut entrer dans une brique ; quand elle monte, seul son bord haut. On teste les deux coins de ce bord : une brique fait au moins 8 points, elle ne peut pas passer entre deux coins distants de 4.
// La balle : on avance en x, puis en y, et on regarde à chaque fois ce que touche le bord
// de la balle qui avance
void deplacerBalle() {
int x = .balleX + .dx;
int y;
int bord;
// les murs de gauche et de droite
if (x < 0) {
x = 0;
.dx = -.dx;
} else if (x > .LARGEUR - .BALLE) {
x = .LARGEUR - .BALLE;
.dx = -.dx;
}
// les briques, en x : le bord gauche ou droit de la balle, en haut et en bas
bord = x;
if (.dx > 0) {
bord = x + .BALLE - 1;
}
if (toucher(bord, .balleY) != 0 || toucher(bord, .balleY + .BALLE - 1) != 0) {
.dx = -.dx;
x = .balleX;
}
.balleX = x;
// le plafond
y = .balleY + .dy;
if (y < 0) {
y = 0;
.dy = -.dy;
}
// les briques, en y : le bord haut ou bas de la balle, à gauche et à droite
bord = y;
if (.dy > 0) {
bord = y + .BALLE - 1;
}
if (toucher(.balleX, bord) != 0 || toucher(.balleX + .BALLE - 1, bord) != 0) {
.dy = -.dy;
y = .balleY;
}
.balleY = y;
rebondRaquette();
}
Remarquez l'ordre : on calcule la position future, on teste, et si la balle touche, on la laisse à sa place en inversant sa vitesse. La balle n'entre jamais dans une brique, et ne se coince jamais dans un mur.
toucher() fait tout le travail d'une brique cassée : la retirer du tableau,
compter les points, demander à Ecran de l'effacer, et vérifier s'il en reste.
// Casse la brique sous le point (x, y) s'il y en a une ; rend 1 si la balle a touché
ubyte toucher(int x, int y) {
ubyte n = briqueEn(x, y);
if (n == 255) {
return 0;
}
.briques[n] = 0;
.restantes--;
.score = .score + .POINTS[.ligneTouchee];
Ecran.effacerBrique(.colonneTouchee, .ligneTouchee);
majInfos();
if (.restantes == 0) {
gagne();
}
return 1;
}
Le rebond sur la raquette
Si la balle repartait toujours avec le même angle, le joueur ne pourrait pas viser. On coupe la raquette en quatre zones de 6 points : plus la balle touche près du bord, plus elle repart en biais.
// La raquette : la balle descend et chevauche la raquette
void rebondRaquette() {
int zone;
if (.dy < 0) {
return;
}
if (.balleY + .BALLE < .RAQUETTE_Y || .balleY > .RAQUETTE_Y + .RAQUETTE_H) {
return;
}
if (.balleX + .BALLE <= .raquetteX || .balleX >= .raquetteX + .RAQUETTE_L) {
return;
}
// l'endroit touché donne l'angle : le milieu de la balle sur l'une des 4 zones de
// 6 points de la raquette. Trois comparaisons, pas de division par 6
zone = .balleX + 2 - .raquetteX;
if (zone < 6) {
.dx = -2;
} else if (zone < 12) {
.dx = -1;
} else if (zone < 18) {
.dx = 1;
} else {
.dx = 2;
}
.dy = -2;
.balleY = .RAQUETTE_Y - .BALLE;
.visee++;
if (.visee == 6) {
.visee = 0;
}
}
Trois comparaisons suffisent : pas besoin de diviser par 6. Le test du début
(.dy < 0) évite qu'une balle qui remonte rebondisse une seconde fois sur la
raquette.
La démo
Une démo qui place toujours la balle au même endroit de la raquette la renvoie toujours avec le même angle. Pendant la mise au point de ce chapitre, avant le rebond actuel, cela a suffi pour que la balle tourne en rond dans une zone déjà vide : le score ne bougeait plus. La nôtre vise donc un endroit différent de la raquette à chaque rebond, pris dans une table :
// La raquette : les touches, ou la démo quand personne ne joue
void bougerRaquette(ubyte touches) {
if (touches != 0) {
.inactif = 0;
} else if (.inactif < 255) {
.inactif++;
}
if (.inactif > 120) {
// la démo : la raquette suit la balle
int cible = .balleX + 2 - .VISEES[.visee];
if (.raquetteX + 12 < cible - 1) {
touches = .DROITE;
} else if (.raquetteX + 12 > cible + 1) {
touches = .GAUCHE;
}
}
if ((touches & .GAUCHE) != 0 && .raquetteX > 0) {
.raquetteX = .raquetteX - 2;
}
if ((touches & .DROITE) != 0 && .raquetteX < .LARGEUR - .RAQUETTE_L) {
.raquetteX = .raquetteX + 2;
}
}
Avec VISEES[] = { -9, 6, -3, 9, -6, 3 }, la raquette se présente tantôt
à gauche, tantôt à droite de la balle : les rebonds successifs partent dans des
directions différentes. Une démo n'a pas besoin d'être intelligente, juste variée.
Trois façons de détecter une collision
Détecter qu'une balle touche une brique est la question centrale d'un casse-briques. Il y a trois grandes méthodes, et toutes ont servi à l'époque.
1. Calculer dans une grille
C'est la méthode de notre jeu. Le programme connaît la position de chaque brique dans un tableau, et calcule ce qu'il y a sous un point. Elle ne lit jamais l'écran :
- elle est rapide : deux décalages, une table, une case de tableau ;
- elle prévoit le contact avant qu'il ait lieu, et dit quelle brique est touchée ;
- elle est la même sur toutes les machines. C'est elle qui permet d'avoir une seule unit Jeu.
Sa limite : elle suppose un décor rangé dans une grille. Pour un décor de formes libres, il faut autre chose.
2. Lire l'écran
Sur un écran bitmap, on peut regarder la couleur du point vers lequel la balle avance.
Sur Amstrad CPC, gfx.getPixel rend le pen d'un point :
// le pen du point juste au-dessus de la balle : 0, rien ; sinon, une brique ou un mur
ubyte devant = gfx.getPixel(.X0 + x + 1, .Y0 + y - 1);
C'est tentant, mais la couleur ne dit pas quelle brique est touchée : pour l'effacer, il faut de toute façon calculer sa colonne et sa rangée. Il faut aussi que chaque chose ait sa couleur (une brique orange et le texte orange se confondent), et lire un point coûte plus cher que lire une case de tableau.
Sur les machines à tuiles, l'équivalent serait de lire la carte de tuiles. Mais elle est
rangée dans la mémoire vidéo, que le processeur ne lit qu'à travers les ports du circuit
vidéo (PC Engine, NES, Mega Drive), parfois seulement pendant le retour vertical (NES,
Game Boy). La solution de l'époque était de garder une copie de la carte en
mémoire vive… c'est exactement notre tableau briques.
3. Les collisions matérielles
Certains circuits vidéo détectent eux-mêmes les contacts entre sprites, ou entre un
sprite et le décor. Sur Commodore 64, le VIC-II met à 1, pour chaque sprite, un bit dans
$D01E (contact avec un autre sprite) et dans $D01F (contact avec
le décor) :
ubyte contact = readMem(0xD01F); // un bit par sprite ; la lecture remet à 0
if ((contact & (1 << .spriteBalle)) != 0) {
// la balle a touché un caractère du décor
}
| Machine | Collisions matérielles |
|---|---|
| Commodore 64 | sprite contre sprite, sprite contre décor, un bit par sprite |
| Amiga | sprites et plans de l'écran, par paires (registre CLXDAT) ; sprite.hit() compare deux bobs au point près avec le Blitter |
| PC Engine | le sprite 0 contre les autres sprites, un seul bit |
| Mega Drive | deux sprites qui se chevauchent, un seul bit, sans dire lesquels |
| NES, Game Boy, Amstrad CPC | rien d'utilisable pour le jeu |
Elles sont précises au point près, et gratuites pour le processeur. Mais elles arrivent après coup : le circuit signale un chevauchement une fois l'image dessinée, donc quand la balle est déjà dans la brique. Elles ne disent pas non plus quelle brique est touchée, et n'existent pas sur toutes les machines.
Laquelle choisir ?
| Méthode | Pour | Contre |
|---|---|---|
| La grille | rapide, prévoit le contact, dit quelle brique, la même partout | un décor rangé en grille |
| Lire l'écran | marche pour n'importe quelle forme | ne dit pas quel objet ; mémoire vidéo lente ou inaccessible |
| Le matériel | précis au point, gratuit pour le processeur | après coup, rarement le détail, pas partout |
Dans un vrai jeu, on les combine souvent : la grille pour le décor, des rectangles qui se chevauchent pour les objets entre eux, et le test au point près (matériel ou Blitter) seulement quand les rectangles se touchent.
Tuiles, bitmap et sprites
Les règles sont écrites. Reste à dessiner, et c'est là que les machines se séparent.
Les tuiles
Sur une machine à tuiles (Commodore 64 en mode texte, PC Engine, NES, Game Boy, Mega Drive), l'écran n'est pas une grille de points mais une grille de cases de 8 × 8. Chaque case contient le numéro d'un motif, dessiné une fois pour toutes en mémoire vidéo. Pour afficher une brique de 16 × 8, on écrit deux numéros. Pour l'effacer, deux autres. Le circuit vidéo fait le reste à chaque image.
C'est économique, et parfaitement adapté à un décor en grille comme un mur de briques. La contrainte : tout ce qui est dans le décor doit tomber sur la grille des cases.
Le bitmap
Sur une machine bitmap (Amstrad CPC, Amiga), chaque point de l'écran a sa place en mémoire. On peut dessiner n'importe quoi, n'importe où : des courbes, des polygones, des dégradés. Mais chaque point se paie : une brique de 16 × 8, ce sont 128 points à écrire.
L'Amiga compense par son Blitter, un circuit qui remplit et copie des rectangles pendant que le processeur fait autre chose. L'Amstrad CPC n'a que son Z80 : c'est le processeur qui écrit chaque octet.
Les sprites matériels
Un sprite est un petit dessin que le circuit vidéo superpose à l'écran, à la position qu'on lui donne. Pour le déplacer, on change deux nombres. Rien à effacer, rien à redessiner, et le décor dessous reste intact. C'est idéal pour ce qui bouge : la balle et la raquette.
Chaque machine a ses limites : 8 sprites de 24 × 21 sur Commodore 64, 64 sprites de 8 × 8 sur NES, 40 objets sur Game Boy, 64 sprites jusqu'à 32 × 64 sur PC Engine, 80 sur Mega Drive, 8 sprites de 16 points de large sur Amiga. Une raquette de 24 points demande donc un sprite sur Commodore 64, trois sur NES et Game Boy, deux sur Amiga.
Les sprites logiciels
L'Amstrad CPC n'a pas de sprites. Il faut les imiter : à chaque image, effacer l'ancienne place, puis dessiner la nouvelle. C'est ce qu'on appelle un sprite logiciel.
Sur un fond noir, effacer revient à repeindre en noir. Sur un décor dessiné, il
faudrait d'abord sauver ce qui est sous le sprite, pour le remettre
ensuite : c'est ce que font les « bobs » de l'Amiga (video.sprite), avec
l'aide du Blitter.
Machine par machine
Chaque Ecran.s8u fait entre 87 et 161 lignes. Voici, pour chaque machine,
ce qui a guidé ses choix.
Commodore 64
- Briques
- deux espaces en inversion vidéo : deux caractères pleins
- Couleurs
- une par case (la mémoire couleur) : un damier de deux teintes par rangée
- Balle, raquette
- deux sprites matériels de 24 × 21
- Texte
- la police du Commodore, directement
- Sources
- tous les fichiers du projet
Le mode texte du C64 est déjà une machine à tuiles : 40 × 25 cases, chacune avec sa couleur. Un espace en inversion vidéo est un carré plein de la couleur du texte. Une brique, ce sont donc deux espaces inversés, sans dessiner un seul motif :
// Une brique = deux espaces en inversion vidéo, de la couleur de sa rangée
void dessinerBriques() {
for (ubyte l = 0; l < 5; l++) {
for (ubyte c = 0; c < 10; c++) {
if (((c + l) & 1) == 0) {
vic.setFontColor(.COULEUR_A[l]);
} else {
vic.setFontColor(.COULEUR_B[l]);
}
vic.setCursor((ubyte)(.COL0 + c * 2), (ubyte)(.LIG0 + 2 + l));
vic.setInvertOn();
vic.print(" ");
vic.setInvertOff();
}
}
}
La balle et la raquette sont des sprites : deux appels à moveSprite par
image, et c'est tout.
// Les sprites suivent le jeu : deux appels, rien à effacer
void placer(ubyte raquette, ubyte x, ubyte y) {
sprite.moveSprite(.spriteRaquette, .X0 + raquette, .Y0 + Jeu.RAQUETTE_Y);
sprite.moveSprite(.spriteBalle, .X0 + x, .Y0 + y);
}
PC Engine
- Briques
- des tuiles pleines de la BAT
- Couleurs
- une palette de tuiles par couleur de brique (1 à 10), une pour les murs
- Balle, raquette
- deux sprites, de 16 × 16 et 32 × 16
- Texte
- une police chargée en mémoire vidéo, depuis une ressource
- Sources
- tous les fichiers du projet
La PC Engine a 16 palettes de 16 couleurs pour les tuiles : on peut s'offrir une palette
par couleur de brique, et choisir la couleur d'une brique en choisissant sa palette. Elle
n'a pas de police en ROM : police.bin est ajoutée par
addResource et copiée en mémoire vidéo au démarrage.
Ses sprites lisent leurs motifs en mémoire vidéo : on les y copie, puis on crée les sprites en donnant l'adresse du motif et leur taille.
// les motifs des sprites, copiés en mémoire vidéo
vdc.writeToVRAM(@.MOTIF_BALLE, .VRAM_BALLE, 128);
vdc.writeToVRAM(@.MOTIF_RAQUETTE, .VRAM_RAQUETTE, 256);
sprite.initSprite();
sprite.setSpritePriority(sprite.SPRITE_PRIORITY_FG);
sprite.setSpriteSize(sprite.SPRITE_16x16);
sprite.setSpritePalette(0);
.spriteBalle = sprite.createSprite(0, -64, .VRAM_BALLE);
sprite.setSpriteSize(sprite.SPRITE_32x16);
sprite.setSpritePalette(1);
.spriteRaquette = sprite.createSprite(0, -64, .VRAM_RAQUETTE);
NES
- Briques
- six tuiles dessinées : deux moitiés de brique en trois couleurs
- Couleurs
- une palette de 3 couleurs par bloc de 32 × 32 points
- Balle, raquette
- 4 sprites de 8 × 8 : 1 pour la balle, 3 pour la raquette
- Texte
- la police de
ppu.screentext - Sources
- tous les fichiers du projet
La NES impose la contrainte la plus forte : la palette ne se choisit pas par case, mais par bloc de 32 × 32 points (4 × 4 cases). Quatre rangées de briques partagent donc la même palette de 3 couleurs. L'astuce : mettre la couleur dans la tuile elle-même (une tuile de couleur 1, une de couleur 2, une de couleur 3), et aligner le terrain sur les blocs pour que les murs aient les leurs.
// une palette par bloc de 32 x 32 points : les blocs des briques
for (byte bx = 2; bx < 7; bx++) {
ppu.setAreaPalette(bx, 2, 1);
ppu.setAreaPalette(bx, 3, 2);
}
Autre contrainte : on n'écrit dans la carte de tuiles que pendant le retour vertical.
Pour poser les 100 tuiles du mur d'un coup, dessinerBriques() éteint l'écran
(ppu.off()) ; en cours de jeu, ppu.putTile met les tuiles en
attente, et ppu.waitFrame() les pose au retour vertical.
Game Boy
- Briques
- six tuiles dessinées, d'un gris par rangée
- Couleurs
- 4 gris en tout : le blanc du fond, le noir du texte, deux gris
- Balle, raquette
- 4 objets de 8 × 8
- Texte
- une police générée par
tools/police.py(Police.s8u) - Sources
- tous les fichiers du projet
L'écran fait exactement 160 × 144 points : le terrain occupe tout l'écran, le score s'écrit sur la première ligne. Avec 4 gris, les rangées se distinguent par leur teinte, sans plus.
Ici, les objets (les sprites de la Game Boy) ne s'écrivent pas directement : le programme remplit une copie de leur table en mémoire vive, et le DMA la recopie d'un coup pendant le retour vertical.
// Un objet dans la copie de la table : y + 16, x + 8, tuile, attributs
void objet(ubyte n, ubyte x, ubyte y, ubyte tuile) {
ubyte i = (ubyte)(n * 4);
oam.shadow[i] = (ubyte)(y + 16);
oam.shadow[i + 1] = (ubyte)(x + 8);
oam.shadow[i + 2] = tuile;
oam.shadow[i + 3] = 0;
}
// Attendre le retour vertical, puis y recopier les objets par le DMA
void attendreImage() {
ubyte avant = Main.images;
while (Main.images == avant) {
}
oam.flush();
}
Mega Drive
- Briques
- les tuiles de la Game Boy, converties en 16 couleurs
- Couleurs
- 4 jeux de 3 couleurs dans la palette 0
- Balle, raquette
- deux sprites : 8 × 8, et 24 × 8 (trois tuiles)
- Texte
- la police de
vdp.loadFont - Sources
- tous les fichiers du projet
La Mega Drive a des tuiles de 16 couleurs (4 bits par point). Plutôt que de redessiner les
briques, on reprend les tuiles de la Game Boy (2 bits par point) :
vdp.loadTiles2bpp les convertit en décalant leurs couleurs.
Les mêmes 6 tuiles, chargées 4 fois avec un décalage de 0, 4, 8 et 12, donnent 4 jeux de
couleurs :
for (ubyte jeu = 0; jeu < 4; jeu++) {
vdp.loadTiles2bpp(@.TUILES_BRIQUES, (uint)(.BRIQUES + jeu * 6), 6, (ubyte)(jeu * 4), 0);
}
Ses sprites forment une liste chaînée : chacun indique le suivant. La balle (0) mène à la raquette (1), qui termine la liste. Un sprite de la Mega Drive peut faire jusqu'à 32 × 32 points : la raquette tient en un seul.
// Deux sprites chaînés : la balle (0), puis la raquette (1), la fin de la liste
void placer(ubyte raquette, ubyte x, ubyte y) {
vdp.setSprite(0, .X0 + x, .Y0 + y, 0, 1, vdp.cell(.BALLE, 2, 0));
vdp.setSprite(1, .X0 + raquette, .Y0 + Jeu.RAQUETTE_Y, 8, 0, vdp.cell(.RAQUETTE, 2, 0));
}
Amstrad CPC
- Briques
- des rectangles pleins,
gfx.setFillRect - Couleurs
- 4 pens en mode 1 : noir, rouge, orange, blanc
- Balle, raquette
- des sprites logiciels : effacer, puis redessiner
- Texte
gfx.setText, sur une zone d'abord repeinte en noir- Sources
- tous les fichiers du projet
Le CPC est la machine la plus dépouillée : un écran bitmap, et c'est tout. En mode 1 (320 × 200, 4 couleurs), il reste deux couleurs pour les briques, une fois le noir et le blanc pris. La balle et la raquette sont des sprites logiciels, et on ne les redessine que si elles ont bougé :
// Les sprites logiciels : effacer l'ancienne place, dessiner la nouvelle
void placer(ubyte raquette, ubyte x, ubyte y) {
int ry = .Y0 + Jeu.RAQUETTE_Y;
if (raquette != .ancienneRaquette) {
if (.ancienneRaquette >= 0) {
gfx.setPen(.NOIR);
gfx.setFillRect(.X0 + .ancienneRaquette, ry, .X0 + .ancienneRaquette + 23, ry + 3);
}
gfx.setPen(.BLANC);
gfx.setFillRect(.X0 + raquette, ry, .X0 + raquette + 23, ry + 3);
.ancienneRaquette = raquette;
}
if (x != .ancienneBalleX || y != .ancienneBalleY) {
if (.ancienneBalleX >= 0) {
gfx.setPen(.NOIR);
gfx.setFillRect(.X0 + .ancienneBalleX, .Y0 + .ancienneBalleY,
.X0 + .ancienneBalleX + 3, .Y0 + .ancienneBalleY + 3);
}
if (y < Jeu.HAUTEUR) {
gfx.setPen(.BLANC);
gfx.setFillRect(.X0 + x, .Y0 + y, .X0 + x + 3, .Y0 + y + 3);
}
.ancienneBalleX = x;
.ancienneBalleY = y;
}
}
Même le texte demande de l'attention : en bitmap, écrire un chiffre par-dessus un autre superpose les deux. On repeint donc la zone du score en noir avant de l'écrire.
Amiga
- Briques
- des rectangles posés par le Blitter, avec une ligne de reflet
- Couleurs
- 16 couleurs (4 plans), chacune choisie parmi 4 096
- Balle, raquette
- trois sprites matériels : 1 pour la balle, 2 pour la raquette
- Texte
- une police de 3 × 5 points, dessinée en petits carrés
- Sources
- tous les fichiers du projet
L'Amiga réunit le meilleur des deux mondes : un écran bitmap, que le Blitter remplit vite, et de vrais sprites. Avec 16 couleurs, chaque brique peut avoir une ligne plus claire en haut, un reflet. Les sprites sont décrits par une liste en mémoire : leurs lignes sont écrites une fois, au démarrage, puis seuls leurs deux mots de contrôle (leur position) changent à chaque image.
// Les sprites : seulement leurs mots de contrôle
void placer(ubyte raquette, ubyte x, ubyte y) {
int rx = .X0 + raquette;
ocs.spritePosition(ocs.sprite(0, 0), .X0 + x, (uint)(.Y0 + y), 4, 0);
ocs.spritePosition(ocs.sprite(0, 2), rx, (uint)(.Y0 + Jeu.RAQUETTE_Y), 4, 0);
ocs.spritePosition(ocs.sprite(0, 3), rx + 16, (uint)(.Y0 + Jeu.RAQUETTE_Y), 4, 0);
}
Sans le système d'exploitation, l'Amiga n'a pas de police à portée de main. Qu'à cela ne tienne : 5 rangées de 3 points par caractère, chaque point dessiné comme un carré de 2 × 2 :
// Un caractère de la police, en carrés de 2 x 2 ; l'espace ne dessine rien
void lettre(int x, int y, ubyte c) {
if (c < '0' || c > 'Z') {
return;
}
uint g = (uint)(c - '0') * 5;
for (ubyte r = 0; r < 5; r++) {
ubyte bits = .POLICE[g + r];
for (ubyte k = 0; k < 3; k++) {
if ((bits & (4 >> k)) != 0) {
bitmap.rect(x + (int)k * 2, y + (int)r * 2, 2, 2, .TEXTE);
}
}
}
}
Le bilan
| Machine | Briques | Balle et raquette | Ecran.s8u | Programme |
|---|---|---|---|---|
| Commodore 64 | caractères | 2 sprites | 102 lignes | 5,4 Ko (.prg) |
| PC Engine | tuiles | 2 sprites | 125 lignes | HuCard de 16 Ko |
| NES | tuiles | 4 sprites | 116 lignes | cartouche de 32 Ko |
| Game Boy | tuiles | 4 objets | 87 lignes | cartouche de 32 Ko |
| Mega Drive | tuiles | 2 sprites | 115 lignes | cartouche de 128 Ko |
| Amstrad CPC | bitmap | sprites logiciels | 111 lignes | 7,9 Ko (sur disquette) |
| Amiga | bitmap (Blitter) | 3 sprites | 161 lignes | 67 Ko (disquette ADF) |
Les règles, elles, font 236 lignes, écrites une seule fois. Une cartouche a une taille standard : le jeu lui-même en occupe bien moins.
Ce que montre ce projet. Simple8 ne cache pas les machines : sur chacune, on a utilisé ce qu'elle fait de mieux, les tuiles là où il y en a, les sprites là où il y en a, le Blitter sur Amiga. Ce que Simple8 apporte, c'est un langage commun pour la partie qui ne dépend pas du matériel. Et dans un jeu, c'est souvent la plus grosse.
Pour aller plus loin
Le jeu est volontairement minimal. Voici des pistes, de la plus simple à la plus ambitieuse :
- Des briques solides : une valeur 2 dans
briquespour une brique qu'il faut toucher deux fois. Jeu décrémente la case, Ecran change sa couleur. - Des niveaux : une table de 50 octets par niveau, recopiée dans
briquesau début du niveau. Ils peuvent être dessinés dans SquareDot. - Une balle plus fine : des positions en seizièmes de point (comme dans
l'exemple
BobsAmiga), pour des angles plus variés et une vitesse qui augmente doucement. - Du son : un « bip » au rebond, par le SID sur Commodore 64, par
sound.paulasur Amiga. Il s'ajoute dans Ecran, avec une neuvième fonction au contrat. - Des bonus : une capsule qui tombe d'une brique cassée est un sprite de plus ; l'attraper, c'est un test de rectangles avec la raquette.
À retenir
- Séparez les règles (une unit commune, sans ligne
architecture) de l'affichage (une unit par machine, avec les mêmes fonctions). - Un jeu avance d'un pas par image :
attendreImage()donne le rythme, et les vitesses se comptent en points par image. - Une grille en mémoire et des puissances de deux donnent des collisions rapides, qui prévoient le contact et marchent partout.
- Avancez un axe à la fois, et ne testez que le bord qui avance.
- Les tuiles coûtent quelques octets par brique, le bitmap des dizaines ; les sprites matériels se déplacent en changeant deux nombres, les sprites logiciels s'effacent et se redessinent.
À vous de jouer
- Changez les valeurs de
POINTS[]et deVISEES[], et regardez la démo jouer autrement sur deux machines.Voir la solutionMasquer la solution
Les deux tables sont dans
Jeu.s8u: une seule modification vaut pour les sept machines. On a essayé trois réglages, compilés pour Commodore 64 et PC Engine, et laissé la démo jouer seule trois minutes dans les émulateurs du kit de test.POINTS[]: les rangées du bas valent plusconst uint POINTS[] = { 10, 20, 30, 40, 50 }; // les rangées du bas valent plusLa démo joue exactement la même partie : le premier mur tombe à 98,1 s sur C64 et 81,6 s sur PC Engine, comme avec les valeurs d'origine.
POINTS[]ne sert qu'au score. Seul le score en cours change (3 200 au lieu de 3 040 sur C64 après 3 minutes) ; un mur complet, lui, rapporte toujours 1 500 points, puisque la somme des cinq valeurs est la même.VISEES[]: viser toujours le milieuconst byte VISEES[] = { 0, 0, 0, 0, 0, 0 };La raquette se place sous la balle, qui repart toujours avec le même petit angle. Le début est plus lent (830 points à 40 s sur C64, contre 1 170), mais la fin bien plus rapide : le premier mur tombe à 69,1 s au lieu de 98,1 s sur C64, à 57,5 s au lieu de 81,6 s sur PC Engine. Avec les valeurs d'origine, les dernières briques sont longues à trouver ; ici, la balle les atteint mieux. La démo est donc moins variée, mais pas forcément moins efficace : il faut le mesurer pour le savoir.
VISEES[]: viser les extrémités de la raquetteconst byte VISEES[] = { -11, 11, -11, 11, -11, 11 };Cette fois, la démo perd sans arrêt : une vie toutes les 3 à 7 secondes sur les deux machines, et presque aucun point. La balle ne touche plus que les derniers points de la raquette, et la cible saute de 22 points d'un rebond à l'autre ; or la raquette avance de 2 points par image, pas plus vite qu'une balle lancée en biais : elle arrive souvent trop tard.
Deux règles à retenir. Une visée doit rester dans la raquette : entre -11 et 11, puisque la raquette fait 24 points. Et la table doit garder 6 valeurs :
rebondRaquette()remetviseeà 0 quand il atteint 6. Pour une autre taille, il faut changer ce 6 aussi.
La démo sur C64 (VICE), environ 40 secondes après le démarrage : les visées d'origine, puis toutes à 0, puis aux extrémités de la raquette. - Ajoutez des briques solides, qui demandent deux coups.
Voir la solutionMasquer la solution
Le tableau
briquesne dit plus seulement si une brique est là : il compte les coups qu'il faut encore pour la casser. 0, elle est cassée ; 1, une brique normale ; 2, une brique solide. Le reste deJeucontinue de marcher sans changement :briqueEn()teste toujoursbriques[n] == 0, etrestantescompte toujours des briques, pas des coups. Ici, la rangée du haut est solide ; une petite table donne la solidité de chaque rangée.Dans
Jeu.s8uconst ubyte SOLIDITE[] = { 2, 1, 1, 1, 1 }; // les coups pour casser une briqueLes deux boucles qui remplissaient le mur (dans
nouvellePartie()et dansgagne()) mettaient 1 partout. Elles sont remplacées par un appel à une nouvelle fonction,remplirMur():// Un mur neuf : chaque brique reçoit la solidité de sa rangée void remplirMur() { ubyte n = 0; for (ubyte l = 0; l < .LIGNES; l++) { for (ubyte c = 0; c < .COLONNES; c++) { .briques[n] = .SOLIDITE[l]; n++; } } .restantes = 50; }Dans
toucher(), on enlève un coup. S'il en reste, la brique reste debout : pas de points, la balle rebondit (on rend 1), etEcranchange sa couleur.ubyte toucher(int x, int y) { ubyte n = briqueEn(x, y); if (n == 255) { return 0; } .briques[n]--; // un coup de moins if (.briques[n] != 0) { // une brique solide, encore debout : elle change de couleur, la balle rebondit Ecran.fissurerBrique(.colonneTouchee, .ligneTouchee); return 1; } .restantes--; .score = .score + .POINTS[.ligneTouchee]; Ecran.effacerBrique(.colonneTouchee, .ligneTouchee); majInfos(); if (.restantes == 0) { gagne(); } return 1; }Jeuappelle maintenant une neuvième fonction d'Ecran,fissurerBrique(c, l). Le contrat s'allonge : chaqueEcran.s8udoit la définir, sinon le programme de cette machine ne compile plus (Function 'fissurerBrique' not found !, vérifié sur la version Amstrad CPC). Il faut aussi quedessinerBriques()dessine les briques solides d'une autre couleur : elle litJeu.briques, ce qui est permis, puisqueJeuest ajouté avantEcran.Sur Commodore 64 : une brique solide est blanche ; touchée une fois, elle reprend la couleur de sa rangée.
// Une brique = deux espaces en inversion vidéo ; une brique solide est blanche void dessinerBriques() { ubyte n = 0; for (ubyte l = 0; l < 5; l++) { for (ubyte c = 0; c < 10; c++) { if (Jeu.briques[n] == 2) { brique(c, l, vic.WHITE); } else { fissurerBrique(c, l); } n++; } } } // Une brique de la couleur de sa rangée, en damier void fissurerBrique(ubyte c, ubyte l) { if (((c + l) & 1) == 0) { brique(c, l, .COULEUR_A[l]); } else { brique(c, l, .COULEUR_B[l]); } } void brique(ubyte c, ubyte l, ubyte couleur) { vic.setFontColor(couleur); vic.setCursor((ubyte)(.COL0 + c * 2), (ubyte)(.LIG0 + 2 + l)); vic.setInvertOn(); vic.print(" "); vic.setInvertOff(); }Sur PC Engine : la même chose avec une palette de plus, la 12 (les briques utilisent les palettes 1 à 10, les murs la 11), réglée dans
init():vce.setPalette(12, 1, RGB(7, 7, 7)); // les briques solides : blanches// Une brique = deux tuiles pleines (des espaces en inversion), dans la palette de sa couleur ; // une brique solide prend la palette 12 void dessinerBriques() { ubyte n = 0; for (ubyte l = 0; l < 5; l++) { for (ubyte c = 0; c < 10; c++) { if (Jeu.briques[n] == 2) { brique(c, l, 12); } else { fissurerBrique(c, l); } n++; } } } // Une brique dans la palette de sa rangée, en damier void fissurerBrique(ubyte c, ubyte l) { brique(c, l, (ubyte)(1 + l * 2 + ((c + l) & 1))); } void brique(ubyte c, ubyte l, ubyte numero) { vdc.setFontColor(numero); vdc.setCursor((ubyte)(.COL0 + c * 2), (ubyte)(.LIG0 + 2 + l)); vdc.setInvertOn(); vdc.print(" "); vdc.setInvertOff(); }Attention à un piège : le paramètre de
brique()ne peut pas s'appelerpalette. C'est un mot réservé de Simple8 (il sert aux ressources), et le compilateur s'arrête sur une erreur de syntaxe :Encountered " "palette" "palette "". Il s'appelle doncnumero.Les deux versions compilent et jouent. Dans la démo, au bout de 40 secondes, on voit des briques du haut redevenues rouges ou roses (touchées une fois) et d'autres disparues (touchées deux fois). Le jeu est plus long : après 4 minutes, la démo n'a pas encore fini le premier mur (1 450 points sur les deux machines ; il reste une brique, déjà touchée une fois), alors qu'elle le finit en 98 s sur C64 et 82 s sur PC Engine sans briques solides.

Sur PC Engine, après 5 s, 40 s et 4 minutes : la rangée blanche est solide ; une brique touchée une fois reprend la couleur de sa rangée. 
Sur Commodore 64 (VICE), environ 40 secondes après le démarrage. - Sur Amstrad CPC, remplacez le test de la grille par
gfx.getPixel. Que faut-il quand même calculer pour effacer la bonne brique ?Voir la solutionMasquer la solution
Jeune doit rien savoir de la machine : il ne peut pas appelergfx.getPixellui-même. La lecture de l'écran va donc dans l'Ecrandu CPC, sous la forme d'une petite fonction qui dit si un point du terrain a la couleur d'une brique. On ne regarde que les deux pens des briques : le blanc des murs, de la raquette, de la balle et du texte n'est pas une brique.// 1 si le point (x, y) du terrain a la couleur d'une brique : on lit l'écran ubyte briqueVisible(int x, int y) { ubyte pen = gfx.getPixel(.X0 + x, .Y0 + y); if (pen == .BRIQUE_A || pen == .BRIQUE_B) { return 1; } return 0; }Dans
briqueEn(), cette lecture remplace le test de la casebriques[n]. Le test de la zone des briques, au début, est gardé : il ne coûte que quatre comparaisons, et évite de lire l'écran quand la balle est loin des briques, c'est-à-dire presque toujours.// La brique sous le point (x, y), ou 255 s'il n'y en a pas ubyte briqueEn(int x, int y) { ubyte n; if (y < .HAUT_BRIQUES || y >= .BAS_BRIQUES || x < 0 || x >= .LARGEUR) { return 255; } // l'écran dit s'il y a une brique... if (Ecran.briqueVisible(x, y) == 0) { return 255; } // ... mais pas laquelle : sa colonne et sa rangée se calculent toujours .colonneTouchee = (ubyte)(x >> 4); // x / 16 .ligneTouchee = (ubyte)((y - .HAUT_BRIQUES) >> 3); // (y - 16) / 8 n = (ubyte)(.DEBUT_LIGNE[.ligneTouchee] + .colonneTouchee); return n; }Voici la réponse à la question : la couleur dit qu'il y a une brique, pas laquelle. Il faut quand même calculer sa colonne (
x >> 4) et sa rangée ((y - 16) >> 3). La colonne et la rangée servent àEcran.effacerBrique(c, l), qui repeint la bonne brique en noir, et la rangée donne les points (POINTS[ligneTouchee]). Le numéronsert encore àtoucher(), qui met la case à 0 : le tableau n'est plus lu pour les collisions, mais il reste juste.Cette version modifie
Jeu.s8uet appelle une fonction que seul l'Ecrandu CPC possède : elle ne compilerait plus pour les autres machines. C'est le prix de cette méthode : elle lie les règles à l'écran d'une machine.Vérifié dans l'émulateur du kit de test : la démo joue normalement, finit son premier mur en 108,5 s (104,3 s avec la grille : la trajectoire change un peu, sans doute parce que l'écart d'un point entre deux briques est noir, alors que la grille le compte dans la brique). Et la lecture coûte plus cher : un appel de
briqueEn()qui passe le test de zone prend en moyenne 2 136 états au lieu de 1 192 avec la grille, mesuré sur les 30 premières secondes.
La version avec gfx.getPixel, après 30 secondes de démo. - Choisissez une machine et ajoutez un son au rebond, sans toucher à
Jeu.s8uplus que l'appel lui-même.Voir la solutionMasquer la solution
Le Commodore 64 est la machine la plus simple pour un « bip » : son circuit sonore, le SID, joue une note tout seul une fois réglé, et la bibliothèque
sid.basefournit tout le nécessaire. Il suffit de lui donner une forme d'onde, une enveloppe (comment le son monte, tient et s'éteint), une note, puis d'ouvrir et de fermer la « porte » (le gate) de la voix.Dans
Jeu.s8u, une seule ligne s'ajoute, dansrebondRaquette(), quand la balle repart :.dy = -2; .balleY = .RAQUETTE_Y - .BALLE; Ecran.bip(); // le son du rebond : chaque machine le joue à sa façon .visee++;Tout le reste est dans l'
Ecrandu C64. Dansmain.s8p, la bibliothèque du SID :addlib sid.base; // le son : le SIDDans
Ecran.s8u, une variable, la fin deinit(), et deux fonctions :ubyte dureeBip; // images avant la fin du bip, 0 : silence // … void init() { // … // le son : la voix 0 du SID, une onde carrée qui part tout de suite sid.init(); sid.setWaveform(0, sid.PULSE); sid.setPulseWidth(0, 0x800); sid.setEnvelope(0, 0x00, 0xF6); // attaque 0, déclin 0, tenue 15, relâche 6 } // … void attendreImage() { irq.waitFrame(); if (.dureeBip > 0) { .dureeBip--; if (.dureeBip == 0) { sid.noteOff(0); // fin du bip : le son s'éteint (la relâche) } } } // Un bip court : la note part, attendreImage() l'arrête trois images plus tard void bip() { sid.setNote(0, 60); // C-5 (do 5) sid.noteOn(0); .dureeBip = 3; }Pourquoi ne pas fermer la porte tout de suite dans
bip()? Le SID ne relance une note que si la porte passe de fermée à ouverte ; il faut aussi que le son dure assez pour s'entendre.bip()ouvre donc la porte, et c'estattendreImage(), déjà appelée à chaque image, qui la referme trois images plus tard. Le son s'éteint alors en douceur (la relâche), et la porte est fermée pour le rebond suivant.Ajouter une fonction appelée par
Jeu, c'est allonger le contrat : chaqueEcran.s8udoit maintenant avoir unbip(), sinon sa machine ne compile plus (Function 'bip' not found !, obtenu sur la version Amstrad CPC). Une fonction vide suffit sur les machines qui restent muettes :// Pas encore de son sur CPC : la fonction existe, elle ne fait rien void bip() { }Vérifié de deux façons. Dans l'émulateur du kit de test, qui note les écritures dans le SID : en 60 secondes de démo, 28 rebonds sur la raquette et 28 ouvertures de la porte de la voix 0 (
$D404 = $41), chacune refermée 3 images plus tard ($D404 = $40, 0,058 s après). Et dans VICE, en enregistrant le son dans un fichier WAV : un son d'environ un dixième de seconde à chaque rebond (à 2,96 s, 4,42 s, 5,88 s, 7,32 s…), de fréquence proche de 520 Hz, celle du do 5 (523 Hz).
Vous avez maintenant tout ce qu'il faut pour écrire vos propres jeux. Choisissez une machine, ouvrez Simple8 IDE, et lancez-vous !
