Simple8

Structures de contrôle

Jusqu'ici, les programmes exécutaient leurs instructions l'une après l'autre, de haut en bas. Les structures de contrôle changent ce chemin : elles permettent de choisir ce qui s'exécute, et de répéter des instructions.

Les conditions : if

Si…

if exécute un bloc seulement si une condition est vraie. La condition s'écrit entre parenthèses, le plus souvent avec une comparaison (chapitre 3) :

if (.vies == 0) {
    // ce bloc ne s'exécute que si vies vaut 0
    .partieFinie = 1;
}

… sinon

else ajoute un bloc exécuté quand la condition est fausse. L'un des deux blocs s'exécute, jamais les deux :

if (.vies == 0) {
    .partieFinie = 1;
} else {
    .vies--;
}

… sinon si

Pour plus de deux cas, on enchaîne des else if. Les conditions sont testées dans l'ordre, et seule la première vraie s'exécute :

if (.score >= 2000) {
    .rang = 3;          // or
} else if (.score >= 1000) {
    .rang = 2;          // argent : avec 1500 points, c'est cette branche
} else {
    .rang = 1;          // bronze
}

Une valeur comme condition

Une condition peut aussi être une simple valeur : 0 est faux, toute autre valeur est vraie. C'est pratique pour un indicateur qui vaut 0 ou 1 :

if (.touche) {         // vrai si touche est différent de 0
    .tir = 1;
}

== et non =. Pour comparer, on écrit ==. if (.vies = 0) est refusé par le compilateur : = range une valeur, il ne compare pas.

Combiner des conditions

Les opérateurs logiques assemblent plusieurs conditions en une seule :

OpérateurSe litVrai quand…Exemple
&&etles deux conditions sont vraies.x > 0 && .x < 320
||oul'une au moins est vraie.vies == 0 || .temps == 0
!nonla condition est fausse!(.vies > 0)
// la partie s'arrête si le joueur n'a plus de vies, ou plus de temps
if (.vies == 0 || .temps == 0) {
    .partieFinie = 1;
}

// le joueur est dans l'écran
if (.x >= 0 && .x < 320 && .y >= 0 && .y < 200) {
    .visible = 1;
}

Les parenthèses précisent l'ordre quand on mélange && et || : (.a == 1 || .b == 1) && .c == 1.

Le second test peut être sauté. Dès que le résultat est connu, la suite n'est pas évaluée : dans a && b, si a est faux, b n'est pas calculé ; dans a || b, si a est vrai, non plus. Placer le test le plus simple en premier fait gagner du temps.

Choisir parmi plusieurs valeurs : switch

Quand une même variable est comparée à plusieurs valeurs, switch est plus lisible qu'une suite de else if :

switch (.direction) {
    case 1:
        .x--;           // gauche
        break;
    case 2:
        .x++;           // droite
        break;
    case 3:
        .y--;           // haut
        break;
    case 4:
        .y++;           // bas
        break;
    default:
        // aucune direction : on ne bouge pas
}

Ne pas oublier break. Sans lui, l'exécution continue dans le case suivant. Avec .direction qui vaut 1, ce code ajoute 1 puis 10 :

switch (.direction) {
    case 1: .total += 1;        // pas de break...
    case 2: .total += 10;       // ... on continue ici : total vaut 11
        break;
}

La valeur d'un case doit être connue à la compilation : un nombre, un caractère ('A') ou une constante. C'est l'occasion de donner un nom aux valeurs :

const ubyte GAUCHE = 1;
const ubyte DROITE = 2;

switch (.direction) {
    case .GAUCHE: .x--; break;
    case .DROITE: .x++; break;
}

Une variable n'est pas acceptée : case c: not a constant declared before the switch.

La boucle for

for répète un bloc un nombre de fois connu. Sa première ligne a trois parties, séparées par des points-virgules :

