Simple8

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.

Le casse-briques sur Amiga
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.

Examples/commodore/amiga/CasseBriques_Amiga/main.s8pLe programme principal : les bibliothèques de la machine, les deux units, main().22 lignes
// 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();
    }
}
Examples/commodore/amiga/CasseBriques_Amiga/Ecran.s8uL'affichage de cette machine : les huit fonctions du contrat de l'unit Ecran.198 lignes
// 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);
    }
}
Examples/common/CasseBriques/Jeu.s8uLes règles du jeu, les mêmes sur les sept machines. Ce fichier n'existe qu'une fois.310 lignes
// 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();
    }
}
Examples/commodore/amiga/CasseBriques_Amiga/simple8-emulators.jsonL'émulateur que Simple8 IDE lance pour ce projet (FS-UAE, Amiga 500 PAL).22 lignes
{
  "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.