558 lines
23 KiB
Text
558 lines
23 KiB
Text
[#_lesson_12_score_lives_and_game_over]
|
||
== Leçon 12 : Score, vies et fin de partie
|
||
|
||
image::learn/12-score.png[La leçon terminée, 480]
|
||
|
||
Votre jeu tient le score, retire des vies et affiche GAME OVER. C'est presque un
|
||
jeu. Il manque encore trois choses. Il n'a pas de porte d'entrée : il est au
|
||
milieu de l'action à l'instant où il se charge. Il oublie : quittez-le, et le
|
||
meilleur score jamais obtenu part avec. Et il garde la trace de ce qu'il fait
|
||
avec deux variables vrai-ou-faux, `over` et `dying`, ce qui est le piège dont
|
||
la leçon sept vous a prévenu.
|
||
|
||
Cette leçon règle les trois. Une seule variable pour ce que fait le jeu, un
|
||
écran titre d'où partir, un meilleur score qui est toujours là le lendemain, et
|
||
une deuxième partie qui commence exactement comme la première.
|
||
|
||
[#_a_game_is_always_in_exactly_one_state]
|
||
=== Un jeu est toujours dans exactement un état
|
||
|
||
Regardez n'importe quelle borne d'arcade pendant deux minutes. Elle affiche un
|
||
titre et attend quelqu'un. Quelqu'un appuie sur start et elle joue. Le joueur
|
||
n'a plus de vies et elle le dit, puis elle se remet à attendre. La machine fait
|
||
toujours exactement une de ces choses.
|
||
|
||
Dodge, à la leçon sept, gardait cela dans une seule variable qui contenait un
|
||
mot : `"waiting"`, `"playing"` ou `"over"`. Le jeu de la leçon neuf est revenu à
|
||
un `over` vrai-ou-faux, et la leçon dix a ajouté `dying` à côté. Comptez ce que
|
||
deux de ces variables peuvent dire. Chacune vaut `true` ou `false`, donc
|
||
ensemble elles font quatre combinaisons. Trois veulent dire quelque chose : la
|
||
partie en cours, la mort du vaisseau et la fin de partie. La quatrième, les deux
|
||
à la fois, est absurde, et votre code doit prendre soin de ne jamais la
|
||
produire. Il en prend soin : regardez `endGame`, qui remet `dying` à `false`
|
||
avant de mettre `over` à `true`. Ajoutez maintenant un écran titre. Ça fait un
|
||
troisième drapeau, huit combinaisons, dont quatre absurdes. C'est exactement le
|
||
piège de la leçon sept, et c'est là que les petits jeux vont mourir.
|
||
|
||
Alors faites comme Dodge. Quatre états, une seule variable. Mettez les noms des
|
||
états sous vos autres constantes :
|
||
|
||
[source,lua]
|
||
----
|
||
local TITLE = "title"
|
||
local PLAYING = "playing"
|
||
local DYING = "dying"
|
||
local OVER = "over"
|
||
----
|
||
|
||
Puis retirez `over` et `dying` de vos variables, et mettez une seule ligne à
|
||
leur place :
|
||
|
||
[source,lua]
|
||
----
|
||
local state = TITLE
|
||
----
|
||
|
||
Dodge se servait des mots eux-mêmes. Des noms en majuscules ne sont pas plus
|
||
malins, et ils ne rattrapent pas une faute de frappe : Lua traite un nom mal
|
||
tapé comme `nil` et un mot mal tapé comme un mot qui ne correspond à rien, et
|
||
l'un comme l'autre vous donne un jeu qui cesse sans bruit de répondre. Ce que
|
||
les noms vous apportent, c'est une liste. Chaque état dans lequel le jeu peut se
|
||
trouver est écrit une fois, en haut du fichier, là où vous voyez les quatre d'un
|
||
coup d'œil.
|
||
|
||
Partout où le jeu levait un drapeau, il règle maintenant l'état. Dans
|
||
`updateRocks`, là où la dernière vie s'en va :
|
||
|
||
[source,lua]
|
||
----
|
||
if lives <= 0 then
|
||
state = DYING
|
||
deathChannel = channel
|
||
----
|
||
|
||
Dans `endGame`, deux lignes n'en font plus qu'une, et la combinaison absurde a
|
||
disparu parce qu'il ne reste plus rien à combiner :
|
||
|
||
[source,lua]
|
||
----
|
||
function endGame()
|
||
deathChannel = -1
|
||
state = OVER
|
||
musicStop(theme, 1500)
|
||
end
|
||
----
|
||
|
||
Et dans `startGame`, les deux lignes qui baissaient les deux drapeaux
|
||
deviennent `state = PLAYING`.
|
||
|
||
[#_one_place_that_decides]
|
||
=== Un seul endroit qui décide
|
||
|
||
À chaque image, `onOverlayUpdate` prend les deux décisions qui comptent : ce qui
|
||
bouge, et quels mots vont par-dessus. Ce qui bouge tient en un seul test :
|
||
|
||
[source,lua]
|
||
----
|
||
if state == PLAYING then
|
||
updateShip()
|
||
updateShots()
|
||
updateRocks()
|
||
end
|
||
----
|
||
|
||
Ça remplace `if not over and not dying then`, et ça dit ce que ça veut dire.
|
||
Sur l'écran titre, pendant que joue la dernière explosion, et une fois la partie
|
||
finie, rien ne bouge. Vous l'obtenez en n'appelant pas les fonctions qui font
|
||
bouger les choses. Arrêter un jeu n'est pas une fonctionnalité spéciale ; c'est
|
||
laisser des choses de côté.
|
||
|
||
Le monde est toujours dessiné à chaque image, quel que soit l'état, exactement
|
||
comme Dodge le dessinait. Seuls les mots changent. Remplacez la ligne
|
||
`drawHud()` par ceci :
|
||
|
||
[source,lua]
|
||
----
|
||
if state == TITLE then
|
||
drawTitle()
|
||
else
|
||
drawHud()
|
||
end
|
||
----
|
||
|
||
`drawTitle` est une nouvelle fonction, et elle va après `drawStars` :
|
||
|
||
[source,lua]
|
||
----
|
||
function drawTitle()
|
||
overlayPrint(25, 6, "R O C K S")
|
||
overlayPrint(25, 9, "BEST " .. best)
|
||
overlayPrint(22, 12, "PRESS 1 TO PLAY")
|
||
end
|
||
----
|
||
|
||
`best` est le meilleur score jusqu'ici, et il arrive à la section suivante ; le
|
||
jeu ne tournera pas tant qu'il ne sera pas là. Avant la première partie, la
|
||
liste des rochers est vide, donc ce que montre l'écran titre, ce sont les
|
||
étoiles qui défilent et le vaisseau qui attend en bas, et c'est tout ce dont un
|
||
écran titre a besoin.
|
||
|
||
Une ligne doit partir, sinon vous ne verrez jamais l'écran titre. Supprimez le
|
||
`startGame()` tout en bas du fichier. Le jeu commence maintenant sur son écran
|
||
titre, et *Recommencer une partie* explique pourquoi cette ligne compte.
|
||
|
||
`drawHud` pose lui-même une question. Changez son `if over then` en :
|
||
|
||
[source,lua]
|
||
----
|
||
if state == OVER then
|
||
----
|
||
|
||
Le callback des commandes demande le même genre de réflexion. Le voici en
|
||
entier :
|
||
|
||
[source,lua]
|
||
----
|
||
function onInputPressed(what)
|
||
if what == SWITCH_LEFT then
|
||
goLeft = true
|
||
elseif what == SWITCH_RIGHT then
|
||
goRight = true
|
||
elseif what == SWITCH_BUTTON1 and state == PLAYING then
|
||
shots[#shots + 1] = { x = shipX + shipWidth / 2 - shotWidth / 2, y = shipY }
|
||
soundPlay(shootClip, 0, 35)
|
||
elseif what == SWITCH_START1 and (state == TITLE or state == OVER) then
|
||
soundPlay(clickClip)
|
||
startGame()
|
||
end
|
||
end
|
||
----
|
||
|
||
Lisez-le depuis le haut. Les touches fléchées sont notées quel que soit l'état,
|
||
parce qu'elles disent seulement quelles touches sont enfoncées, et rien ne fait
|
||
bouger le vaisseau tant que l'état n'est pas `PLAYING`. Le tir ne veut dire
|
||
quelque chose que pendant la partie. Et `1` lance une partie depuis l'écran
|
||
titre ou depuis l'écran de fin de partie, mais pas pendant que la dernière
|
||
explosion résonne encore : la partie n'est pas finie tant qu'elle ne l'a pas
|
||
dit. Les parenthèses marchent comme en arithmétique. Le `or` qu'elles
|
||
contiennent est calculé d'abord, puis le `and` le relie à la touche.
|
||
|
||
[#_score_and_lives]
|
||
=== Score et vies
|
||
|
||
Vous avez les deux depuis la leçon neuf, et ils ne changent presque pas. Ce qui
|
||
est nouveau, c'est le meilleur score, qui commence sa vie comme une variable
|
||
ordinaire. Mettez-le avec les autres, sous `lives` :
|
||
|
||
[source,lua]
|
||
----
|
||
local best = 0
|
||
----
|
||
|
||
Affichez-le sur la ligne du haut, entre les deux autres. Dans `drawHud` :
|
||
|
||
[source,lua]
|
||
----
|
||
overlayPrint(1, 1, "SCORE " .. score)
|
||
overlayPrint(25, 1, "BEST " .. best)
|
||
overlayPrint(50, 1, "LIVES " .. lives)
|
||
----
|
||
|
||
Et donnez à `endGame` la tâche de remarquer un record :
|
||
|
||
[source,lua]
|
||
----
|
||
if score > best then
|
||
best = score
|
||
end
|
||
----
|
||
|
||
Lancez-le. L'écran titre affiche `BEST 0`. Appuyez sur `1`, tirez sur quelque
|
||
chose, perdez, et l'écran de fin de partie montre votre score comme meilleur
|
||
score. Appuyez de nouveau sur `1` : le meilleur score reste, et le score repart
|
||
de zéro. Maintenant, quittez, relancez, et regardez l'écran titre : `BEST 0`. Le
|
||
record a vécu exactement aussi longtemps que le programme. C'est la deuxième
|
||
moitié de cette leçon.
|
||
|
||
Pendant que vous regardez les vies, remarquez que la leçon neuf a écrit le test
|
||
`lives <= 0`, et non `lives == 0`, et gardez-le ainsi. Aujourd'hui, rien ne peut
|
||
retirer deux vies à la fois, donc les deux tests donnent forcément la même
|
||
réponse. Le jour où vous ajouterez un rocher qui en coûte deux, `== 0` enjambera
|
||
zéro et le joueur continuera de jouer avec moins une vie, alors que `<= 0`
|
||
continuera de marcher. Demandez ce que vous voulez dire.
|
||
|
||
[#_starting_again]
|
||
=== Recommencer une partie
|
||
|
||
Vous avez déjà supprimé le `startGame()` du bas du fichier. À la leçon dix, la
|
||
première partie était lancée par cette ligne et toutes les suivantes par un
|
||
appui sur `1`, ce qui faisait deux entrées. Maintenant, il n'y en a plus qu'une.
|
||
Chaque partie, la première comprise, commence quand quelqu'un appuie sur `1` et
|
||
que `startGame` s'exécute, donc la première partie et la cinquième commencent
|
||
exactement de la même façon.
|
||
|
||
`startGame` doit remettre en place tout ce qu'une partie change :
|
||
|
||
[source,lua]
|
||
----
|
||
function startGame()
|
||
shipX = (screenWidth - shipWidth) / 2
|
||
score = 0
|
||
lives = START_LIVES
|
||
state = PLAYING
|
||
deathChannel = -1
|
||
shots = {}
|
||
for i = 1, ROCK_COUNT do
|
||
rocks[i] = {}
|
||
newRock(rocks[i])
|
||
end
|
||
musicSetVolume(50)
|
||
musicPlay(theme, -1)
|
||
end
|
||
----
|
||
|
||
Il est tout aussi important de savoir ce qu'il laisse tranquille, et pourquoi.
|
||
`best` survit à toutes les parties ; c'est tout son travail. Les étoiles sont du
|
||
décor. Et `goLeft` et `goRight` n'appartiennent pas du tout au jeu. Elles
|
||
représentent le clavier, et seul le clavier doit les changer.
|
||
|
||
Ce dernier point est un bogue que vous venez de corriger sans le remarquer. Le
|
||
callback des commandes de la leçon dix commençait par `if over then`, et tant
|
||
que la partie était finie, il jetait toutes les touches sauf `1`. Appuyez sur la
|
||
flèche droite pendant que GAME OVER est affiché, gardez-la enfoncée, et appuyez
|
||
sur `1`. Le nouveau vaisseau reste immobile : l'appui sur la flèche droite a été
|
||
jeté, le relâchement n'a pas encore eu lieu, et donc `goRight` vaut `false`
|
||
alors que la touche est de toute évidence enfoncée. Le vaisseau ne bouge pas
|
||
tant que vous ne lâchez pas la touche pour appuyer de nouveau. Le nouveau
|
||
callback note les touches fléchées dans tous les états, donc la touche tenue est
|
||
toujours tenue quand la partie commence.
|
||
|
||
Les bogues de redémarrage sont les bogues les plus courants de tous les jeux, et
|
||
ils donnent tous la même impression quand on joue : la deuxième partie est
|
||
subtilement fausse alors que la première allait bien. Quand ça arrive, lisez
|
||
`startGame` et posez-vous deux questions. Qu'est-ce que la partie précédente a
|
||
changé et que celle-ci ne remet pas en place ? Et qu'est-ce que celle-ci remet
|
||
en place alors que ce n'était pas au jeu d'y toucher ?
|
||
|
||
[#_a_number_that_outlives_the_program]
|
||
=== Un nombre qui survit au programme
|
||
|
||
Tout ce que votre jeu a gardé jusqu'ici -- `score`, `lives`, la liste des
|
||
rochers -- vit en mémoire, et la mémoire disparaît à l'instant où le programme
|
||
s'arrête.
|
||
|
||
Singe donne à chaque jeu une sauvegarde bien à lui : un ensemble de noms avec
|
||
des valeurs rangées dessous, écrit sur le disque pour vous. C'est une autre
|
||
partie du kit de jeu de la leçon onze, donc c'est le même genre de chose que
|
||
`collideRects` : de simples appels, rien à charger ni à libérer.
|
||
|
||
[source,lua]
|
||
----
|
||
saveSet("highScore", 4200)
|
||
----
|
||
|
||
`saveSet` prend un nom -- la *clé* -- et une valeur. La clé, c'est vous qui la
|
||
choisissez, et elle ne change jamais ; la valeur, c'est ce dont vous voulez vous
|
||
souvenir. Les nombres, les chaînes, `true` et `false`, et des tables entières de
|
||
tout cela peuvent y aller. Ce qui n'a de sens que pendant que le programme
|
||
tourne, non. Un handle de sprite n'est qu'un nombre, donc il se sauvegarde, et
|
||
demain c'est un nombre qui ne veut plus rien dire. Une fonction ne peut pas du
|
||
tout s'écrire sur le disque, et essayer d'en sauvegarder une arrête le jeu sur
|
||
une erreur plutôt que de vous laisser le découvrir la semaine prochaine.
|
||
|
||
Relire la valeur, c'est l'autre moitié. Changez la ligne que vous avez ajoutée
|
||
pour `best` :
|
||
|
||
[source,lua]
|
||
----
|
||
local best = saveGet("highScore", 0)
|
||
----
|
||
|
||
Elle s'exécute une fois, au démarrage du jeu. Le second argument est la *valeur
|
||
par défaut* : ce qu'il faut répondre quand rien n'a jamais été sauvegardé sous
|
||
cette clé. On y revient dans un instant, parce que c'est toute l'erreur de cette
|
||
leçon.
|
||
|
||
Et faites en sorte que `endGame` note le record quand il tombe :
|
||
|
||
[source,lua]
|
||
----
|
||
function endGame()
|
||
deathChannel = -1
|
||
state = OVER
|
||
musicStop(theme, 1500)
|
||
if score > best then
|
||
best = score
|
||
saveSet("highScore", best)
|
||
saveFlush()
|
||
end
|
||
end
|
||
----
|
||
|
||
Vous n'avez pas à demander à Singe d'écrire le fichier. Il l'écrit une fois à la
|
||
fin de chaque image où quelque chose a changé, quel que soit le nombre de clés
|
||
que vous avez réglées, et de nouveau quand le jeu s'arrête. `saveFlush`, lui,
|
||
l'écrit immédiatement : un nouveau meilleur score mérite une écriture de fichier
|
||
rien qu'à lui, parce qu'on coupe une borne d'arcade à la prise et qu'une image,
|
||
c'est long.
|
||
|
||
Lancez-le, établissez un score, quittez, et relancez-le. L'écran titre s'en
|
||
souvient.
|
||
|
||
La section Sauvegarde de la Référence donne le reste de la famille. `saveDelete`
|
||
oublie une clé, `saveClear` oublie tout -- c'est le « remettre à zéro les
|
||
meilleurs scores » d'un menu de service -- et `saveGetAll` vous donne une copie
|
||
de l'ensemble, ce qui est utile quand vous voulez afficher toute la sauvegarde
|
||
pendant que vous traquez quelque chose.
|
||
|
||
[#_where_the_file_actually_goes]
|
||
=== Où va vraiment le fichier
|
||
|
||
Il ne va pas à côté de votre script, et ce n'est pas un hasard.
|
||
|
||
Un jeu fini peut se trouver sur un disque, sur un partage réseau, ou dans un
|
||
seul fichier empaqueté (leçon trente), et on ne peut écrire dans aucun d'eux. Un
|
||
jeu qui sauvegarde à côté de son propre script marche parfaitement sur la
|
||
machine où vous l'avez écrit et échoue sur la moitié des machines où il est
|
||
installé. Singe 2 faisait comme ça, et c'était une erreur.
|
||
|
||
Donc tout ce que Singe écrit pour un jeu -- la sauvegarde, les captures d'écran,
|
||
son journal -- va au même endroit, et `singeGetDataPath` vous dira où :
|
||
|
||
[source,lua]
|
||
----
|
||
debugPrint(singeGetDataPath())
|
||
----
|
||
|
||
Mettez ça juste sous la ligne `dofile` un instant et lancez-le. Il affiche
|
||
`data/rocks/` : le dossier `data` qui est apparu dans votre dossier de travail
|
||
la première fois que vous avez lancé quoi que ce soit, à la leçon un, et dedans
|
||
un dossier qui porte le nom du dossier de votre jeu. Vous y trouverez `save.json`.
|
||
Ouvrez-le dans votre éditeur de texte : c'est du texte simple, et votre meilleur
|
||
score s'y trouve, sous le nom que vous lui avez donné.
|
||
|
||
C'est aussi la réponse chaque fois que vous avez besoin d'un fichier à vous.
|
||
Construisez son nom à partir de `singeGetDataPath()` et il atterrit dans un
|
||
endroit où l'on peut écrire ; écrivez sous un nom de votre invention et tôt ou
|
||
tard ce ne sera plus le cas.
|
||
|
||
[#_the_first_time_there_is_nothing]
|
||
=== La première fois, il n'y a rien
|
||
|
||
La toute première fois que quelqu'un lance votre jeu, aucun meilleur score n'a
|
||
jamais été sauvegardé. `saveGet` n'a rien à renvoyer, alors il renvoie la valeur
|
||
par défaut que vous lui avez donnée :
|
||
|
||
[source,lua]
|
||
----
|
||
local best = saveGet("highScore", 0)
|
||
----
|
||
|
||
Rien n'est sauvegardé, donc `best` vaut `0`, l'écran titre affiche `BEST 0`, et
|
||
la première partie que quelqu'un joue établit un record. C'est exactement ce que
|
||
vous voulez, et ça coûte un argument de plus.
|
||
|
||
Oubliez le `0` et `saveGet` n'a pas de valeur par défaut sur laquelle se
|
||
rabattre, alors il répond `nil` -- la valeur dont Lua se sert pour dire « il n'y
|
||
a rien ici », que vous avez rencontrée à la leçon trois. `nil` n'est pas zéro.
|
||
Vous ne pouvez rien y ajouter, vous ne pouvez pas le comparer à un nombre, et
|
||
vous ne pouvez pas le coller à une chaîne. Ça tourne mal dès la première image,
|
||
à chaque fois, sur chaque machine où le jeu n'a jamais été joué -- c'est-à-dire
|
||
sur chaque machine où vos joueurs s'apprêtent à l'installer, et pas sur celle où
|
||
vous testez.
|
||
|
||
Donnez une valeur par défaut à chaque `saveGet`. C'est l'habitude la moins chère
|
||
de ce livre.
|
||
|
||
[#_what_just_happened_8]
|
||
=== Ce qui vient de se passer
|
||
|
||
[source,lua]
|
||
----
|
||
local TITLE = "title"
|
||
----
|
||
|
||
Une constante : un nom que vous réglez une fois et ne changez jamais, en
|
||
majuscules pour que vous voyiez d'un coup d'œil que c'en est une, comme à la
|
||
leçon sept. Lua ne l'impose pas -- rien ne vous empêche d'affecter une valeur à
|
||
`TITLE` -- alors les majuscules sont un message pour la prochaine personne qui
|
||
lira le code, c'est-à-dire vous dans quinze jours.
|
||
|
||
[source,lua]
|
||
----
|
||
if state == PLAYING then
|
||
----
|
||
|
||
La question qui décide si le monde bouge. Quand vous ajouterez un cinquième état
|
||
plus tard -- une pause, ou un tour bonus -- la première chose à vous demander
|
||
sera si le monde bouge dans cet état, et c'est ici que va la réponse.
|
||
|
||
[source,lua]
|
||
----
|
||
elseif what == SWITCH_START1 and (state == TITLE or state == OVER) then
|
||
----
|
||
|
||
Sans les parenthèses, Lua relierait d'abord le `and` et lirait ceci comme « la
|
||
touche est `1` et l'état est le titre, ou l'état est la fin de partie ». Alors
|
||
n'importe quelle touche que les branches du dessus n'ont pas attrapée lancerait
|
||
une partie depuis l'écran de fin de partie, le tir compris, et un joueur qui
|
||
martèle encore le bouton de tir quand le dernier rocher le touche serait jeté
|
||
dans une nouvelle partie dès l'apparition de GAME OVER. Les parenthèses font
|
||
passer le `or` en premier. Quand vous mélangez `and` et `or`, mettez les
|
||
parenthèses même si vous croyez connaître l'ordre. La prochaine personne qui
|
||
lira la ligne n'aura pas besoin de le connaître.
|
||
|
||
[source,lua]
|
||
----
|
||
saveSet("highScore", best)
|
||
saveFlush()
|
||
----
|
||
|
||
`saveSet` met le nombre dans la sauvegarde ; `saveFlush` met la sauvegarde sur
|
||
le disque tout de suite plutôt qu'à la fin de l'image. Les deux sont dans le
|
||
`if`, donc le fichier n'est écrit que quand le record est vraiment battu, ce qui
|
||
arrive quelques fois par session plutôt que soixante fois par seconde.
|
||
|
||
[source,lua]
|
||
----
|
||
local best = saveGet("highScore", 0)
|
||
----
|
||
|
||
Lu une fois, au démarrage, dans une variable ordinaire. Le reste du jeu lit
|
||
`best` au lieu de rappeler `saveGet`, parce que la sauvegarde est un fichier et
|
||
qu'une variable est une variable. Écrivez dans les deux quand le record tombe.
|
||
|
||
[#_try_it_12]
|
||
=== Essayez
|
||
|
||
. *Revenez à l'écran titre.* Une borne d'arcade ne reste pas sur GAME OVER pour
|
||
toujours. Cherchez `timerAfter` dans la section Minuteur de la Référence, une
|
||
autre partie du kit de jeu, et servez-vous-en dans `endGame` pour remettre
|
||
l'état à `TITLE` cinq secondes après la fin de la partie. Puis appuyez vite
|
||
sur `1` après la fin d'une partie et vérifiez que le minuteur ne renvoie pas
|
||
votre nouvelle partie à l'écran titre au bout de trois secondes.
|
||
. *Montrez les vies sous forme de vaisseaux.* Dessinez `shipSprite` une fois
|
||
par vie dans le coin supérieur droit avec `spriteDraw` au lieu d'afficher le
|
||
nombre. Une boucle `for` de la leçon quatre, et `shipWidth` vous dit à quelle
|
||
distance les espacer.
|
||
. *Sauvegardez plus qu'un nombre.* Ajoutez `saveSet("gamesPlayed", saveGet("gamesPlayed",
|
||
0) + 1)` à `startGame`, et affichez-le sur l'écran titre. Quittez, relancez,
|
||
et regardez-le se souvenir.
|
||
. *Trouvez le fichier et cassez-le.* Ouvrez `data/rocks/save.json` dans votre
|
||
éditeur, remplacez le meilleur score par quelque chose d'absurde, et lancez le
|
||
jeu. Puis supprimez complètement le fichier et relancez-le. Ces deux choses
|
||
arriveront à vos joueurs.
|
||
. *Remettez-le à zéro depuis le jeu.* Faites qu'une touche sur laquelle vous
|
||
n'appuieriez jamais par accident appelle `saveClear()`, et regardez l'écran
|
||
titre ensuite. Attention : `best` est toujours en mémoire, et `saveClear` n'y
|
||
touche pas. Trouvez ce qui doit se passer d'autre.
|
||
|
||
[#_break_it_on_purpose_11]
|
||
=== Cassez-le exprès
|
||
|
||
Retirez la valeur par défaut, pour que la ligne devienne :
|
||
|
||
[source,lua]
|
||
----
|
||
local best = saveGet("highScore")
|
||
----
|
||
|
||
Si vous avez déjà établi un meilleur score, il ne se passe rien du tout, et
|
||
c'est là le piège : la sauvegarde a la clé, donc `saveGet` la renvoie. Supprimez
|
||
d'abord `data/rocks/save.json`, puis lancez-le. L'écran titre n'apparaît jamais,
|
||
et vous obtenez ceci :
|
||
|
||
----
|
||
Error executing function 'onOverlayUpdate': rocks/rocks.singe:101: attempt to concatenate a nil value (upvalue 'best')
|
||
stack traceback:
|
||
rocks/rocks.singe:101: in function 'drawTitle'
|
||
rocks/rocks.singe:165: in function 'onOverlayUpdate'
|
||
----
|
||
|
||
Lisez-le comme la leçon huit vous l'a appris. Le fichier et la ligne vous mènent
|
||
à `overlayPrint(25, 9, "BEST " .. best)` dans `drawTitle`, et la plainte dit que
|
||
quelque chose qui valait `nil` a été collé à une chaîne. `..` joint deux
|
||
chaînes, et `nil` n'en est pas une. Si vos lignes sont disposées autrement, vos
|
||
numéros le seront aussi.
|
||
|
||
La ligne qu'elle nomme n'est pas celle où se trouve l'erreur. La ligne 101 est
|
||
l'endroit où le jeu a fini par trébucher dessus ; l'erreur a été commise près du
|
||
haut du fichier, là où on a demandé `best` et où il n'y avait rien. Cet écart
|
||
entre l'endroit où une mauvaise valeur est fabriquée et celui où on la remarque
|
||
est ce qu'il y a de plus difficile avec `nil`, et c'est la raison pour laquelle
|
||
une valeur par défaut coûte un argument et épargne une soirée.
|
||
|
||
Remettez le `0`.
|
||
|
||
[#_what_you_learned_12]
|
||
=== Ce que vous avez appris
|
||
|
||
* Un jeu est dans exactement un état à la fois, et les drapeaux vrai-ou-faux qui
|
||
essaient de dire lequel engendrent des combinaisons qui ne veulent rien dire.
|
||
* Gardez l'état dans une seule variable, avec ses valeurs possibles nommées une
|
||
fois en haut du fichier.
|
||
* `onOverlayUpdate` décide de ce qui bouge et de ce qui est dit, en interrogeant
|
||
l'état.
|
||
* Arrêter un jeu, c'est simplement ne pas appeler ce qui le fait bouger.
|
||
* Faites commencer chaque partie, la première comprise, par une seule fonction,
|
||
et faites-lui remettre en place tout ce qu'une partie change -- et rien
|
||
d'autre.
|
||
* Les touches appartiennent au clavier. Notez-les dans tous les états.
|
||
* `saveSet` range une valeur sous une clé, et `saveGet` la relit lors d'un
|
||
lancement ultérieur.
|
||
* `saveFlush` écrit le fichier tout de suite, pour les choses que vous
|
||
détesteriez perdre.
|
||
* La sauvegarde va dans le dossier de données propre au jeu, que nomme
|
||
`singeGetDataPath`. N'écrivez jamais à côté de votre script.
|
||
* `saveGet` sans valeur par défaut répond `nil` quand rien n'a été sauvegardé,
|
||
et `nil` casse la première image sur une machine qui n'a jamais lancé votre
|
||
jeu.
|
||
* Quand vous mélangez `and` et `or`, mettez des parenthèses.
|
||
|
||
[#_next_time_12]
|
||
=== La prochaine fois
|
||
|
||
Votre jeu est maintenant un jeu, et c'est un dossier que vous seul savez lancer.
|
||
La leçon treize lui donne un `games.dat`, pour que le menu du moteur lui-même le
|
||
liste avec un titre et une image, et qu'un joueur qui n'a jamais entendu parler
|
||
de vous puisse le lancer sans rien taper.
|