[#_lesson_7_a_game] == Leçon 7 : Un jeu image::learn/07-a-game.png[La leçon terminée, 480] Il y a six leçons, vous avez mis un mot sur un écran noir. Depuis, vous avez appris toutes les idées dont est fait un petit jeu : les variables, les décisions, les boucles, vos propres fonctions, et les listes de choses. Cette leçon n'enseigne aucune idée nouvelle. Elle dépense celles que vous avez. Vous allez construire un jeu appelé Dodge. Des blocs tombent du ciel, vous faites glisser une barre le long du bas de l'écran pour les esquiver, et chaque bloc qui vous rate vaut un point. Prenez-en trois et c'est fini. Vous le construirez en six étapes, et chacune d'elles tourne : vous n'aurez jamais à taper cent lignes en croisant les doigts. [#_make_the_folder] === Créez le dossier Créez un dossier appelé `dodge`, et dedans un fichier appelé `dodge.singe`. Lancez-le comme vous lancez tout depuis la leçon un : ---- Singe -R dodge ---- Laissez-le tourner dans un terminal pendant toute la leçon. Chaque fois que vous enregistrez, le jeu redémarre avec votre modification. [#_stage_one_something_you_can_steer] === Première étape : quelque chose à diriger Tapez ceci, enregistrez, et dirigez-le avec les touches fléchées gauche et droite. [source,lua] ---- dofile("Singe/Framework.singe") local PLAYER_WIDTH = 40 local PLAYER_HEIGHT = 8 local PLAYER_SPEED = 4 local PLAYER_MARGIN = 4 local screenWidth = overlayGetWidth() local screenHeight = overlayGetHeight() local playerY = screenHeight - PLAYER_HEIGHT - PLAYER_MARGIN local playerX = (screenWidth - PLAYER_WIDTH) / 2 local goingLeft = false local goingRight = false function onInputPressed(what) if what == SWITCH_LEFT then goingLeft = true elseif what == SWITCH_RIGHT then goingRight = true end end function onInputReleased(what) if what == SWITCH_LEFT then goingLeft = false elseif what == SWITCH_RIGHT then goingRight = false end end function onOverlayUpdate() if goingLeft then playerX = playerX - PLAYER_SPEED end if goingRight then playerX = playerX + PLAYER_SPEED end if playerX < 0 then playerX = 0 elseif playerX > screenWidth - PLAYER_WIDTH then playerX = screenWidth - PLAYER_WIDTH end overlayClear() colorForeground(80, 255, 160) overlayBox(playerX, playerY, playerX + PLAYER_WIDTH - 1, playerY + PLAYER_HEIGHT - 1) return OVERLAY_UPDATED end ---- Cinquante lignes, et presque rien de nouveau. Il y a deux choses nouvelles, alors commençons par la première ligne. [source,lua] ---- dofile("Singe/Framework.singe") ---- `dofile` appartient à Lua plutôt qu'à Singe, et ça veut dire « va exécuter ce fichier, puis reviens continuer ici ». Le fichier qu'il exécute est `Singe/Framework.singe`, l'un des fichiers que le moteur a extraits dans votre dossier de travail la première fois que vous avez lancé quoi que ce soit, à la leçon un. Ouvrez-le dans votre éditeur de texte et regardez : c'est du Lua ordinaire, de la même nature que ce que vous écrivez depuis la leçon deux, et il n'a rien de magique. L'exécuter donne à votre script une série de noms dont il peut ensuite se servir. Ceux auxquels ce livre fait appel sont les suivants : * `DIR`, qui contient le dossier où se trouve votre propre script. La leçon neuf en a besoin dès qu'un jeu charge une image, et c'est la vraie raison pour laquelle cette ligne est là. * `SCANCODE` et `MODIFIER`, des noms pour chaque touche d'un clavier, pour un jeu qui lit ce qu'on tape plutôt qu'un joystick. * `GAMEPAD_0` à `GAMEPAD_3` et `MOUSE_0` à `MOUSE_3`, pour une borne sur laquelle on a branché plus d'un exemplaire de quelque chose. * Quelques fonctions utilitaires dont le nom commence par `util`, et les six appels qui envoient un score à un tableau des scores, ce qui est l'objet de la leçon vingt-huit. * Une seconde façon d'écrire un jeu, tout entière, qui est l'objet de la leçon quinze. Une chose qu'il ne vous donne *pas*, parce que c'est ce qu'on croirait naturellement. `SWITCH_LEFT`, et tous les autres noms qui commencent par `SWITCH_`, viennent du moteur lui-même, avant même qu'un script ne s'exécute. C'est pourquoi les leçons trois à six pouvaient lire les touches fléchées sans rien charger. Le framework ajoute à ce qui est déjà là ; il ne le fournit pas. Ce jeu-ci n'utilise rien du framework. La ligne est là parce que chaque jeu de la leçon neuf jusqu'à la fin du livre en a besoin, et parce que c'est ainsi que commence presque chaque vrai jeu Singe. Elle est lue une fois, au démarrage, puis on n'y pense plus. La deuxième chose nouvelle, c'est la façon dont les touches fléchées sont gérées, parce que c'est la première chose sur laquelle les débutants se trompent. `onInputPressed` ne déplace pas le joueur. Il met `goingLeft` à `true`, et `onInputReleased` le remet à `false`. Le déplacement se fait dans `onOverlayUpdate`, qui demande « le joueur garde-t-il la gauche enfoncée ? » soixante fois par seconde. Si vous déplaciez plutôt le joueur dans `onInputPressed`, vous obtiendriez un pas par appui : Singe envoie un appui exactement une fois, quand la touche s'enfonce, et ne le répète jamais, ce que l'entrée de `onInputPressed` dans la Référence dit clairement. Garder la touche enfoncée ne ferait rien du tout. Le gestionnaire d'appui note donc ce qui est vrai dans le monde, et la mise à jour décide quoi en faire. Gardez cette séparation, et votre code de lecture des commandes fera cinq lignes pour toujours. Les quatre noms en majuscules sont des variables ordinaires. Écrire `PLAYER_SPEED` en majuscules est un message à vous-même et à quiconque vous lit : celui-là est fixé une fois en haut et ne change jamais. Rien dans Lua ne l'impose. Ça compte parce que dans une minute il y en aura dix, et quand le jeu sera trop difficile, vous voudrez un endroit évident où aller baisser un nombre. `screenWidth` et `screenHeight` viennent du moteur au lieu d'être tapés à la main, si bien que le jeu se met en page tout seul, quelle que soit la taille de l'overlay. `playerY` est calculé une fois à partir de la hauteur : la barre se tient à `PLAYER_MARGIN` pixels du bas et ne monte ni ne descend jamais, alors il n'y a aucune raison de le recalculer à chaque image. Le `- 1` de la ligne `overlayBox` n'est pas une faute de frappe. `overlayBox` dessine en incluant les deux coins que vous lui donnez, donc une boîte de `playerX` à `playerX + 40` fait quarante et un pixels de large. Retirer un donne une barre visible d'exactement quarante de large, ce qui comptera à la quatrième étape, quand quelque chose devra décider si elle vous a touché. [#_stage_two_something_to_dodge] === Deuxième étape : quelque chose à éviter Un bloc, qui tombe. Ajoutez deux constantes à la liste du haut : [source,lua] ---- local BLOCK_SIZE = 12 local BLOCK_SPEED = 1.5 ---- Deux variables de plus sous `goingRight` : [source,lua] ---- local blockX = math.random(0, screenWidth - BLOCK_SIZE) local blockY = -BLOCK_SIZE ---- Dans `onOverlayUpdate`, juste au-dessus de la ligne `overlayClear()`, faites-le tomber : [source,lua] ---- blockY = blockY + BLOCK_SPEED if blockY > screenHeight then blockX = math.random(0, screenWidth - BLOCK_SIZE) blockY = -BLOCK_SIZE end ---- Et sous la ligne qui dessine le joueur, dessinez-le : [source,lua] ---- colorForeground(255, 90, 90) overlayBox(blockX, blockY, blockX + BLOCK_SIZE - 1, blockY + BLOCK_SIZE - 1) ---- Enregistrez. Un carré rouge tombe, atteint le bas, et réapparaît en haut à un nouvel endroit. Il démarre à `-BLOCK_SIZE`, c'est-à-dire au-dessus du bord supérieur de l'écran. Une position négative n'a rien d'anormal : le moteur dessine la partie de la boîte qui est sur l'overlay et jette sans rien dire la partie qui n'y est pas. Le bloc glisse dans le champ au lieu de surgir de nulle part. `BLOCK_SPEED` vaut `1.5`, pas un nombre entier, donc `blockY` passe la plus grande partie de sa vie à valoir quelque chose comme `83.5`. Ça aussi, c'est normal. Le dessin laisse tomber la partie fractionnaire, et la garder dans la variable est ce qui permet au bloc d'avancer de moins d'un pixel par image. [#_stage_three_a_sky_full_of_them] === Troisième étape : le ciel en est plein Un bloc, ce n'est pas un jeu. À la leçon six, vous avez appris à garder beaucoup d'exemplaires de quelque chose dans une liste, et c'est à ça que ça servait. Supprimez `blockX` et `blockY`, et mettez ceci à leur place : [source,lua] ---- local blocks = {} local spawnTimer = 0 ---- Ajoutez une constante de plus en haut : [source,lua] ---- local SPAWN_FRAMES = 40 ---- Maintenant, une fonction qui fabrique un bloc et le met dans la liste. Mettez-la au-dessus de `onInputPressed` : [source,lua] ---- function spawnBlock() local block = {} block.x = math.random(0, screenWidth - BLOCK_SIZE) block.y = -BLOCK_SIZE table.insert(blocks, block) end ---- Remplacez le code de chute de la deuxième étape par ceci : [source,lua] ---- spawnTimer = spawnTimer - 1 if spawnTimer <= 0 then spawnBlock() spawnTimer = SPAWN_FRAMES end for i = #blocks, 1, -1 do local block = blocks[i] block.y = block.y + BLOCK_SPEED if block.y > screenHeight then table.remove(blocks, i) end end ---- Et remplacez la ligne qui dessinait le bloc par une boucle sur tous les blocs : [source,lua] ---- for i, block in ipairs(blocks) do overlayBox(block.x, block.y, block.x + BLOCK_SIZE - 1, block.y + BLOCK_SIZE - 1) end ---- Enregistrez. Il pleut des blocs. Un bloc est un enregistrement : une petite table avec un `x` et un `y` dedans, ce qui est la manière d'empêcher deux nombres qui vont ensemble de se séparer. `blocks` est une liste de ces enregistrements, et c'est la forme qu'a le monde de presque tous les jeux. `spawnTimer` décompte d'un par image, et quand il atteint zéro, un bloc naît et le minuteur est remonté. Quarante images, c'est environ deux tiers de seconde. Il n'y a pas d'horloge dans ce jeu, et il n'en faut pas : `onOverlayUpdate` est le battement de cœur, et compter les battements est une très bonne manière de mesurer le temps. La boucle de chute tourne *à rebours* -- de `#blocks` jusqu'à `1`, un pas à la fois -- parce qu'elle retire des choses en chemin. C'est la règle de la leçon six, et c'est la première fois que vous avez une vraie raison de la suivre. Retirez l'élément trois en avançant et tout recule d'un cran, si bien que le bloc qui était l'élément quatre devient l'élément trois et que la boucle ne le regarde jamais. En allant à rebours, tout ce qui bouge a déjà été traité. La boucle de dessin utilise `ipairs` à la place, parce qu'elle ne retire rien et veut seulement chaque bloc à tour de rôle. [#_stage_four_getting_hit] === Quatrième étape : se faire toucher Les blocs vous traversent. Il est temps de s'en apercevoir. Le moteur a une fonction pour exactement cette question. Changez le test à l'intérieur de la boucle de chute, de ceci : [source,lua] ---- if block.y > screenHeight then table.remove(blocks, i) end ---- à ceci : [source,lua] ---- if collideRects(block.x, block.y, BLOCK_SIZE, BLOCK_SIZE, playerX, playerY, PLAYER_WIDTH, PLAYER_HEIGHT) or block.y > screenHeight then table.remove(blocks, i) end ---- Enregistrez, et lancez la barre contre un bloc qui tombe. Il disparaît. `collideRects` prend deux rectangles et répond `true` s'ils se chevauchent. Chaque rectangle fait quatre nombres : un coin, puis une largeur et une hauteur -- *pas* deux coins, ce qui est l'autre manière raisonnable de décrire un rectangle et celle qu'utilise `overlayBox`. L'entrée de `collideRects` dans la Référence donne l'ordre, et ça vaut la peine d'y jeter un œil maintenant, parce que mettre les huit arguments dans le mauvais ordre est le genre d'erreur qui ne produit aucun message, juste un jeu où rien ne touche jamais rien. Vous pourriez l'écrire vous-même. C'est quatre comparaisons, et ce n'est vraiment pas difficile. Servez-vous quand même de celle du moteur, pour trois raisons. Elle est juste, y compris dans les cas délicats : un rectangle sans largeur, deux rectangles qui ne se touchent que par un bord. Elle dit ce qu'elle veut dire, donc la ligne se lit « ces deux-là se sont-ils chevauchés » au lieu de quatre comparaisons reliées par `and` qu'il faut déchiffrer chaque fois que vous y revenez. Et quand vous arriverez à la leçon onze, où les choses se heurtent sans arrêt, vous connaîtrez déjà la famille : `collidePointRect` pour un clic de souris, `collideCircles` pour deux choses rondes, et `collideSegments` pour un tir contre un mur. La Référence les présente ensemble. La seule chose que le moteur ne peut pas faire pour vous, c'est décider ce qu'un rectangle *veut dire*. Ici, le rectangle du bloc est le bloc entier. Dans un vrai jeu, vous voulez souvent que le rectangle de collision soit un peu plus petit que l'image, pour qu'un frôlement ressemble à un frôlement. C'est une décision de conception, et c'est à la leçon onze que vous la prendrez. [#_stage_five_score_and_lives] === Cinquième étape : score et vies Se faire toucher devrait coûter quelque chose, et survivre devrait rapporter quelque chose. Deux constantes de plus en haut : [source,lua] ---- local BLOCK_FASTER = 0.05 local START_LIVES = 3 ---- Trois variables de plus, à côté de `spawnTimer` : [source,lua] ---- local blockSpeed = BLOCK_SPEED local score = 0 local lives = START_LIVES ---- `BLOCK_SPEED` est maintenant la vitesse à laquelle le jeu *démarre*, et `blockSpeed` la vitesse à laquelle il va en ce moment. Changez la ligne de chute pour qu'elle utilise la variable : [source,lua] ---- block.y = block.y + blockSpeed ---- Puis séparez les deux cas, parce qu'ils font maintenant des choses différentes : [source,lua] ---- if collideRects(block.x, block.y, BLOCK_SIZE, BLOCK_SIZE, playerX, playerY, PLAYER_WIDTH, PLAYER_HEIGHT) then table.remove(blocks, i) lives = lives - 1 elseif block.y > screenHeight then table.remove(blocks, i) score = score + 1 blockSpeed = blockSpeed + BLOCK_FASTER end ---- Et affichez les nombres. Mettez ceci juste avant le `return` : [source,lua] ---- overlayPrint(1, 1, "SCORE " .. score .. " LIVES " .. lives) ---- Enregistrez, et jouez une minute. Ça devient plus difficile, et c'est tout le truc : chaque bloc que vous esquivez fait tomber les suivants plus vite, d'un vingtième de pixel par image. Au bout de vingt points, ils sont nettement plus rapides. Vous n'avez pas eu à écrire un seul mot sur les niveaux de difficulté. Vous verrez aussi `LIVES` descendre à `-4`, parce que rien n'arrête encore le jeu. C'est l'étape suivante, et c'est celle qui compte le plus. [#_stage_six_game_over_and_going_again] === Sixième étape : fin de partie, et on recommence Voici le piège. La manière évidente d'ajouter un écran de fin de partie, c'est une variable appelée `gameOver`, mise à `true` quand il n'y a plus de vies. Puis vous voulez un écran titre avant la première partie, alors vous ajoutez `started`. Puis vous voulez une touche de pause, alors vous ajoutez `paused`. Vous avez maintenant trois variables vrai-ou-faux, huit combinaisons entre elles, et cinq de ces combinaisons sont des absurdités que votre code doit prendre soin de ne jamais produire. Chaque débutant écrit ce jeu au moins une fois, et c'est là que les petits jeux vont mourir. Faites plutôt ceci. Une seule variable contient *ce que fait le jeu*, sous forme de mot : [source,lua] ---- local state = "waiting" ---- Elle vaut `"waiting"` avant la première partie, `"playing"` pendant une partie, et `"over"` quand les vies sont épuisées. Trois états, une variable, et rien qui ressemble à une combinaison impossible. Ces chaînes n'ont rien de magique ; ce sont des mots que vous avez choisis, et vous devez les écrire de la même façon à chaque fois. Maintenant, la fonction qui commence une partie. Chaque variable qui appartient à une partie revient ici à sa valeur de départ, en un seul endroit : [source,lua] ---- function startGame() playerX = (screenWidth - PLAYER_WIDTH) / 2 blocks = {} blockSpeed = BLOCK_SPEED spawnTimer = 0 score = 0 lives = START_LIVES state = "playing" end ---- Perdre la dernière vie y met fin. Dans la branche de la collision, sous `lives = lives - 1` : [source,lua] ---- if lives == 0 then state = "over" end ---- La barre d'espace lance une partie, mais seulement quand il n'y en a pas déjà une en cours. Ajoutez une branche à `onInputPressed` : [source,lua] ---- elseif what == SWITCH_BUTTON1 and state ~= "playing" then startGame() ---- `~=` veut dire « n'est pas égal à ». `state ~= "playing"` est vrai pendant l'attente et vrai après la fin, ce qui correspond exactement aux deux moments où Espace doit lancer une partie. `SWITCH_BUTTON1` est la barre d'espace par défaut, et aussi le bouton A d'une manette. Deux petites fonctions mettent les mots à l'écran. `printCentered` fait le calcul une fois, pour que l'écran de titre et celui de fin de partie ne le fassent pas chacun de travers : [source,lua] ---- function drawOverText() printCentered(7, "GAME OVER") printCentered(9, "PRESS SPACE TO PLAY AGAIN") end function drawWaitingText() printCentered(6, "DODGE THE BLOCKS") printCentered(8, "ARROW KEYS TO MOVE") printCentered(10, "PRESS SPACE TO START") end function printCentered(row, text) overlayPrint(math.floor((TEXT_COLUMNS - #text) / 2), row, text) end ---- avec une constante de plus en haut : [source,lua] ---- local TEXT_COLUMNS = overlayGetWidth() // overlayGetFontWidth() ---- Enfin, la partie importante. Déplacez tout ce qui dessine hors de `onOverlayUpdate`, dans une fonction appelée `drawGame`, déplacez tout ce qui bouge dans une fonction appelée `updatePlaying` -- toutes les deux sont dans le code complet ci-dessous -- et laissez `onOverlayUpdate` ainsi : [source,lua] ---- function onOverlayUpdate() if state == "playing" then updatePlaying() end overlayClear() drawGame() if state == "waiting" then drawWaitingText() elseif state == "over" then drawOverText() end return OVERLAY_UPDATED end ---- Enregistrez. Vous avez un jeu : un écran titre, une partie, une fin de partie, et un moyen de recommencer. Relisez ces treize lignes, parce qu'elles ont la forme de tous les jeux que vous écrirez. Le monde ne bouge que tant que l'état vaut `"playing"`, donc l'écran de fin de partie se fige avec les derniers blocs suspendus exactement là où ils étaient. Le monde est dessiné à chaque image quel que soit l'état, parce que l'image est la même dans les trois ; l'état décide seulement quels mots vont par-dessus. Et il y a exactement un endroit qui prend cette décision. C'est ce que vous a acheté la variable `state`. Quand vous ajouterez une pause, vous ajouterez une branche à ce `if` et une ligne à `onInputPressed`, et rien d'autre dans le jeu n'aura besoin d'en savoir quoi que ce soit. [#_the_whole_thing_6] === Le tout Le voici, terminé. Si le vôtre ne correspond pas, la différence vaut la peine d'être trouvée. [source,lua] ---- dofile("Singe/Framework.singe") local PLAYER_WIDTH = 40 local PLAYER_HEIGHT = 8 local PLAYER_SPEED = 4 local PLAYER_MARGIN = 4 local BLOCK_SIZE = 12 local BLOCK_SPEED = 1.5 local BLOCK_FASTER = 0.05 local SPAWN_FRAMES = 40 local START_LIVES = 3 local TEXT_COLUMNS = overlayGetWidth() // overlayGetFontWidth() local screenWidth = overlayGetWidth() local screenHeight = overlayGetHeight() local playerY = screenHeight - PLAYER_HEIGHT - PLAYER_MARGIN local playerX = (screenWidth - PLAYER_WIDTH) / 2 local goingLeft = false local goingRight = false local blocks = {} local blockSpeed = BLOCK_SPEED local spawnTimer = 0 local score = 0 local lives = START_LIVES local state = "waiting" function drawGame() colorForeground(80, 255, 160) overlayBox(playerX, playerY, playerX + PLAYER_WIDTH - 1, playerY + PLAYER_HEIGHT - 1) colorForeground(255, 90, 90) for i, block in ipairs(blocks) do overlayBox(block.x, block.y, block.x + BLOCK_SIZE - 1, block.y + BLOCK_SIZE - 1) end overlayPrint(1, 1, "SCORE " .. score .. " LIVES " .. lives) end function drawOverText() printCentered(7, "GAME OVER") printCentered(9, "PRESS SPACE TO PLAY AGAIN") end function drawWaitingText() printCentered(6, "DODGE THE BLOCKS") printCentered(8, "ARROW KEYS TO MOVE") printCentered(10, "PRESS SPACE TO START") end function printCentered(row, text) overlayPrint(math.floor((TEXT_COLUMNS - #text) / 2), row, text) end function spawnBlock() local block = {} block.x = math.random(0, screenWidth - BLOCK_SIZE) block.y = -BLOCK_SIZE table.insert(blocks, block) end function startGame() playerX = (screenWidth - PLAYER_WIDTH) / 2 blocks = {} blockSpeed = BLOCK_SPEED spawnTimer = 0 score = 0 lives = START_LIVES state = "playing" end function updatePlaying() if goingLeft then playerX = playerX - PLAYER_SPEED end if goingRight then playerX = playerX + PLAYER_SPEED end if playerX < 0 then playerX = 0 elseif playerX > screenWidth - PLAYER_WIDTH then playerX = screenWidth - PLAYER_WIDTH end spawnTimer = spawnTimer - 1 if spawnTimer <= 0 then spawnBlock() spawnTimer = SPAWN_FRAMES end for i = #blocks, 1, -1 do local block = blocks[i] block.y = block.y + blockSpeed if collideRects(block.x, block.y, BLOCK_SIZE, BLOCK_SIZE, playerX, playerY, PLAYER_WIDTH, PLAYER_HEIGHT) then table.remove(blocks, i) lives = lives - 1 if lives == 0 then state = "over" end elseif block.y > screenHeight then table.remove(blocks, i) score = score + 1 blockSpeed = blockSpeed + BLOCK_FASTER end end end function onInputPressed(what) if what == SWITCH_LEFT then goingLeft = true elseif what == SWITCH_RIGHT then goingRight = true elseif what == SWITCH_BUTTON1 and state ~= "playing" then startGame() end end function onInputReleased(what) if what == SWITCH_LEFT then goingLeft = false elseif what == SWITCH_RIGHT then goingRight = false end end function onOverlayUpdate() if state == "playing" then updatePlaying() end overlayClear() drawGame() if state == "waiting" then drawWaitingText() elseif state == "over" then drawOverText() end return OVERLAY_UPDATED end ---- Environ cent vingt lignes de code, et vous les avez toutes écrites quelques-unes à la fois. [#_what_just_happened_4] === Ce qui vient de se passer Quelques morceaux méritent un second regard. [source,lua] ---- local blocks = {} local score = 0 function startGame() score = 0 end ---- Les données sont `local` et les fonctions ne le sont pas. Ce sont deux choix délibérés, et opposés. Les données sont `local` pour qu'elles appartiennent à ce fichier et que rien à l'extérieur ne puisse y mettre la main et les changer. Les fonctions sont globales pour qu'elles puissent s'appeler les unes les autres dans n'importe quel ordre : `drawOverText` est défini avant `printCentered` et l'appelle quand même, parce qu'un nom global est cherché au moment de l'appel, et qu'à ce moment-là le fichier entier a été lu. Rendez ces fonctions `local` et le fichier devra être trié pour que rien ne soit utilisé avant d'apparaître, et c'est une règle sur laquelle vous trébucheriez pendant tout le reste du livre. Trois des noms d'ici n'ont pas le choix du tout : `onOverlayUpdate` et les deux callbacks des commandes doivent être globaux, parce que c'est ainsi que Singe les trouve. Dans le fichier, l'ordre est alphabétique, avec les trois callbacks à la fin. Ce n'est pas une règle de Lua. C'est pour que dans six mois vous puissiez trouver `spawnBlock` sans rien lire. [source,lua] ---- overlayPrint(math.floor((TEXT_COLUMNS - #text) / 2), row, text) ---- `+#text+` est la longueur d'une chaîne, en caractères. Vous avez rencontré `+#+` à la leçon six, pour compter les éléments d'une liste, et il fait le même travail ici. `"GAME OVER"` fait neuf caractères, donc sur un écran de soixante colonnes il commence à la colonne vingt-cinq, ce qui laisse vingt-cinq cellules d'espace à sa gauche et vingt-six à sa droite. `math.floor` jette la partie fractionnaire, parce que la colonne vingt-cinq et demie n'existe pas. `TEXT_COLUMNS` calcule combien de cellules de caractère tiennent sur la largeur de l'écran. `overlayPrint` compte en cellules alors que tout le reste compte en pixels, donc la largeur de l'overlay est divisée par la largeur d'une cellule, et c'est `overlayGetFontWidth` qui vous la donne. Deux barres obliques plutôt qu'une, parce qu'une fraction de colonne n'est pas un endroit où afficher. Calculez-le de cette façon et le centrage reste juste quelle que soit la taille de l'overlay, ce qui est le quatrième point de la section *Essayez*. [source,lua] ---- colorForeground(255, 90, 90) ---- `colorForeground` règle une couleur qui reste réglée. Ce n'est pas un argument passé à `overlayBox` ; c'est un interrupteur que vous basculez, et tout ce qui est dessiné ensuite sort dans cette couleur jusqu'à ce que vous le basculiez de nouveau. C'est pourquoi `drawGame` règle le vert, dessine le joueur, règle le rouge, puis dessine tous les blocs sans plus parler de couleur. Ça n'affecte pas `overlayPrint`. La police de la console est copiée sur l'overlay telle quelle, dans ses propres couleurs, ce que l'entrée de `overlayPrint` dans la Référence dit en toutes lettres. Du texte en couleur demande une police chargée, et c'est la leçon dix-neuf. [#_try_it_7] === Essayez . *Rendez-le plus facile, puis injuste.* Changez `PLAYER_WIDTH` en `80` et jouez une partie. Puis changez-le en `12`, la taille d'un bloc. Remarquez que vous n'avez eu à toucher ni au dessin ni à la collision, ni pour l'un ni pour l'autre. . *Accélérez plus vite.* Changez `BLOCK_FASTER` de `0.05` à `0.5` et voyez combien de points vous arrivez à marquer. Puis essayez `0`, et demandez-vous si le jeu vaut encore la peine d'être joué. . *Une vie en plus.* Rendez une vie au joueur tous les dix points. Tout ce dont vous avez besoin est déjà dans `updatePlaying`, dans la branche qui ajoute au score. . *Jouez en plus grand.* Lancez-le avec `Singe -R --canvas=1280x720 dodge`. Le jeu se met en page correctement, texte centré compris, parce qu'il a demandé au moteur la taille de l'écran. `TEXT_COLUMNS` l'a demandée aussi. . *Mettez-le en pause.* Ajoutez un quatrième état, `"paused"`. `SWITCH_BUTTON2` doit faire passer de `"playing"` à `"paused"` et inversement, et rien ne doit bouger pendant la pause. Bien fait, ça tient en trois lignes et vous n'avez pas du tout à toucher à `updatePlaying`. [#_break_it_on_purpose_7] === Cassez-le exprès Allez dans `spawnBlock` et supprimez la ligne qui règle `block.y`, pour que la fonction se lise ainsi : [source,lua] ---- function spawnBlock() local block = {} block.x = math.random(0, screenWidth - BLOCK_SIZE) table.insert(blocks, block) end ---- Enregistrez, appuyez sur Espace, et Singe s'arrête avec ceci : ---- Error executing function 'onOverlayUpdate': dodge/dodge.singe:97: attempt to perform arithmetic on a nil value (field 'y') stack traceback: dodge/dodge.singe:97: in function 'updatePlaying' dodge/dodge.singe:135: in function 'onOverlayUpdate' ---- La ligne 97, c'est `block.y = block.y + blockSpeed`. La plainte, c'est que quelque chose dans ce calcul n'est pas un nombre, et la partie entre parenthèses vous dit lequel : `field 'y'`, c'est-à-dire le `y` à l'intérieur d'une table. Il n'a pas de valeur, parce que vous ne lui en avez jamais donné, et une chose sans valeur, en Lua, c'est `nil`. Remarquez où est l'erreur et où est la faute. L'erreur est dans `updatePlaying`, à quarante lignes de `spawnBlock`, là où rien ne cloche du tout. `updatePlaying` est seulement le premier morceau de code assez malchanceux pour toucher aux dégâts. C'est le cas normal, pas l'exception, et remonter cette piste est un savoir-faire. Remettez la ligne. Puis tournez la page, parce que la leçon suivante ne parle que de ça. [#_what_you_learned_7] === Ce que vous avez appris * Un jeu, c'est un monde qui change, un monde qu'on dessine, et une décision sur ce qu'on fait des deux -- et ce sont trois morceaux de code séparés. * Les gestionnaires de commandes doivent noter ce qui est enfoncé ; la mise à jour doit décider ce que ça veut dire. * Une liste d'enregistrements est la forme de presque tous les mondes de jeu. * Retirez des choses d'une liste en la parcourant à rebours. * `collideRects` répond à la question de savoir si deux rectangles se chevauchent, et la version du moteur vaut mieux que la vôtre parce qu'elle est juste, qu'elle se lit bien, et qu'elle a une famille que vous reverrez. * Une seule variable `state` qui contient un mot vaut mieux que trois indicateurs vrai-ou-faux, et continue de mieux valoir à mesure que le jeu grandit. * Lancez une partie depuis une seule fonction qui remet tout à zéro, pour que la deuxième partie soit exactement comme la première. * Les nombres que vous voudrez peut-être régler ont leur place dans des constantes nommées en haut du fichier. * Construisez un jeu par étapes qui tournent chacune. Un programme qui n'a jamais marché est bien plus difficile à réparer qu'un programme qui marchait il y a cinq minutes. [#_next_time_7] === La prochaine fois Vous avez maintenant écrit assez de code pour faire des erreurs intéressantes. La leçon suivante est celle dont aucun livre pour débutants ne s'embarrasse : ce que disent vraiment les messages d'erreur de Singe, comment découvrir ce que contient réellement une variable au lieu de deviner, et comment couper un problème en deux jusqu'à ce qu'il n'ait plus nulle part où se cacher.