589 lines
24 KiB
Text
589 lines
24 KiB
Text
[#_lesson_28_online]
|
||
== Leçon 28 : En ligne
|
||
|
||
image::learn/28-online.png[La leçon terminée, 480]
|
||
|
||
À la leçon douze, vous avez sauvegardé un meilleur score. Il est toujours là le
|
||
lendemain matin, ce qui est l'essentiel de ce à quoi sert un meilleur score,
|
||
mais c'est un meilleur score sur une seule machine. Cette leçon met un score
|
||
sur un tableau que d'autres peuvent voir.
|
||
|
||
C'est aussi la leçon sur l'échec. Tout ce que vous avez écrit jusqu'ici échoue
|
||
pour des raisons que vous pouvez trouver : un fichier manque, un nombre est
|
||
faux, une fonction est mal orthographiée. Le réseau échoue pour des raisons
|
||
que personne dans la pièce ne peut voir. Un câble a été débranché dans un
|
||
bâtiment où vous n'avez jamais mis les pieds, et votre jeu doit continuer à
|
||
tourner. L'essentiel de ce qui suit parle de cela.
|
||
|
||
[#_what_the_service_is]
|
||
=== Ce qu'est le service
|
||
|
||
Singe peut parler à un *service maître*, un serveur qui détient trois choses :
|
||
|
||
* un *compte*, qui appartient à la machine et non à un jeu en particulier ;
|
||
* un *catalogue* de jeux à télécharger et à mettre à jour ;
|
||
* des *tableaux des meilleurs scores*, un ou plusieurs par jeu.
|
||
|
||
Le service par défaut est `master.singeengine.com`, et un exploitant qui fait
|
||
tourner le sien fait pointer la machine ailleurs. La section du manuel sur le
|
||
service en ligne décrit les deux côtés.
|
||
|
||
Maintenant, la partie que les gens sautent. Le service n'est *pas* du
|
||
multijoueur : deux joueurs n'ont aucun moyen d'être dans la même partie. Ce
|
||
n'est pas un nuage de sauvegardes ; la sauvegarde de la leçon douze reste sur
|
||
la machine. Ce n'est pas une protection contre la copie, et rien en lui
|
||
n'empêche qui que ce soit de jouer à votre jeu. Il n'est pas obligatoire : un
|
||
jeu qui ne se connecte jamais tourne exactement comme il l'a toujours fait, et
|
||
c'est le seul comportement par défaut raisonnable pour quelque chose qui a
|
||
besoin d'une connexion qui marche.
|
||
|
||
Le tableau est donc un bonus. Gardez le meilleur score local de la leçon
|
||
douze, continuez à l'afficher, et laissez le tableau en ligne être la chose qui
|
||
est là de temps en temps.
|
||
|
||
[#_signing_in_happens_once_and_not_in_your_game]
|
||
=== On se connecte une fois, et pas dans votre jeu
|
||
|
||
Le compte appartient à la machine. On le crée et on s'y connecte depuis le menu
|
||
fourni, derrière la touche affectée à `INPUT_SERVICE`, dans l'outil appelé
|
||
*Online Account*. Le jeton qu'il obtient est écrit dans `master.dat` à la
|
||
racine des données, un niveau au-dessus du dossier de données propre à chaque
|
||
jeu, si bien que tous les jeux de la machine envoient leurs scores sous la même
|
||
connexion.
|
||
|
||
Votre jeu ne demande jamais d'adresse e-mail et ne demande jamais de mot de
|
||
passe. Il n'existe aucune fonction pour le faire, et c'est voulu : un joueur
|
||
qui tape un mot de passe dans un jeu qu'il a téléchargé n'a aucun moyen de
|
||
savoir où il part.
|
||
|
||
Ce sous quoi un joueur apparaît sur un tableau est un *nom de joueur* : de
|
||
trois à vingt caractères, lettres, chiffres, traits d'union et tirets bas, qui
|
||
commencent et finissent par une lettre ou un chiffre. Pas d'espaces, pas de
|
||
points, et pas de `@`, si bien qu'un nom de joueur ne peut même pas ressembler
|
||
à une adresse e-mail. Il est séparé du compte, c'est la seule chose d'un joueur
|
||
que les autres joueurs voient, et il est choisi une fois pour toutes -- un
|
||
exploitant peut en changer un, un joueur ne le peut pas, parce qu'un tableau
|
||
dont les noms bougent sans cesse n'est vraiment l'histoire de personne. Tant
|
||
qu'aucun nom de joueur n'a été choisi, les scores sont refusés, puisqu'un score
|
||
qui ne porte le nom de personne n'est sur un tableau dans aucun sens utile.
|
||
|
||
L'outil Online Account montre le nom sous lequel cette machine envoie ses
|
||
scores, et votre jeu le lit avec `scorePlayerName()`, qui répond `nil` quand il
|
||
n'y en a pas. Bon à savoir avant que cela ne vous intrigue : un score envoyé
|
||
sans nom de joueur derrière lui est refusé par le *service*, pas par
|
||
`scoreSubmit`, si bien que votre jeu n'en est pas informé. Si
|
||
`scorePlayerName()` vaut `nil`, dites-le à l'écran tant qu'il y a encore
|
||
quelqu'un pour le lire.
|
||
|
||
[#_your_game_needs_an_id]
|
||
=== Votre jeu a besoin d'un identifiant
|
||
|
||
Un tableau doit être rangé sous quelque chose. Ce quelque chose, c'est
|
||
`GAME_ID`, dans l'entrée de votre jeu dans `games.dat`, le fichier que vous
|
||
avez créé à la leçon treize :
|
||
|
||
[source,lua]
|
||
----
|
||
GAME_ID = "6f1e7b62-0a4e-4d9c-9b2f-1c7a5e3d8a10"
|
||
----
|
||
|
||
C'est un UUID, exactement de cette forme : huit chiffres hexadécimaux, trois
|
||
groupes de quatre, puis douze, séparés par des traits d'union. Générez-en un
|
||
comme vous voulez et collez-le ; peu importe lequel vous obtenez, l'important
|
||
est qu'il ne change plus jamais. L'entrée de la Référence pour `singeGetGameId`
|
||
explique pourquoi rien d'autre n'est accepté.
|
||
|
||
`singeGetGameId()` le renvoie, ou `nil` quand l'entrée n'en a pas. Un jeu sans
|
||
identifiant peut être joué mais pas classé, et demander d'abord vous évite de
|
||
proposer un tableau des scores qui ne pourra jamais marcher :
|
||
|
||
[source,lua]
|
||
----
|
||
if singeGetGameId() then
|
||
-- showLeaderboardButton est votre propre fonction, pas une de celles de Singe.
|
||
showLeaderboardButton()
|
||
end
|
||
----
|
||
|
||
[#_the_six_calls]
|
||
=== Les six appels
|
||
|
||
Tout ce qu'un jeu fait avec le service tient en six fonctions, et elles
|
||
viennent de `Singe/Framework.singe`, alors le `dofile` en haut de votre script
|
||
n'est pas facultatif ici :
|
||
|
||
[source,lua]
|
||
----
|
||
dofile("Singe/Framework.singe")
|
||
----
|
||
|
||
`scoreBegin()` dit qu'une partie commence. `scoreSubmit(value)` met un score en
|
||
file d'attente. `scoreBoard(onDone)` récupère le tableau. `scorePlayerName()`
|
||
est le nom de joueur. `scoreWaiting()` est le nombre de scores qui ne sont pas
|
||
encore partis. Et `scoreUpdate()` doit s'exécuter une fois par image, depuis
|
||
`onOverlayUpdate`, sinon aucun des autres ne se termine jamais.
|
||
|
||
Ce dernier est toute l'astuce de cette leçon, alors il est dit deux fois. Rien
|
||
ici ne parle au réseau quand vous l'appelez. `scoreSubmit` écrit dans une file
|
||
d'attente et rend la main aussitôt. `scoreBoard` retient ce que vous voulez et
|
||
rend la main aussitôt. Le vrai travail se fait par petites tranches dans
|
||
`scoreUpdate`, entre vos images, et c'est pour cela que votre jeu ne saccade
|
||
jamais quand la ligne est lente. Oubliez `scoreUpdate` et rien ne casse, aucune
|
||
erreur ne s'affiche, et il ne se passe jamais rien.
|
||
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
scoreUpdate()
|
||
overlayClear()
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
[#_posting_a_score]
|
||
=== Envoyer un score
|
||
|
||
À la fin d'une partie :
|
||
|
||
[source,lua]
|
||
----
|
||
if scoreSubmit(score) then
|
||
fetchBoard()
|
||
else
|
||
saying = "Kept here only: " .. whyNotPosted()
|
||
end
|
||
----
|
||
|
||
`scoreSubmit` répond `false` quand il n'y a pas de compte ou que le jeu n'a pas
|
||
de `GAME_ID`, et `true` quand le score est entré dans la file d'attente.
|
||
Remarquez ce que le `else` ne fait *pas* : il ne jette pas le score, il ne
|
||
s'excuse pas longuement, et il n'empêche pas le joueur de commencer une autre
|
||
partie. Il dit une courte ligne, parce que le joueur n'a rien fait de mal et ne
|
||
peut rien y corriger d'ici.
|
||
|
||
C'est la file d'attente qui rend cela sûr. Un score en file d'attente est écrit
|
||
sur le disque, il survit donc à l'extinction de la machine, et il est remis au
|
||
service la prochaine fois qu'il y a une connexion -- une heure plus tard, ou
|
||
mardi prochain. Voilà pourquoi une borne dans une salle au wifi médiocre finit
|
||
quand même sur le tableau.
|
||
|
||
Soyez honnête avec vous-même sur la limite de cette promesse : la file
|
||
d'attente garde les scores d'une machine qui *a* un compte. Si personne ne
|
||
s'est jamais connecté, `scoreSubmit` répond `false` et le score n'est pas gardé
|
||
pour plus tard, parce qu'il n'y a personne pour qui le garder. `scoreWaiting()`
|
||
vous dit combien il en reste dans la file d'attente, et afficher ce nombre
|
||
quelque part est une gentillesse : c'est la différence entre « le service est
|
||
en panne » et « trois de vos scores attendent le retour d'Internet ».
|
||
|
||
`scoreBegin()` mérite un paragraphe à lui seul. Appelez-le quand une partie
|
||
commence, et le service note le moment d'après sa propre horloge. Le score qui
|
||
suit porte la durée de la partie, mesurée sur le serveur, et c'est le seul
|
||
nombre d'un envoi qui n'est pas passé par la machine du joueur. Rien n'est
|
||
rejeté sur cette base : la durée est montrée à celui qui fait tourner le
|
||
service, à côté du score, et c'est une personne qui décide. Une machine qui
|
||
était hors ligne quand la partie a commencé n'enregistre aucune durée, ce qui
|
||
est honnête plutôt que cassé.
|
||
|
||
Et puisque nous sommes dans l'honnêteté : un score envoyé est une
|
||
*déclaration*. Le jeu tourne sur l'ordinateur du joueur, alors rien de ce que
|
||
peut faire le client n'en fait davantage, et une clé secrète livrée dans votre
|
||
jeu n'aurait que l'apparence de la sécurité. Le service enregistre qui a
|
||
déclaré quoi, limite la vitesse à laquelle les scores peuvent arriver, et
|
||
signale les scores aberrants pour qu'une personne les examine. Concevez votre
|
||
tableau en le sachant.
|
||
|
||
[#_reading_the_board_back]
|
||
=== Relire le tableau
|
||
|
||
`scoreBoard` est la première fonction de ce livre qui prend une fonction comme
|
||
argument.
|
||
|
||
[source,lua]
|
||
----
|
||
scoreBoard(function(ok, result)
|
||
...
|
||
end)
|
||
----
|
||
|
||
Vous avez écrit des fonctions à la leçon cinq et vous leur avez donné des noms.
|
||
Celle-ci n'a pas de nom : elle est écrite là où elle sert, passée directement à
|
||
`scoreBoard`, et appelée plus tard -- peut-être plusieurs secondes plus tard --
|
||
quand la réponse arrive. C'est encore un *callback*, la même idée que
|
||
`onOverlayUpdate`, sauf que cette fois, c'est vous qui choisissez qui
|
||
l'appelle.
|
||
|
||
`ok` vaut `true` ou `false`. Quand il vaut `false`, `result` est une courte
|
||
chaîne qui dit pourquoi. Quand il vaut `true`, `result` est une table en deux
|
||
parties :
|
||
|
||
* `result.top`, une liste de lignes, chacune avec un `name` et une `value` ;
|
||
* `result.standing`, la propre `value` de ce joueur, son `rank`, et le nombre
|
||
de `players` sur le tableau -- qui peut manquer, si ce joueur n'a rien
|
||
dessus.
|
||
|
||
Entre l'appel et le callback, votre jeu continue de tourner et de dessiner. Ce
|
||
délai est réel et le joueur le voit, alors mettez-y quelque chose. Le script
|
||
ci-dessous règle une ligne de texte sur `"Asking the service..."` avant
|
||
l'appel, et le callback remplace cette ligne soit par le tableau, soit par la
|
||
raison pour laquelle il n'y en a pas. Un écran qui ne dit rien pendant quatre
|
||
secondes a l'air cassé ; un écran qui dit ce qu'il fait est juste lent.
|
||
|
||
[#_the_catalogue]
|
||
=== Le catalogue
|
||
|
||
La troisième chose que fait le service, c'est lister des jeux. L'outil *Get
|
||
Games* du menu montre tout ce que le service propose, marque ce qui est déjà
|
||
installé sur cette machine, et le télécharge, le met à jour ou le supprime. Un
|
||
téléchargement est vérifié d'après l'empreinte publiée par le catalogue avant
|
||
de remplacer quoi que ce soit, si bien qu'un transfert qui arrive abîmé échoue
|
||
au lieu d'installer un jeu qui ne tournera pas. Un jeu que le service a retiré
|
||
continue de marcher et apparaît comme installé mais plus proposé : la seule
|
||
chose qui s'arrête, ce sont les mises à jour, parce que quelqu'un qui a un jeu
|
||
l'a.
|
||
|
||
Votre jeu n'appelle rien de tout cela, et ne doit pas le faire. Le catalogue
|
||
vous concerne en tant qu'auteur d'un jeu plutôt qu'en tant que rédacteur d'un
|
||
script : c'est là que va votre fichier `.game` quand vous le publiez, ce qui
|
||
est le sujet de la leçon trente, et c'est une raison de plus pour que votre
|
||
`games.dat` ait un `GAME_ID` qui ne change jamais. Si un jour vous voulez la
|
||
liste depuis un script, `Singe/Master.singe` est le module qui se trouve sous
|
||
tout cela, et il vaut la peine d'être lu une fois.
|
||
|
||
[#_what_the_service_knows_about_a_player]
|
||
=== Ce que le service sait d'un joueur
|
||
|
||
Si vous publiez un jeu qui envoie des scores, vous confiez les données de
|
||
quelqu'un d'autre à un serveur, et c'est votre responsabilité même si vous
|
||
n'avez pas écrit le serveur. Alors dites-le clairement, dans l'écran « À
|
||
propos » de votre jeu ou dans son fichier lisez-moi :
|
||
|
||
* Le service détient une *adresse e-mail* et un mot de passe pour le compte.
|
||
L'adresse sert à récupérer un mot de passe perdu, et à rien d'autre. Elle
|
||
n'est jamais montrée aux autres joueurs et ne peut pas être changée ensuite.
|
||
* Le *nom de joueur* est ce qui apparaît sur les tableaux. Il est
|
||
volontairement séparé du compte pour qu'une adresse ne soit jamais ce que
|
||
voient les autres joueurs, et il est vérifié d'après une liste de noms que
|
||
personne ne devrait prendre et de mots qui n'ont pas leur place sur un écran
|
||
dans une salle publique.
|
||
* Un *score* porte sa valeur, le tableau où il se trouve, l'heure qu'a
|
||
enregistrée le service, et le nom de joueur. C'est tout.
|
||
* Le catalogue sait *quels jeux sont installés* sur la machine, et en quelle
|
||
version, parce que c'est ainsi qu'il vous propose une mise à jour.
|
||
|
||
Ce que rien de tout cela ne fait, c'est suivre un joueur d'un jeu à l'autre,
|
||
vendre quoi que ce soit, ou aller chercher dans la machine quoi que ce soit que
|
||
vous n'avez pas envoyé. Votre jeu ne doit rien ajouter à la liste. Si vous
|
||
joignez des informations supplémentaires à un score -- `scoreSubmit` prend pour
|
||
cela un troisième argument facultatif -- qu'elles concernent le jeu.
|
||
|
||
[#_the_script]
|
||
=== Le script
|
||
|
||
Un test de réflexes : attendez le mot, appuyez sur le bouton, marquez ce qui
|
||
reste de mille. C'est un petit jeu, et tout le reste, c'est la partie en
|
||
ligne.
|
||
|
||
Créez un dossier appelé `reaction` et mettez ceci dans un fichier
|
||
`reaction.singe`, dedans. Donnez au dossier un `games.dat` comme celui de la
|
||
leçon treize, qui désigne `reaction/reaction.singe`, avec dans son entrée la
|
||
ligne `GAME_ID` vue plus haut.
|
||
|
||
[source,lua]
|
||
----
|
||
dofile("Singe/Framework.singe")
|
||
|
||
local WAIT_MIN = 1000
|
||
local WAIT_MAX = 3000
|
||
local PERFECT = 1000
|
||
|
||
local state = "attract"
|
||
local goAt = 0
|
||
local score = 0
|
||
local best = saveGet("best", 0)
|
||
local saying = "Button 1 to play."
|
||
local top = nil
|
||
local standing = nil
|
||
|
||
|
||
local function whyNotPosted()
|
||
if not singeGetGameId() then
|
||
return "this game has no GAME_ID."
|
||
end
|
||
return "this machine is not signed in."
|
||
end
|
||
|
||
|
||
local function fetchBoard()
|
||
top = nil
|
||
standing = nil
|
||
saying = "Asking the service..."
|
||
scoreBoard(function(ok, result)
|
||
if not ok then
|
||
saying = "No board: " .. tostring(result)
|
||
return
|
||
end
|
||
top = result.top
|
||
standing = result.standing
|
||
if #top == 0 then
|
||
saying = "The board is empty. You could be first."
|
||
else
|
||
saying = ""
|
||
end
|
||
end)
|
||
end
|
||
|
||
|
||
local function finish(reaction)
|
||
score = PERFECT - reaction
|
||
if score < 0 then
|
||
score = 0
|
||
end
|
||
if score > best then
|
||
best = score
|
||
saveSet("best", best)
|
||
end
|
||
state = "result"
|
||
if scoreSubmit(score) then
|
||
fetchBoard()
|
||
else
|
||
saying = "Kept here only: " .. whyNotPosted()
|
||
end
|
||
end
|
||
|
||
|
||
local function play()
|
||
state = "waiting"
|
||
goAt = singeGetTicks() + math.random(WAIT_MIN, WAIT_MAX)
|
||
score = 0
|
||
top = nil
|
||
standing = nil
|
||
saying = ""
|
||
scoreBegin()
|
||
end
|
||
|
||
|
||
function onInputPressed(what)
|
||
if what ~= SWITCH_BUTTON1 then
|
||
return
|
||
end
|
||
if state == "waiting" then
|
||
state = "result"
|
||
score = 0
|
||
saying = "Too soon. Nothing posted."
|
||
elseif state == "go" then
|
||
finish(singeGetTicks() - goAt)
|
||
else
|
||
play()
|
||
end
|
||
end
|
||
|
||
|
||
function onOverlayUpdate()
|
||
scoreUpdate()
|
||
|
||
if state == "waiting" and singeGetTicks() >= goAt then
|
||
state = "go"
|
||
end
|
||
|
||
overlayClear()
|
||
overlayPrint(2, 0, "REACTION TEST")
|
||
|
||
if state == "attract" then
|
||
overlayPrint(2, 2, "Button 1 to play.")
|
||
elseif state == "waiting" then
|
||
overlayPrint(2, 2, "Wait for it...")
|
||
elseif state == "go" then
|
||
overlayPrint(2, 2, "NOW!")
|
||
else
|
||
overlayPrint(2, 2, "You scored " .. score .. ". Button 1 plays again.")
|
||
end
|
||
|
||
overlayPrint(2, 4, "Best on this machine: " .. best)
|
||
overlayPrint(2, 5, "Posting as: " .. (scorePlayerName() or "nobody yet"))
|
||
overlayPrint(2, 6, "Waiting to send: " .. scoreWaiting())
|
||
overlayPrint(2, 8, saying)
|
||
|
||
if top then
|
||
for place, row in ipairs(top) do
|
||
if place > 5 then
|
||
break
|
||
end
|
||
overlayPrint(2, 9 + place, place .. ". " .. row.name .. " " .. row.value)
|
||
end
|
||
end
|
||
if standing then
|
||
overlayPrint(2, 16, "You are " .. standing.rank .. " of " .. standing.players .. ".")
|
||
end
|
||
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
Lancez-le avec `Singe -R reaction`. Sur une machine qui ne s'est jamais
|
||
connectée, il se joue, il garde votre meilleur score, et il dit `Kept here only: this machine is not signed in.`
|
||
C'est l'exécution importante, et c'est celle qu'auront la plupart de vos
|
||
joueurs.
|
||
|
||
[#_what_just_happened_23]
|
||
=== Ce qui vient de se passer
|
||
|
||
[source,lua]
|
||
----
|
||
local top = nil
|
||
local standing = nil
|
||
----
|
||
|
||
Deux variables qui valent `nil` la plupart du temps. `nil` veut dire « rien
|
||
ici », et c'est ce que contient une variable avant qu'on y mette quoi que ce
|
||
soit. Celles-ci contiennent le tableau quand il y en a un, et `nil` quand il
|
||
n'y en a pas : pendant que la requête est en route, après un échec, et avant
|
||
la première partie. Le code de dessin demande `if top then` et ne dessine rien
|
||
quand il n'y a rien, ce qui est la forme de presque tout le code réseau que
|
||
vous écrirez.
|
||
|
||
[source,lua]
|
||
----
|
||
saying = "Asking the service..."
|
||
scoreBoard(function(ok, result)
|
||
----
|
||
|
||
L'ordre compte. La ligne de texte est réglée *avant* l'appel, parce que
|
||
l'appel rend la main aussitôt et que le callback peut ne s'exécuter que des
|
||
secondes plus tard. Si vous ne réglez le texte que dans le callback, l'écran
|
||
montre le message de la partie précédente jusqu'à ce que la réponse arrive, ce
|
||
qui est exactement le mauvais message au mauvais moment.
|
||
|
||
[source,lua]
|
||
----
|
||
if not ok then
|
||
saying = "No board: " .. tostring(result)
|
||
return
|
||
end
|
||
----
|
||
|
||
L'échec vient en premier, et il est court. `tostring` transforme ce qui est
|
||
revenu en texte, pour que le joindre avec `..` ne puisse pas échouer ;
|
||
`result` est une chaîne quand `ok` vaut false, mais une ligne de code de
|
||
dessin qui fait confiance au type de quelque chose reçu d'un serveur est une
|
||
ligne qui attend de planter. Le joueur voit une phrase, et le jeu continue.
|
||
|
||
[source,lua]
|
||
----
|
||
if scoreSubmit(score) then
|
||
fetchBoard()
|
||
else
|
||
saying = "Kept here only: " .. whyNotPosted()
|
||
end
|
||
----
|
||
|
||
Que `scoreSubmit` réponde `false` n'est pas une erreur. C'est un fait
|
||
concernant cette machine, et les deux faits qu'il peut signifier valent d'être
|
||
distingués, ce que fait `whyNotPosted` : un `GAME_ID` manquant est *votre*
|
||
erreur, et une machine non connectée n'est l'erreur de personne. Le meilleur
|
||
score local a déjà été sauvegardé, deux lignes plus haut, avant toute
|
||
tentative. Sauvegardez d'abord, envoyez ensuite, toujours.
|
||
|
||
[source,lua]
|
||
----
|
||
overlayPrint(2, 6, "Waiting to send: " .. scoreWaiting())
|
||
----
|
||
|
||
La file d'attente, à l'écran. Sur une machine qui a une connexion, cela
|
||
affiche `0` pour toujours et personne ne le remarque. Sur une machine qui n'en
|
||
a pas, le nombre monte, et le joueur sait que le jeu garde ses scores au lieu
|
||
de les avaler.
|
||
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
scoreUpdate()
|
||
----
|
||
|
||
Première ligne de l'image, avant tout le reste. Mettez-la à un endroit où aucun
|
||
`if` ne peut la sauter, parce qu'un `scoreUpdate` qui ne s'exécute que sur
|
||
l'écran de fin de partie, c'est une file d'attente qui ne se vide que sur
|
||
l'écran de fin de partie.
|
||
|
||
[#_try_it_28]
|
||
=== Essayez
|
||
|
||
. *Débranchez la prise.* Déconnectez la machine du réseau, jouez quelques
|
||
parties, et regardez le nombre `Waiting to send`. Rebranchez-la et
|
||
regardez-le descendre sans que vous fassiez quoi que ce soit.
|
||
. *Supprimez l'identifiant.* Retirez `GAME_ID` de votre `games.dat` et
|
||
relancez. Le message change. Remettez-le.
|
||
. *Montrez plus du tableau.* La boucle s'arrête à cinq lignes. L'overlay fait
|
||
par défaut dix-huit lignes de caractères de haut, alors calculez combien vous
|
||
pouvez vraiment en faire tenir, et déplacez la ligne `standing` s'il le faut.
|
||
. *Nommez le tableau.* `scoreSubmit(score, "reaction")` et
|
||
`scoreBoard(onDone, "reaction")` utilisent un tableau de ce nom au lieu de
|
||
celui qui porte le nom de votre script. Envoyez vers deux tableaux depuis le
|
||
même jeu -- par exemple un pour le score et un pour la réaction la plus
|
||
rapide -- et laissez le bouton 2 choisir lequel est affiché.
|
||
. *Rendez l'attente visible.* Entre l'appel à `scoreBoard` et l'exécution du
|
||
callback, dessinez un point qui bouge. Puis découvrez combien de temps cela
|
||
dure vraiment sur votre connexion, et si ça valait la peine de le dessiner.
|
||
|
||
[#_break_it_on_purpose_27]
|
||
=== Cassez-le exprès
|
||
|
||
Retirez la première ligne de `onOverlayUpdate`, pour que l'appel à
|
||
`scoreUpdate()` disparaisse, et jouez une partie.
|
||
|
||
Il ne se passe rien. Pas d'erreur, pas de message, pas de tableau, et le
|
||
nombre `Waiting to send` monte et reste en haut pour toujours. Le jeu se joue
|
||
parfaitement.
|
||
|
||
C'est la pire sorte de bug, et elle vaut la peine d'être rencontrée une fois
|
||
exprès : une étape manquante qui produit du silence plutôt qu'une plainte. Il
|
||
n'y a aucun numéro de ligne à regarder, parce que rien ne s'est mal passé --
|
||
vous avez demandé du travail et ne lui avez jamais laissé l'occasion de se
|
||
faire. Quand quelque chose dans Singe qui prend un callback ne vous rappelle
|
||
jamais, la première question est toujours de savoir si ce qui le fait avancer
|
||
s'exécute à chaque image.
|
||
|
||
L'autre, que vous rencontrerez aussi, est plus bruyant. Remettez
|
||
`scoreUpdate()`, supprimez la ligne `dofile` en haut, et lancez :
|
||
|
||
----
|
||
Error executing function 'onOverlayUpdate': reaction/reaction.singe:89: attempt to call a nil value (global 'scoreUpdate')
|
||
stack traceback:
|
||
reaction/reaction.singe:89: in function 'onOverlayUpdate'
|
||
----
|
||
|
||
La ligne 89 est le `scoreUpdate()` que vous avez remis, une ligne plus haut
|
||
maintenant que le `dofile` a disparu. Lisez la première ligne, comme
|
||
toujours : la fonction que le moteur appelait, le fichier, la ligne et la
|
||
plainte. Encore `nil` -- le nom `scoreUpdate` ne
|
||
contient rien, parce que le fichier qui le définit n'a jamais été chargé. Les
|
||
six appels vivent tous dans `Singe/Framework.singe`.
|
||
|
||
[#_what_you_learned_28]
|
||
=== Ce que vous avez appris
|
||
|
||
* Le service détient un compte, un catalogue et des tableaux des meilleurs
|
||
scores. Ce n'est pas du multijoueur, ni un nuage de sauvegardes, et il n'est
|
||
jamais obligatoire.
|
||
* Le compte appartient à la machine, et on s'y connecte depuis les outils de
|
||
service du menu. Un jeu ne demande jamais de mot de passe.
|
||
* Un tableau est rangé sous `GAME_ID` dans `games.dat`, un UUID qui ne doit
|
||
jamais changer. `singeGetGameId()` le lit.
|
||
* `scoreSubmit` met un score en file d'attente sur le disque et rend la main
|
||
aussitôt ; la file d'attente survit à l'extinction de la machine.
|
||
* `scoreUpdate()` doit s'exécuter à chaque image, sinon rien de ce que fait le
|
||
service ne se termine jamais.
|
||
* `scoreBoard` donne sa réponse à une fonction que vous écrivez, et qui
|
||
s'exécute plus tard. Dessinez quelque chose en attendant.
|
||
* Chaque appel réseau peut échouer, et échouer n'est pas une erreur. Dites une
|
||
courte ligne et continuez à jouer.
|
||
* Un score envoyé est une déclaration, et le service le traite comme tel.
|
||
* Gardez le meilleur score local. C'est celui qui marche toujours.
|
||
|
||
[#_next_time_28]
|
||
=== La prochaine fois
|
||
|
||
Un meilleur score, c'est un nombre, et la sauvegarde de la leçon douze est le
|
||
bon endroit pour lui. La leçon vingt-neuf est pour tout le reste de ce qu'un
|
||
jeu veut garder : chaque manche jouée par qui que ce soit, les niveaux que vous
|
||
livrez, et les questions que vous voulez poser aux deux. Elle reprend ce test
|
||
de réflexes et lui donne une base de données.
|