579 lines
23 KiB
Text
579 lines
23 KiB
Text
[#_lesson_28_online]
|
|
== Lezione 28: Online
|
|
|
|
image::learn/28-online.png[La lezione finita, 480]
|
|
|
|
Nella dodicesima lezione hai salvato un punteggio record. La mattina dopo è
|
|
ancora lì, che è quasi tutto quello a cui serve un punteggio record, ma è un
|
|
punteggio record su una sola macchina. Questa lezione mette un punteggio su una
|
|
classifica che possono vedere anche gli altri.
|
|
|
|
È anche la lezione sul fallimento. Tutto il resto che hai scritto fallisce per
|
|
motivi che puoi trovare: manca un file, un numero è sbagliato, il nome di una
|
|
funzione è scritto male. La rete fallisce per motivi che nessuno nella stanza
|
|
può vedere. Qualcuno ha staccato un cavo in un edificio in cui non sei mai
|
|
stato, e il tuo gioco deve continuare a girare. Quasi tutto quello che segue
|
|
parla di questo.
|
|
|
|
[#_what_the_service_is]
|
|
=== Che cos'è il servizio
|
|
|
|
Singe può parlare con un *servizio master*, un server che custodisce tre cose:
|
|
|
|
* un *account*, che appartiene alla macchina e non a un singolo gioco;
|
|
* un *catalogo* di giochi da scaricare e aggiornare;
|
|
* *classifiche dei punteggi record*, una o più per gioco.
|
|
|
|
Il servizio predefinito è `master.singeengine.com`, e un gestore che ha un
|
|
servizio suo indirizza la macchina altrove. La sezione del manuale sul servizio
|
|
online descrive entrambe le parti.
|
|
|
|
Ora la parte che tutti saltano. Il servizio *non* è multigiocatore: non c'è
|
|
modo che due giocatori stiano nella stessa partita. Non è un cloud per i
|
|
salvataggi; il salvataggio della dodicesima lezione resta sulla macchina. Non è
|
|
una protezione dalla copia, e niente al suo interno impedisce a qualcuno di
|
|
giocare al tuo gioco. Non è obbligatorio: un gioco che non accede mai funziona
|
|
esattamente come ha sempre fatto, ed è l'unica impostazione predefinita sensata
|
|
per qualcosa che ha bisogno di una connessione funzionante.
|
|
|
|
Quindi la classifica è un extra. Tieni il punteggio record locale della
|
|
dodicesima lezione, continua a mostrarlo, e lascia che la classifica online sia
|
|
la cosa che a volte c'è.
|
|
|
|
[#_signing_in_happens_once_and_not_in_your_game]
|
|
=== L'accesso si fa una volta, e non nel tuo gioco
|
|
|
|
L'account appartiene alla macchina. Lo si crea e ci si accede dal menu
|
|
incluso, dietro il tasto assegnato a `INPUT_SERVICE`, nello strumento chiamato
|
|
*Online Account*. Il token che ottiene viene scritto in `master.dat` nella
|
|
radice dei dati, un livello sopra la cartella dei dati di ogni gioco, così ogni
|
|
gioco sulla macchina invia i punteggi con lo stesso accesso.
|
|
|
|
Il tuo gioco non chiede mai un indirizzo email e non chiede mai una password.
|
|
Non c'è nessuna funzione per farlo, ed è voluto: un giocatore che scrive una
|
|
password in un gioco che ha scaricato non ha modo di sapere dove va a finire.
|
|
|
|
Quello con cui un giocatore compare su una classifica è un *nickname*: da tre a
|
|
venti caratteri tra lettere, cifre, trattini e trattini bassi, che iniziano e
|
|
finiscono con una lettera o una cifra. Niente spazi, niente punti e niente
|
|
`@`, così un nickname non può nemmeno sembrare un indirizzo email. È tenuto
|
|
separato dall'account, è l'unica cosa di un giocatore che gli altri giocatori
|
|
vedono, e si sceglie una volta sola -- un gestore può cambiarne uno, un
|
|
giocatore no, perché una classifica i cui nomi continuano a cambiare non è
|
|
davvero la storia di nessuno. Finché non è stato scelto un nickname i punteggi
|
|
vengono rifiutati, dato che un punteggio senza il nome di nessuno sopra non sta
|
|
su una classifica in nessun senso utile.
|
|
|
|
Lo strumento Online Account mostra il nome con cui questa macchina invia i
|
|
punteggi, e il tuo gioco lo legge con `scorePlayerName()`, che risponde `nil`
|
|
quando non ce n'è uno. Meglio saperlo prima che ti lasci perplesso: un
|
|
punteggio inviato senza un nickname dietro viene rifiutato dal *servizio*, non
|
|
da `scoreSubmit`, quindi al tuo gioco non viene detto niente. Se
|
|
`scorePlayerName()` è `nil`, dillo sullo schermo finché c'è ancora qualcuno lì
|
|
a leggerlo.
|
|
|
|
[#_your_game_needs_an_id]
|
|
=== Il tuo gioco ha bisogno di un id
|
|
|
|
Una classifica deve essere conservata sotto qualcosa. Quel qualcosa è
|
|
`GAME_ID` nella voce del tuo gioco in `games.dat`, il file che hai creato nella
|
|
tredicesima lezione:
|
|
|
|
[source,lua]
|
|
----
|
|
GAME_ID = "6f1e7b62-0a4e-4d9c-9b2f-1c7a5e3d8a10"
|
|
----
|
|
|
|
È un UUID, esattamente in quella forma: otto cifre esadecimali, tre gruppi di
|
|
quattro, poi dodici, separati da trattini. Generane uno come preferisci e
|
|
incollalo; non importa quale ti capita, solo che non cambi mai più. La voce del
|
|
Riferimento per `singeGetGameId` spiega perché non viene accettato nient'altro.
|
|
|
|
`singeGetGameId()` lo restituisce, o `nil` quando la voce non ne ha uno. Un
|
|
gioco senza id si può giocare ma non può finire in classifica, e chiedere prima
|
|
è il modo per evitare di offrire una classifica che non potrà mai funzionare:
|
|
|
|
[source,lua]
|
|
----
|
|
if singeGetGameId() then
|
|
-- showLeaderboardButton è una tua funzione, non una di Singe.
|
|
showLeaderboardButton()
|
|
end
|
|
----
|
|
|
|
[#_the_six_calls]
|
|
=== Le sei chiamate
|
|
|
|
Tutto quello che un gioco fa con il servizio sono sei funzioni, e vengono da
|
|
`Singe/Framework.singe`, quindi qui il `dofile` in cima al tuo script non è
|
|
facoltativo:
|
|
|
|
[source,lua]
|
|
----
|
|
dofile("Singe/Framework.singe")
|
|
----
|
|
|
|
`scoreBegin()` dice che sta cominciando una partita. `scoreSubmit(value)`
|
|
mette in coda un punteggio. `scoreBoard(onDone)` recupera la classifica.
|
|
`scorePlayerName()` è il nickname. `scoreWaiting()` è quanti punteggi non sono
|
|
ancora partiti. E `scoreUpdate()` deve girare una volta per fotogramma, da
|
|
`onOverlayUpdate`, altrimenti nessuna delle altre arriva mai in fondo.
|
|
|
|
Quest'ultima è tutto il trucco di questa lezione, quindi la diciamo due volte.
|
|
Niente di tutto questo parla con la rete quando lo chiami. `scoreSubmit` scrive
|
|
in una coda e ritorna subito. `scoreBoard` si ricorda che cosa vuoi e ritorna
|
|
subito. Il lavoro vero avviene un pezzetto alla volta dentro `scoreUpdate`, tra
|
|
un fotogramma e l'altro, ed è per questo che il tuo gioco non va mai a scatti
|
|
quando la linea è lenta. Togli `scoreUpdate` e non si rompe niente, non c'è
|
|
nessun errore, e non succede mai niente.
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
scoreUpdate()
|
|
overlayClear()
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#_posting_a_score]
|
|
=== Inviare un punteggio
|
|
|
|
Alla fine di una partita:
|
|
|
|
[source,lua]
|
|
----
|
|
if scoreSubmit(score) then
|
|
fetchBoard()
|
|
else
|
|
saying = "Kept here only: " .. whyNotPosted()
|
|
end
|
|
----
|
|
|
|
`scoreSubmit` risponde `false` quando non c'è un account o il gioco non ha un
|
|
`GAME_ID`, e `true` quando il punteggio è entrato nella coda. Nota che cosa
|
|
*non* fa il ramo `else`: non butta via il punteggio, non si profonde in scuse e
|
|
non impedisce al giocatore di cominciare un'altra partita. Dice una sola riga
|
|
breve, perché il giocatore non ha sbagliato niente e da qui non può rimediare.
|
|
|
|
La coda è il motivo per cui tutto questo è sicuro. Un punteggio in coda viene
|
|
scritto su disco, quindi sopravvive allo spegnimento della macchina, e viene
|
|
consegnato al servizio la volta dopo che c'è una connessione -- un'ora dopo, o
|
|
martedì prossimo. Ecco perché un cabinato in una sala con un wifi scadente
|
|
finisce comunque sulla classifica.
|
|
|
|
Sii onesto con te stesso sul limite di quella promessa: la coda conserva i
|
|
punteggi per una macchina che *ha* un account. Se nessuno ha mai effettuato
|
|
l'accesso, `scoreSubmit` risponde `false` e il punteggio non viene conservato
|
|
per dopo, perché non c'è nessuno per cui conservarlo. `scoreWaiting()` ti dice
|
|
quanti sono ancora in coda, e mostrare quel numero da qualche parte è una
|
|
gentilezza: è la differenza tra «il servizio è rotto» e «tre dei tuoi punteggi
|
|
aspettano che torni internet».
|
|
|
|
`scoreBegin()` merita un paragrafo tutto suo. Chiamala quando comincia una
|
|
partita, e il servizio annota il momento con il proprio orologio. Il punteggio
|
|
che segue porta con sé quanto è durata la partita, misurato sul server, ed è
|
|
l'unico numero di un invio che non è passato dalla macchina del giocatore.
|
|
Niente viene rifiutato in base a quello: viene mostrato a chi gestisce il
|
|
servizio, accanto al punteggio, e decide una persona. Una macchina che era
|
|
offline quando è cominciata la partita non registra nessun tempo, il che è
|
|
onesto, non rotto.
|
|
|
|
E visto che siamo onesti: un punteggio inviato è una *dichiarazione*. Il gioco
|
|
gira sul computer del giocatore, quindi niente di ciò che può fare il client lo
|
|
rende qualcosa di più, e una chiave segreta inclusa nel tuo gioco sembrerebbe
|
|
soltanto sicurezza. Il servizio registra chi ha dichiarato che cosa, limita la
|
|
velocità con cui possono arrivare i punteggi e segnala quelli assurdi perché li
|
|
guardi una persona. Progetta la tua classifica sapendolo.
|
|
|
|
[#_reading_the_board_back]
|
|
=== Rileggere la classifica
|
|
|
|
`scoreBoard` è la prima funzione di questo libro che prende una funzione come
|
|
argomento.
|
|
|
|
[source,lua]
|
|
----
|
|
scoreBoard(function(ok, result)
|
|
...
|
|
end)
|
|
----
|
|
|
|
Hai scritto delle funzioni nella quinta lezione e hai dato loro un nome. Questa
|
|
non ha nome: è scritta dove viene usata, passata direttamente a `scoreBoard` e
|
|
chiamata più tardi -- magari secondi dopo -- quando arriva la risposta. È di
|
|
nuovo una *callback*, la stessa idea di `onOverlayUpdate`, solo che stavolta
|
|
scegli tu chi la chiama.
|
|
|
|
`ok` è `true` o `false`. Quando è `false`, `result` è una breve stringa che
|
|
dice perché. Quando è `true`, `result` è una tabella con due parti:
|
|
|
|
* `result.top`, un elenco di righe, ognuna con un `name` e un `value`;
|
|
* `result.standing`, il `value` di questo giocatore, il suo `rank` e il numero
|
|
di `players` sulla classifica -- che può mancare, se questo giocatore non ci
|
|
ha niente sopra.
|
|
|
|
Tra la chiamata e la callback il tuo gioco continua a girare e a disegnare.
|
|
Quell'intervallo è reale e il giocatore lo vede, quindi mettici qualcosa. Lo
|
|
script qui sotto imposta una riga di testo a `"Asking the service..."` prima
|
|
della chiamata, e la callback sostituisce quella riga con la classifica o con
|
|
il motivo per cui non c'è. Una schermata che non dice niente per quattro
|
|
secondi sembra rotta; una schermata che dice che cosa sta facendo è solo
|
|
lenta.
|
|
|
|
[#_the_catalogue]
|
|
=== Il catalogo
|
|
|
|
La terza cosa che fa il servizio è elencare giochi. Lo strumento *Get Games*
|
|
del menu mostra tutto quello che il servizio offre, segna ciò che è già
|
|
installato su questa macchina, e lo scarica, lo aggiorna o lo rimuove. Un
|
|
download viene controllato rispetto all'impronta pubblicata dal catalogo prima
|
|
di sostituire qualsiasi cosa, così un trasferimento arrivato danneggiato
|
|
fallisce invece di installare un gioco che non partirà. Un gioco che il
|
|
servizio ha ritirato continua a funzionare e appare come installato ma non più
|
|
offerto: smette soltanto di ricevere aggiornamenti, perché chi ha un gioco ce
|
|
l'ha.
|
|
|
|
Il tuo gioco non chiama niente di tutto questo, e non dovrebbe. Il catalogo ti
|
|
riguarda come autore di un gioco più che come chi scrive uno script: è dove va
|
|
il tuo file `.game` quando lo pubblichi, che è l'argomento della trentesima
|
|
lezione, ed è un altro motivo per cui il tuo `games.dat` ha bisogno di un
|
|
`GAME_ID` che non cambi mai. Se un giorno vuoi davvero l'elenco da dentro uno
|
|
script, `Singe/Master.singe` è il modulo che sta sotto tutto questo, e vale la
|
|
pena leggerlo una volta.
|
|
|
|
[#_what_the_service_knows_about_a_player]
|
|
=== Che cosa sa il servizio di un giocatore
|
|
|
|
Se distribuisci un gioco che invia punteggi, stai consegnando i dati di
|
|
qualcun altro a un server, e questa è una tua responsabilità anche se il server
|
|
non l'hai scritto tu. Quindi dillo chiaramente, nella schermata di informazioni
|
|
o nel file leggimi del tuo gioco:
|
|
|
|
* Il servizio conserva un *indirizzo email* e una password per l'account.
|
|
L'indirizzo serve a recuperare una password dimenticata e a nient'altro. Non
|
|
viene mai mostrato agli altri giocatori e non si può cambiare in seguito.
|
|
* Il *nickname* è quello che compare sulle classifiche. È tenuto separato
|
|
dall'account apposta, così che gli altri giocatori non vedano mai un
|
|
indirizzo, e viene controllato rispetto a un elenco di nomi che nessuno
|
|
dovrebbe rivendicare e di parole che non stanno bene su uno schermo in una
|
|
sala pubblica.
|
|
* Un *punteggio* porta con sé il suo valore, la classifica su cui sta, l'ora
|
|
registrata dal servizio e il nickname. Tutto qui.
|
|
* Il catalogo sa *quali giochi sono installati* sulla macchina, e in quale
|
|
versione, perché è così che ti offre un aggiornamento.
|
|
|
|
Quello che nessuna di queste cose fa è seguire un giocatore da un gioco
|
|
all'altro, vendere qualcosa o frugare nella macchina in cerca di qualcosa che
|
|
non hai mandato tu. Il tuo gioco non dovrebbe allungare l'elenco. Se aggiungi
|
|
informazioni extra a un punteggio -- `scoreSubmit` prende un terzo argomento
|
|
facoltativo apposta -- fa' che riguardino il gioco.
|
|
|
|
[#_the_script]
|
|
=== Lo script
|
|
|
|
Un test di reazione: aspetta la parola, premi il pulsante, il punteggio è
|
|
quello che resta di mille. È un piccolo gioco, e tutto il resto è la parte
|
|
online.
|
|
|
|
Crea una cartella chiamata `reaction` e metti questo in `reaction.singe`, al suo
|
|
interno. Dai alla cartella un `games.dat` come quello della tredicesima lezione,
|
|
che indichi `reaction/reaction.singe`, con nella sua voce la riga `GAME_ID` di
|
|
prima.
|
|
|
|
[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
|
|
----
|
|
|
|
Eseguilo con `Singe -R reaction`. Su una macchina che non ha mai effettuato
|
|
l'accesso, il gioco gira, tiene il tuo punteggio migliore e dice
|
|
`Kept here only: this machine is not signed in.` Questa è l'esecuzione importante, ed è quella che avrà la maggior
|
|
parte dei tuoi giocatori.
|
|
|
|
[#_what_just_happened_23]
|
|
=== Cos'è appena successo
|
|
|
|
[source,lua]
|
|
----
|
|
local top = nil
|
|
local standing = nil
|
|
----
|
|
|
|
Due variabili che sono `nil` quasi sempre. `nil` vuol dire «qui non c'è
|
|
niente», ed è quello che contiene una variabile prima che ci si metta dentro
|
|
qualcosa. Queste contengono la classifica quando ce n'è una, e `nil` quando non
|
|
c'è: mentre la richiesta è in viaggio, dopo un errore e prima della prima
|
|
partita. Il codice che disegna chiede `if
|
|
top then` e non disegna niente quando non c'è niente, che è la forma di quasi
|
|
ogni pezzo di codice di rete che scriverai.
|
|
|
|
[source,lua]
|
|
----
|
|
saying = "Asking the service..."
|
|
scoreBoard(function(ok, result)
|
|
----
|
|
|
|
L'ordine conta. La riga di testo viene impostata *prima* della chiamata, perché
|
|
la chiamata ritorna subito e la callback potrebbe non partire per qualche
|
|
secondo. Se imposti il testo solo dentro la callback, lo schermo mostra il
|
|
messaggio della partita precedente finché non arriva la risposta, cioè
|
|
esattamente il messaggio sbagliato nel momento esattamente sbagliato.
|
|
|
|
[source,lua]
|
|
----
|
|
if not ok then
|
|
saying = "No board: " .. tostring(result)
|
|
return
|
|
end
|
|
----
|
|
|
|
Il fallimento viene per primo, ed è breve. `tostring` trasforma in testo
|
|
qualunque cosa sia tornata, così che unirla con `..` non possa fallire;
|
|
`result` è una stringa quando `ok` è falso, ma una riga di codice di disegno
|
|
che si fida del tipo di qualcosa ricevuto da un server è una riga che aspetta
|
|
solo di andare in crash. Il giocatore vede una frase, e il gioco va avanti.
|
|
|
|
[source,lua]
|
|
----
|
|
if scoreSubmit(score) then
|
|
fetchBoard()
|
|
else
|
|
saying = "Kept here only: " .. whyNotPosted()
|
|
end
|
|
----
|
|
|
|
`scoreSubmit` che risponde `false` non è un errore. È un fatto che riguarda
|
|
questa macchina, e vale la pena distinguere i due fatti che può voler dire, che
|
|
è quello che fa `whyNotPosted`: un `GAME_ID` mancante è un errore *tuo*, e una
|
|
macchina che non ha effettuato l'accesso non è colpa di nessuno. Il punteggio
|
|
migliore locale era già stato salvato, due righe sopra, prima di tentare tutto
|
|
questo. Prima salva, poi invia, sempre.
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(2, 6, "Waiting to send: " .. scoreWaiting())
|
|
----
|
|
|
|
La coda, sullo schermo. Su una macchina con una connessione segna `0` per
|
|
sempre e nessuno ci fa caso. Su una macchina senza, sale, e il giocatore sa che
|
|
il gioco sta conservando i suoi punteggi invece di mangiarseli.
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
scoreUpdate()
|
|
----
|
|
|
|
Prima riga del fotogramma, prima di qualsiasi altra cosa. Mettila in un punto in
|
|
cui un `if` non possa saltarla, perché uno `scoreUpdate` che gira solo nella
|
|
schermata di game over è una coda che si svuota solo nella schermata di game
|
|
over.
|
|
|
|
[#_try_it_28]
|
|
=== Prova tu
|
|
|
|
. *Stacca la spina.* Scollega la macchina dalla rete, gioca qualche partita e
|
|
guarda il numero di `Waiting to send`. Ricollegala e guardalo scendere senza
|
|
che tu faccia niente.
|
|
. *Cancella l'id.* Togli `GAME_ID` dal tuo `games.dat` ed eseguilo di nuovo.
|
|
Il messaggio cambia. Rimettilo.
|
|
. *Mostra più classifica.* Il ciclo si ferma a cinque righe. L'overlay è alto
|
|
diciotto righe di caratteri per impostazione predefinita, quindi calcola
|
|
quante ne puoi far stare davvero, e sposta la riga di `standing` se serve.
|
|
. *Dai un nome alla classifica.* `scoreSubmit(score, "reaction")` e
|
|
`scoreBoard(onDone, "reaction")` usano una classifica con quel nome invece di
|
|
quella che porta il nome del tuo script. Invia a due classifiche dallo stesso gioco -- per esempio, una per
|
|
il punteggio e una per la singola reazione più rapida -- e fai che il
|
|
pulsante 2 cambi quale viene mostrata.
|
|
. *Rendi visibile l'attesa.* Tra la chiamata a `scoreBoard` e il momento in cui
|
|
parte la callback, disegna un puntino che si muove. Poi scopri quanto dura
|
|
davvero sulla tua connessione, e se valeva la pena disegnarlo.
|
|
|
|
[#_break_it_on_purpose_27]
|
|
=== Rompilo apposta
|
|
|
|
Togli la prima riga di `onOverlayUpdate`, così la chiamata a `scoreUpdate()`
|
|
sparisce, e gioca una partita.
|
|
|
|
Non succede niente. Nessun errore, nessun messaggio, nessuna classifica, e il
|
|
numero di `Waiting to send` sale e resta su per sempre. Il gioco funziona
|
|
perfettamente.
|
|
|
|
Questo è il peggior tipo di bug, e vale la pena incontrarlo una volta apposta:
|
|
un passo mancante che produce silenzio invece di una lamentela. Non c'è nessun
|
|
numero di riga da guardare, perché non è andato storto niente -- hai chiesto
|
|
del lavoro e poi non gli hai mai dato occasione di succedere. Quando qualcosa in
|
|
Singe che prende una callback non ti richiama mai, la prima domanda è sempre se
|
|
la cosa che lo fa andare avanti gira a ogni fotogramma.
|
|
|
|
L'altro che incontrerai è più rumoroso. Rimetti `scoreUpdate()`, cancella la
|
|
riga del `dofile` in cima ed eseguilo:
|
|
|
|
----
|
|
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 riga 89 è lo `scoreUpdate()` che hai rimesso, una riga più in alto ora che il
|
|
`dofile` non c'è più. Leggi la prima riga come sempre: la funzione che il motore stava chiamando, il
|
|
file, la riga e la lamentela. Di nuovo `nil` -- il nome `scoreUpdate` non
|
|
contiene niente, perché il file che lo definisce non è mai stato caricato.
|
|
Tutte e sei queste chiamate vivono in `Singe/Framework.singe`.
|
|
|
|
[#_what_you_learned_28]
|
|
=== Cosa hai imparato
|
|
|
|
* Il servizio custodisce un account, un catalogo e le classifiche dei punteggi
|
|
record. Non è multigiocatore, non è un cloud per i salvataggi e non è mai
|
|
obbligatorio.
|
|
* L'account appartiene alla macchina e l'accesso si fa dagli strumenti di
|
|
servizio del menu. Un gioco non chiede mai una password.
|
|
* Una classifica è conservata sotto `GAME_ID` in `games.dat`, un UUID che non
|
|
deve cambiare mai. `singeGetGameId()` lo legge.
|
|
* `scoreSubmit` mette in coda un punteggio su disco e ritorna subito; la coda
|
|
sopravvive allo spegnimento della macchina.
|
|
* `scoreUpdate()` deve girare a ogni fotogramma, altrimenti niente di quello
|
|
che fa il servizio arriva mai in fondo.
|
|
* `scoreBoard` consegna la sua risposta a una funzione che scrivi tu, che gira
|
|
più tardi. Nel frattempo disegna qualcosa.
|
|
* Ogni chiamata di rete può fallire, e fallire non è un errore. Di' una riga
|
|
breve e continua a giocare.
|
|
* Un punteggio inviato è una dichiarazione, e il servizio lo tratta come tale.
|
|
* Tieni il punteggio record locale. È quello che funziona sempre.
|
|
|
|
[#_next_time_28]
|
|
=== La prossima volta
|
|
|
|
Un punteggio record è un numero solo, e il salvataggio della dodicesima lezione
|
|
è il posto giusto per lui. La ventinovesima lezione è per tutto il resto che un
|
|
gioco vuole conservare: ogni round giocato da chiunque, i livelli che
|
|
distribuisci, e le domande che vuoi fare a entrambi. Prende questo test di
|
|
reazione e gli dà un database.
|