for (ubyte i = 1; i <= 10; i++) {
    .total = .total + i;        // 1 + 2 + ... + 10 : total vaut 55
}
PartieIciRôle
le départubyte i = 1exécuté une fois, avant le premier tour : crée le compteur
la conditioni <= 10testée avant chaque tour : la boucle continue tant qu'elle est vraie
le pasi++exécuté après chaque tour : fait avancer le compteur

Le compteur peut aussi descendre, avec i--. Comme toute variable, il a un type : un byte peut devenir négatif, un ubyte non.

Le piège du compte à rebours. Cette boucle ne s'arrête jamais :

for (ubyte n = 7; n >= 0; n--) { ... }

Un ubyte est toujours supérieur ou égal à 0 : après 0, il repart à 255 (le débordement du chapitre 3), et la condition reste vraie. Avec byte n, la boucle fait bien ses 8 tours, de 7 à 0.

Des boucles dans des boucles

Une boucle peut en contenir une autre. Pour chaque tour de la boucle extérieure, la boucle intérieure fait tous les siens : c'est la façon naturelle de parcourir des lignes et des colonnes.

for (ubyte y = 0; y < 3; y++) {           // 3 lignes
    for (ubyte x = 0; x < 4; x++) {       // de 4 colonnes chacune
        .cases++;                         // 3 × 4 : cases vaut 12
    }
}

Les boucles while et do … while

while

while répète un bloc tant qu'une condition est vraie, sans compteur. Elle convient quand on ne sait pas d'avance combien de tours il faudra :

uint n = 1;
while (n < 1000) {
    n = n * 2;          // 1, 2, 4, 8... la boucle s'arrête à 1024
}

La condition est testée avant chaque tour : si elle est fausse dès le départ, le bloc ne s'exécute pas du tout.

do … while

La condition est testée après chaque tour : le bloc s'exécute donc au moins une fois. Attention au point-virgule final :

do {
    .touche = io.getButtons(0);     // lire la manette...
} while (.touche == 0);             // ... jusqu'à ce qu'un bouton soit appuyé

La boucle sans fin

while (1) a une condition toujours vraie : la boucle ne s'arrête jamais. C'est la forme habituelle de la boucle principale d'un jeu, qui lit la manette, déplace les personnages et redessine l'écran, encore et encore :

while (1) {
    // un tour de jeu
}

Sortir ou sauter un tour : break et continue

Dans une boucle (for, while ou do … while) :

// chercher le premier multiple de 7 après 50
for (ubyte i = 51; i < 100; i++) {
    if (i % 7 == 0) {
        .trouve = i;    // 56
        break;          // trouvé : inutile de continuer
    }
}

// compter les nombres impairs de 0 à 9
for (ubyte i = 0; i < 10; i++) {
    if (i % 2 == 0) {
        continue;       // pair : on passe au suivant
    }
    .impairs++;         // impairs vaut 5 à la fin
}

Dans un switch placé dans une boucle, break sort du switch, pas de la boucle.

Un exemple : le sapin

Ce programme dessine un sapin d'étoiles. Chaque ligne du feuillage a deux étoiles de plus que la précédente, et une ligne sur deux change de couleur.

// Structures de contrôle : un sapin (Commodore 64)
architecture commodore.c64;

addlib vic.base;
addlib vic.screentext;

unit {
    const ubyte HAUTEUR = 8;            // le nombre de lignes du feuillage

    main() {
        ubyte t = 0;

        vic.setBorderColor(0);
        vic.setBackgroundColor(0);      // fond noir
        vic.initScreen();
        vic.clearScreen();

        // le feuillage : une ligne par tour de boucle
        for (ubyte ligne = 0; ligne < .HAUTEUR; ligne++) {
            // une ligne sur deux en vert clair
            if (ligne % 2 == 0) {
                vic.setFontColor(5);    // vert
            } else {
                vic.setFontColor(13);   // vert clair
            }
            vic.setCursor((ubyte)(20 - ligne), (ubyte)(6 + ligne));
            // 1, 3, 5, 7... étoiles
            for (ubyte i = 0; i < ligne * 2 + 1; i++) {
                vic.putChar('*');
            }
        }

        // le tronc : deux lignes
        vic.setFontColor(9);            // marron
        while (t < 2) {
            vic.setCursor(20, (ubyte)(6 + .HAUTEUR + t));
            vic.putChar('#');
            t++;
        }
    }
}
Écran du Commodore 64 : un sapin d'étoiles vertes et son tronc
Le sapin sur Commodore 64.
La version PC Engine

