singe/docs/it/lessons/28-online.adoc

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.