Casse-briques sur Amiga
Le meilleur des deux mondes : un bitmap que le Blitter remplit vite, et de vrais sprites, dont seuls les mots de contrôle changent à chaque image. La police, absente sans le système, est dessinée en petits carrés.
- Dossier
Examples/commodore/amiga/CasseBriques_Amiga/- Architecture
commodore.amiga- Briques
- rectangles posés par le Blitter, avec un reflet
- Balle, raquette
- 3 sprites matériels
- Fichiers
- 4, dont les règles communes
- Explications
- le chapitre 14, machine par machine
Les sources
Tous les fichiers du projet, tels qu'ils sont dans le dépôt. Les onglets passent d'un fichier à l'autre ; l'adresse de la page garde le fichier ouvert, pour le partager.
main().// Casse-briques — Amiga 500
//
// Les règles sont dans Examples/common/CasseBriques/Jeu.s8u, les mêmes sur toutes les machines.
// Ici, l'affichage de l'Amiga : les briques en bitmap, posées par le Blitter, la balle et la
// raquette en sprites matériels.
// Joystick du port 2 (ou les flèches et l'espace) : gauche, droite, bouton pour lancer la
// balle. Sans joystick, la démo joue.
architecture commodore.amiga;
addlib system.system;
addlib video.ocs;
addlib video.bitmap;
addlib input.controls;
addunit "../../../common/CasseBriques/Jeu.s8u"; // les règles, communes
addunit "Ecran.s8u"; // l'affichage de l'Amiga
unit {
main() {
Jeu.jouer();
}
}// Ecran.s8u : l'affichage du casse-briques sur Amiga 500.
//
// L'Amiga a un écran bitmap (320 x 256, ici jusqu'à 32 couleurs), un Blitter qui y remplit des
// rectangles bien plus vite que le processeur, et 8 sprites matériels de 16 points de large :
// - les briques et les murs sont des rectangles posés par le Blitter (video.bitmap) ;
// - la balle et la raquette sont des sprites matériels : rien à effacer, il suffit de
// réécrire leurs deux mots de contrôle (leur position) à chaque image ;
// - l'Amiga n'a pas de police en ROM utilisable sans le système : le texte est dessiné avec
// une petite police de 3 x 5 points, chaque point étant un carré de 2 x 2.
// On dessine toujours dans la page 0 de video.ocs, celle qui est montrée : sans page qui
// bouge, pas besoin de double tampon.
unit Ecran {
// Le terrain de 160 x 144 points, au milieu de l'écran
const int X0 = 80;
const int Y0 = 64;
// Les couleurs : 0 à 15 pour l'écran, 17 pour le sprite 0, 21 pour les sprites 2 et 3
const ubyte NOIR = 0;
const ubyte MUR = 1;
const ubyte TEXTE = 2;
const ubyte BRIQUE = 3; // 3 à 7 : une couleur par rangée
const ubyte REFLET = 8; // 8 à 12 : la même, plus claire, pour le haut de la brique
const uint PALETTE[32] = {
0x002, 0x669, 0xFFF, 0xD22, 0xE70, 0xDC1, 0x2B3, 0x37E,
0xF88, 0xFB6, 0xFF8, 0x8F8, 0x9CF, 0x000, 0x000, 0x000,
0x000, 0xFFF, 0x000, 0x000, 0x000, 0xFFF, 0x000, 0x000,
0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000
};
// La police : 5 rangées de 3 points par caractère, de '0' à 'Z'
const ubyte POLICE[] = {
7, 5, 5, 5, 7, // 0
2, 6, 2, 2, 7, // 1
7, 1, 7, 4, 7, // 2
7, 1, 3, 1, 7, // 3
5, 5, 7, 1, 1, // 4
7, 4, 7, 1, 7, // 5
7, 4, 7, 5, 7, // 6
7, 1, 2, 2, 2, // 7
7, 5, 7, 5, 7, // 8
7, 5, 7, 1, 7, // 9
0, 0, 0, 0, 0, // :
0, 0, 0, 0, 0, // ;
0, 0, 0, 0, 0, // <
0, 0, 0, 0, 0, // =
0, 0, 0, 0, 0, // >
0, 0, 0, 0, 0, // ?
0, 0, 0, 0, 0, // @
2, 5, 7, 5, 5, // A
6, 5, 6, 5, 6, // B
3, 4, 4, 4, 3, // C
6, 5, 5, 5, 6, // D
7, 4, 6, 4, 7, // E
0, 0, 0, 0, 0, // F
0, 0, 0, 0, 0, // G
0, 0, 0, 0, 0, // H
7, 2, 2, 2, 7, // I
0, 0, 0, 0, 0, // J
0, 0, 0, 0, 0, // K
0, 0, 0, 0, 0, // L
0, 0, 0, 0, 0, // M
0, 0, 0, 0, 0, // N
2, 5, 5, 5, 2, // O
6, 5, 6, 4, 4, // P
0, 0, 0, 0, 0, // Q
6, 5, 6, 5, 5, // R
3, 4, 2, 1, 6, // S
0, 0, 0, 0, 0, // T
5, 5, 5, 5, 7, // U
5, 5, 5, 5, 2, // V
0, 0, 0, 0, 0, // W
0, 0, 0, 0, 0, // X
0, 0, 0, 0, 0, // Y
0, 0, 0, 0, 0 // Z
};
ubyte texteVies[2];
void init() {
ocs.init(@.PALETTE);
blitter.targetOcs(ocs.PAGE0);
bitmap.planes(4); // 16 couleurs suffisent : 4 plans à remplir, pas 6
// les murs : à gauche, à droite et en haut, 8 points d'épaisseur
bitmap.rect(.X0 - 8, .Y0 - 8, 8, 152, .MUR);
bitmap.rect(.X0 + 160, .Y0 - 8, 8, 152, .MUR);
bitmap.rect(.X0, .Y0 - 8, 160, 8, .MUR);
// les sprites : leurs lignes une fois pour toutes (deux mots par ligne : les deux plans
// du sprite, ici la couleur 1 seule), puis deux mots nuls pour finir
ulong at = ocs.spritePosition(ocs.sprite(0, 0), 0, 0, 4, 0);
for (ubyte i = 0; i < 4; i++) {
system.poke32(at + i * 4, 0xF0000000); // la balle : 4 points
}
system.poke32(at + 16, 0);
at = ocs.spritePosition(ocs.sprite(0, 2), 0, 0, 4, 0);
for (ubyte i = 0; i < 4; i++) {
system.poke32(at + i * 4, 0xFFFF0000); // la raquette : 16 points...
}
system.poke32(at + 16, 0);
at = ocs.spritePosition(ocs.sprite(0, 3), 0, 0, 4, 0);
for (ubyte i = 0; i < 4; i++) {
system.poke32(at + i * 4, 0xFF000000); // ... et 8 de plus
}
system.poke32(at + 16, 0);
}
void attendreImage() {
ocs.frame();
}
// Le joystick, rendu comme une manette de Game Boy : A (le bouton) bit 0, droite bit 4,
// gauche bit 5
ubyte manette() {
ubyte p = controls.poll();
ubyte t = 0;
if ((p & 0x20) != 0) {
t = t | Jeu.GAUCHE;
}
if ((p & 0x10) != 0) {
t = t | Jeu.DROITE;
}
if ((p & 0x01) != 0) {
t = t | Jeu.BOUTON;
}
return t;
}
// Une brique : un rectangle de sa couleur, une ligne plus claire en haut, un point de
// vide à droite et en bas pour la séparer de ses voisines
void dessinerBriques() {
bitmap.rect(.X0, .Y0, 160, 144, .NOIR);
for (ubyte l = 0; l < 5; l++) {
for (ubyte c = 0; c < 10; c++) {
int x = .X0 + (int)c * 16;
int y = .Y0 + 16 + (int)l * 8;
bitmap.rect(x, y, 15, 7, .BRIQUE + l);
bitmap.rect(x, y, 15, 1, .REFLET + l);
}
}
}
void effacerBrique(ubyte c, ubyte l) {
bitmap.rect(.X0 + (int)c * 16, .Y0 + 16 + (int)l * 8, 16, 8, .NOIR);
}
// 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);
}
// 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);
}
}
}
}
// Un texte (fini par 0), 8 points par caractère, sur un fond effacé
void ecrire(int x, int y, uint texte) {
ubyte *s = texte;
ubyte n = 0;
while (s[n] != 0) {
n++;
}
bitmap.rect(x, y, (int)n * 8, 10, .NOIR);
for (ubyte i = 0; i < n; i++) {
lettre(x + (int)i * 8, y, s[i]);
}
}
void afficherInfos() {
ecrire(.X0, .Y0 - 24, "SCORE");
ecrire(.X0 + 48, .Y0 - 24, @Jeu.texteScore);
ecrire(.X0 + 104, .Y0 - 24, "VIES");
.texteVies[0] = (ubyte)('0' + Jeu.vies);
.texteVies[1] = 0;
ecrire(.X0 + 144, .Y0 - 24, @.texteVies);
}
void message(uint texte) {
if (texte == 0) {
bitmap.rect(.X0 + 60, .Y0 + 90, 40, 10, .NOIR);
return;
}
ecrire(.X0 + 60, .Y0 + 90, texte);
}
}// Jeu.s8u : les règles du casse-briques, les mêmes sur toutes les machines.
//
// Aucune ligne `architecture` : cette unit est compilée pour la machine du programme. Elle ne
// connaît que l'unit Ecran, que chaque machine écrit à sa façon (tuiles, sprites, bitmap…) :
//
// Ecran.init() préparer l'écran
// Ecran.attendreImage() attendre l'image suivante
// Ecran.manette() les touches : GAUCHE, DROITE, BOUTON
// Ecran.dessinerBriques() dessiner toutes les briques (Jeu.briques)
// Ecran.effacerBrique(c, l) effacer la brique de la colonne c, rangée l
// Ecran.placer(raquette, x, y) placer la raquette et la balle
// Ecran.afficherInfos() écrire le score et les vies (Jeu.texteScore, Jeu.vies)
// Ecran.message(texte) écrire un message au milieu, 0 pour l'effacer
//
// Le terrain fait 160 x 144 points ; chaque machine le place où elle veut sur son écran.
unit Jeu {
// Le terrain
const ubyte LARGEUR = 160;
const ubyte HAUTEUR = 144;
// Les briques : 10 colonnes de 16 points, 5 rangées de 8 points. 16 et 8 sont des
// puissances de deux : la case d'un point se calcule par des décalages (x >> 4, y >> 3).
const ubyte COLONNES = 10;
const ubyte LIGNES = 5;
const ubyte BRIQUE_L = 16;
const ubyte BRIQUE_H = 8;
const ubyte HAUT_BRIQUES = 16; // le haut de la première rangée
const ubyte BAS_BRIQUES = 56; // HAUT_BRIQUES + LIGNES * BRIQUE_H
const ubyte DEBUT_LIGNE[] = { 0, 10, 20, 30, 40 }; // une table plutôt qu'une multiplication
const uint POINTS[] = { 50, 40, 30, 20, 10 }; // les rangées du haut valent plus
// La raquette et la balle
const ubyte RAQUETTE_Y = 132;
const ubyte RAQUETTE_L = 24;
const ubyte RAQUETTE_H = 4;
const ubyte BALLE = 4; // la balle est un carré de 4 points
// Les touches, rendues par Ecran.manette()
const ubyte GAUCHE = 1;
const ubyte DROITE = 2;
const ubyte BOUTON = 4;
// La démo vise un endroit différent de la raquette à chaque rebond : l'angle change, et la
// balle ne tourne pas en rond dans une zone déjà vide
const byte VISEES[] = { -9, 6, -3, 9, -6, 3 };
ubyte briques[50]; // 1 : la brique est là
ubyte restantes; // briques encore debout
ubyte vies;
uint score;
ubyte texteScore[6]; // le score en chiffres, pour Ecran.afficherInfos()
int balleX;
int balleY;
int dx; // la vitesse de la balle, en points par image
int dy;
int raquetteX;
ubyte collee; // 1 : la balle attend sur la raquette
ubyte attente; // images avant le lancement automatique
ubyte inactif; // images sans toucher la manette : la démo prend la main
ubyte visee; // la visée de la démo, dans VISEES
ubyte colonneTouchee; // la brique trouvée par briqueEn()
ubyte ligneTouchee;
// ---------------------------------------------------------------------------------------
// 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);
}
}
// ---------------------------------------------------------------------------------------
// Une partie, une balle
void nouvellePartie() {
for (ubyte n = 0; n < 50; n++) {
.briques[n] = 1;
}
.restantes = 50;
.vies = 3;
.score = 0;
.inactif = 255; // la démo joue tant que personne ne touche la manette
Ecran.dessinerBriques();
majInfos();
nouvelleBalle();
}
void nouvelleBalle() {
.raquetteX = 68; // au milieu : (160 - 24) / 2
.collee = 1;
.attente = 60; // une seconde avant le lancement automatique
coller();
}
// La balle posée sur la raquette, en son milieu
void coller() {
.balleX = .raquetteX + 10;
.balleY = .RAQUETTE_Y - .BALLE;
}
// ---------------------------------------------------------------------------------------
// 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();
}
}
// 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;
}
}
// ---------------------------------------------------------------------------------------
// 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();
}
// 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;
}
}
// ---------------------------------------------------------------------------------------
// Les collisions avec les briques : la grille, sans regarder l'écran
// 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;
}
// 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;
}
// ---------------------------------------------------------------------------------------
// Perdre une balle, gagner, attendre
void ballePerdue() {
.vies--;
majInfos();
if (.vies == 0) {
Ecran.message("PERDU");
pause(150);
Ecran.message(0);
nouvellePartie();
return;
}
nouvelleBalle();
}
void gagne() {
Ecran.message("BRAVO");
pause(150);
Ecran.message(0);
for (ubyte n = 0; n < 50; n++) {
.briques[n] = 1;
}
.restantes = 50;
Ecran.dessinerBriques();
nouvelleBalle();
}
void pause(ubyte images) {
for (ubyte i = 0; i < images; i++) {
Ecran.attendreImage();
}
}
// Le score en chiffres dans texteScore (5 chiffres et le 0 de fin), puis l'affichage
void majInfos() {
uint v = .score;
for (ubyte i = 5; i > 0; i--) {
.texteScore[i - 1] = (ubyte)('0' + v % 10);
v = v / 10;
}
.texteScore[5] = 0;
Ecran.afficherInfos();
}
}{
"defaults": {
"commodore.amiga": "fs-uae-amiga"
},
"emulators": {
"fs-uae-amiga": {
"name": "FS-UAE (Amiga 500 PAL / OCS)",
"command": "fs-uae",
"args": [
"--amiga-model=A500",
"--chip-memory=512",
"--slow-memory=512",
"--ntsc-mode=0",
"--floppy-drive-0=${rom}"
],
"architectures": [
"commodore.amiga"
],
"rom": "${asmDir}/${name}.adf"
}
}
}Compiler et lancer
Dans Simple8 IDE : ouvrez le dossier du projet, puis compilez et lancez. En ligne de commande, depuis la racine du dépôt :
cd Examples/commodore/amiga/CasseBriques_Amiga/
java -jar ../../../../dist/LitleCompilator.jar -l ../../../../libs8 main.s8p
Le compilateur écrit l'assembleur et le programme dans le dossier asm/ du
projet : asm/main.adf, à ouvrir dans un émulateur. Sans manette, la démo joue
toute seule.
Ce programme est pédagogique : ce n'est pas la meilleure façon d'écrire un casse-briques optimisé pour cette machine, mais une façon claire de montrer comment on l'aborde avec Simple8.