Les boucles et la condition sont les mêmes. Chaque couleur de texte a sa palette (0, 1 et 2), et le sapin est centré sur la colonne 16, le milieu des 32 colonnes de l'écran.

// Structures de contrôle : un sapin (PC Engine)
architecture nec.pce;

addlib sys.resource;
addlib vce.base;
addlib vdc.base;
addlib vdc.screentext;

unit {
    const int font = addResource("font.bin");
    const ubyte HAUTEUR = 8;            // le nombre de lignes du feuillage

    main() {
        ubyte t = 0;
        int source;
        int length;

        // l'écran, la police et les couleurs, comme au chapitre 2
        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, 0, RGB(0, 0, 1));     // le fond, bleu nuit
        vce.setPalette(0, 1, RGB(0, 5, 0));     // palette 0 : vert
        vce.setPalette(1, 1, RGB(3, 7, 3));     // palette 1 : vert clair
        vce.setPalette(2, 1, RGB(4, 2, 0));     // palette 2 : marron
        vdc.clearScreen();

        // le feuillage : une ligne par tour de boucle
        for (ubyte ligne = 0; ligne < .HAUTEUR; ligne++) {
            // une ligne sur deux en vert clair
            if (ligne % 2 == 0) {
                vdc.setFontColor(0);
            } else {
                vdc.setFontColor(1);
            }
            vdc.setCursor((ubyte)(16 - ligne), (ubyte)(8 + ligne));
            // 1, 3, 5, 7... étoiles
            for (ubyte i = 0; i < ligne * 2 + 1; i++) {
                vdc.putChar('*');
            }
        }

        // le tronc : deux lignes
        vdc.setFontColor(2);
        while (t < 2) {
            vdc.setCursor(16, (ubyte)(8 + .HAUTEUR + t));
            vdc.putChar('#');
            t++;
        }
    }
}
Écran de la PC Engine : un sapin d'étoiles vertes et son tronc
Le sapin sur PC Engine.

À retenir

