singe/docs/fr/lessons/07-a-game.adoc

877 lines
30 KiB
Text
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[#_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.