À vous de jouer

  1. Passez la hauteur du sapin à 12 lignes, en ne changeant qu'une seule ligne du programme.
    Voir la solutionMasquer la solution

    La hauteur du sapin n'est écrite qu'à un seul endroit : la constante HAUTEUR. Toutes les boucles et toutes les positions sont calculées à partir d'elle. On change donc cette seule ligne :

    const ubyte HAUTEUR = 12;           // le nombre de lignes du feuillage

    La boucle du feuillage fait alors 12 tours, la ligne la plus large a 12 × 2 - 1 = 23 étoiles, et le tronc descend tout seul sous la dernière ligne, puisque sa position utilise aussi .HAUTEUR. C'est tout l'intérêt d'une constante : si le nombre 8 avait été recopié dans chaque calcul, il aurait fallu tous les retrouver.

    Écran du Commodore 64 : un sapin d'étoiles de 12 lignes
    Un sapin de 12 lignes sur Commodore 64.

    Le sapin tient encore dans l'écran : sur C64, il commence à la colonne 20 - 11 = 9 et finit à la ligne 6 + 12 + 1 = 19 ; sur PC Engine, il va de la colonne 5 à la ligne 21, sur 28 lignes visibles. Au-delà d'une vingtaine de lignes, il sortirait de l'écran.

  2. Élargissez le tronc à trois colonnes, avec une boucle for dans le while.
    Voir la solutionMasquer la solution

    Le tronc garde ses deux lignes, faites par le while. À chaque ligne, une boucle for écrit trois # au lieu d'un seul. Comme putChar avance le curseur d'une case, il suffit de placer le curseur une seule fois, une colonne plus à gauche pour que le tronc reste centré.

    Sur Commodore 64

    // le tronc : deux lignes de trois colonnes
    vic.setFontColor(9);            // marron
    while (t < 2) {
        vic.setCursor(19, (ubyte)(6 + .HAUTEUR + t));    // une colonne plus à gauche
        for (ubyte c = 0; c < 3; c++) {
            vic.putChar('#');         // trois caractères côte à côte
        }
        t++;
    }

    Sur PC Engine

    // le tronc : deux lignes de trois colonnes
    vdc.setFontColor(2);
    while (t < 2) {
        vdc.setCursor(15, (ubyte)(8 + .HAUTEUR + t));    // une colonne plus à gauche
        for (ubyte c = 0; c < 3; c++) {
            vdc.putChar('#');         // trois caractères côte à côte
        }
        t++;
    }
    Écran de la PC Engine : le sapin avec un tronc de trois colonnes
    Le tronc élargi, sur PC Engine.

    La boucle for a son propre compteur, c, qui repart de 0 à chaque tour du while. Les deux boucles sont imbriquées : 2 lignes de 3 caractères, soit 6 #.

  3. Remplacez le if par un switch sur ligne % 3 pour alterner trois couleurs.
    Voir la solutionMasquer la solution

    ligne % 3 est le reste de la division par 3 : il vaut 0, 1, 2, puis de nouveau 0, 1, 2… Un switch sur ce reste choisit une couleur parmi trois. Les deux premiers cas finissent par break ; le troisième est le default, qui reçoit le reste 2.

    On range d'abord le reste dans une variable, couleur, puis on fait le switch sur elle. Le cast (ubyte) est nécessaire, car le calcul se fait en int.

    Sur Commodore 64

    // le feuillage : une ligne par tour de boucle
    for (ubyte ligne = 0; ligne < .HAUTEUR; ligne++) {
        // trois couleurs, l'une après l'autre : 0, 1, 2, 0, 1, 2...
        couleur = (ubyte)(ligne % 3);
        switch (couleur) {
            case 0:
                vic.setFontColor(5);    // vert
                break;
            case 1:
                vic.setFontColor(13);   // vert clair
                break;
            default:
                vic.setFontColor(7);    // jaune : une guirlande
        }
        vic.setCursor((ubyte)(20 - ligne), (ubyte)(6 + ligne));
        // 1, 3, 5, 7... étoiles
        for (ubyte i = 0; i < ligne * 2 + 1; i++) {
            vic.putChar('*');
        }
    }

    Il faut aussi déclarer ubyte couleur; en tête de main(), à côté de t.

    Écran du Commodore 64 : le sapin aux lignes vert, vert clair et jaune
    Trois couleurs qui alternent : vert, vert clair, jaune.

    Sur PC Engine, chaque couleur de texte demande sa palette. Les palettes 0, 1 et 2 sont déjà prises par le vert, le vert clair et le marron du tronc : la troisième couleur va dans la palette 3.

    Sur PC Engine

    vce.setPalette(0, 0, RGB(0, 0, 1));     // le fond, bleu nuit
    vce.setPalette(0, 1, RGB(0, 5, 0));     // palette 0 : vert
    vce.setPalette(1, 1, RGB(3, 7, 3));     // palette 1 : vert clair
    vce.setPalette(2, 1, RGB(4, 2, 0));     // palette 2 : marron
    vce.setPalette(3, 1, RGB(7, 7, 0));     // palette 3 : jaune
    // le feuillage : une ligne par tour de boucle
    for (ubyte ligne = 0; ligne < .HAUTEUR; ligne++) {
        // trois couleurs, l'une après l'autre : 0, 1, 2, 0, 1, 2...
        couleur = (ubyte)(ligne % 3);
        switch (couleur) {
            case 0:
                vdc.setFontColor(0);    // vert
                break;
            case 1:
                vdc.setFontColor(1);    // vert clair
                break;
            default:
                vdc.setFontColor(3);    // jaune : une guirlande
        }
        vdc.setCursor((ubyte)(16 - ligne), (ubyte)(8 + ligne));
        // 1, 3, 5, 7... étoiles
        for (ubyte i = 0; i < ligne * 2 + 1; i++) {
            vdc.putChar('*');
        }
    }
    Écran de la PC Engine : le sapin aux lignes vert, vert clair et jaune
    La même alternance sur PC Engine, avec la palette 3.

    Pourquoi passer par la variable couleur ? Avec la version actuelle du compilateur, switch (ligne % 3) compile sans erreur, mais le reste n'est pas calculé : le sapin s'affiche en jaune, ou presque (toutes les lignes sauf la pointe sur C64, toutes sur PC Engine). Ranger le calcul dans une variable avant le switch évite le problème, et le code reste aussi lisible.

  4. Dessinez le sapin la tête en bas, avec une boucle qui compte à rebours. Quel type faut-il donner au compteur ?
    Voir la solutionMasquer la solution

    Pour retourner le sapin, on dessine d'abord le tronc en haut, puis le feuillage de la ligne la plus large jusqu'à la pointe. Le compteur ligne part de HAUTEUR - 1 et descend jusqu'à 0 compris : la condition est ligne >= 0. Les calculs de la boucle ne changent pas (largeur, couleur, colonne de départ) : seul l'ordre change, et la ligne d'écran est calculée pour que la ligne la plus large soit en haut.

    // le tronc, en haut : deux lignes
    vic.setFontColor(9);    // marron
    while (t < 2) {
        vic.setCursor(20, (ubyte)(6 + t));
        vic.putChar('#');
        t++;
    }
    
    // le feuillage, de la ligne la plus large jusqu'à la pointe :
    // le compteur descend de HAUTEUR - 1 à 0, il faut donc un byte
    for (byte ligne = .HAUTEUR - 1; ligne >= 0; ligne--) {
        if (ligne % 2 == 0) {
            vic.setFontColor(5);    // vert
        } else {
            vic.setFontColor(13);   // vert clair
        }
        vic.setCursor((ubyte)(20 - ligne), (ubyte)(6 + 2 + .HAUTEUR - 1 - ligne));
        for (ubyte i = 0; i < ligne * 2 + 1; i++) {
            vic.putChar('*');
        }
    }

    Le compteur doit être un byte, signé. Avec un ubyte, la condition ligne >= 0 est toujours vraie : après 0, ligne-- donne 255, et la boucle ne s'arrête jamais. C'est le piège du compte à rebours vu dans ce chapitre. Le compilateur l'accepte sans erreur ni avertissement ; à l'exécution sur C64, le programme écrit des étoiles et des caractères au hasard sur tout l'écran :

    Écran du Commodore 64 rempli de caractères au hasard
    Avec un compteur ubyte : la boucle ne s'arrête plus.

    Avec byte ligne, la boucle fait bien ses 8 tours, de 7 à 0. Sur PC Engine, le code est le même, avec vdc., les palettes 0, 1 et 2, la colonne 16 et la ligne 8 :

    // le tronc, en haut : deux lignes
    vdc.setFontColor(2);
    while (t < 2) {
        vdc.setCursor(16, (ubyte)(8 + t));
        vdc.putChar('#');
        t++;
    }
    
    // le feuillage, de la ligne la plus large jusqu'à la pointe :
    // le compteur descend de HAUTEUR - 1 à 0, il faut donc un byte
    for (byte ligne = .HAUTEUR - 1; ligne >= 0; ligne--) {
        if (ligne % 2 == 0) {
            vdc.setFontColor(0);
        } else {
            vdc.setFontColor(1);
        }
        vdc.setCursor((ubyte)(16 - ligne), (ubyte)(8 + 2 + .HAUTEUR - 1 - ligne));
        for (ubyte i = 0; i < ligne * 2 + 1; i++) {
            vdc.putChar('*');
        }
    }
    Écran de la PC Engine : le sapin la tête en bas, le tronc au-dessus
    Le sapin la tête en bas, sur PC Engine.