569 lines
23 KiB
Text
569 lines
23 KiB
Text
[#_lesson_28_online]
|
|
== Lección 28: En línea
|
|
|
|
image::learn/28-online.png[La lección terminada, 480]
|
|
|
|
En la lección doce guardaste un récord. Sigue ahí a la mañana siguiente, que es
|
|
casi todo para lo que sirve un récord, pero es un récord de una sola máquina.
|
|
Esta lección pone una puntuación en una tabla de récords que otras personas
|
|
pueden ver.
|
|
|
|
También es la lección sobre los fallos. Todo lo demás que has escrito falla por
|
|
motivos que puedes encontrar: falta un archivo, un número está mal, una función
|
|
está mal escrita. La red falla por motivos que nadie en la habitación puede ver.
|
|
Alguien desenchufó un cable en un edificio en el que nunca has estado, y tu
|
|
juego tiene que seguir funcionando. Casi todo lo que viene ahora trata de eso.
|
|
|
|
[#_what_the_service_is]
|
|
=== Qué es el servicio
|
|
|
|
Singe puede hablar con un *servicio maestro*, un servidor que guarda tres
|
|
cosas:
|
|
|
|
* una *cuenta*, que pertenece a la máquina y no a ningún juego en concreto;
|
|
* un *catálogo* de juegos para descargar y actualizar;
|
|
* *tablas de récords*, una o más por juego.
|
|
|
|
El servicio por defecto es `master.singeengine.com`, y un operador que tenga el
|
|
suyo propio apunta la máquina a otro sitio. La sección del manual sobre el
|
|
servicio en línea describe las dos partes.
|
|
|
|
Ahora la parte que la gente se salta. El servicio *no* es multijugador: no hay
|
|
forma de que dos jugadores estén en la misma partida. No es una nube de
|
|
partidas guardadas; la partida guardada de la lección doce se queda en la
|
|
máquina. No es una protección anticopia, y nada en él impide que nadie juegue a
|
|
tu juego. No es obligatorio: un juego que nunca inicia sesión funciona
|
|
exactamente igual que siempre, que es el único valor por defecto sensato para
|
|
algo que necesita una conexión que funcione.
|
|
|
|
Así que la tabla de récords es un extra. Conserva el récord local de la lección
|
|
doce, sigue mostrándolo y deja que la tabla en línea sea eso que a veces está.
|
|
|
|
[#_signing_in_happens_once_and_not_in_your_game]
|
|
=== La sesión se inicia una vez, y no en tu juego
|
|
|
|
La cuenta pertenece a la máquina. Se crea, y se inicia sesión con ella, desde el
|
|
menú incluido, detrás de la tecla asignada a `INPUT_SERVICE`, en la herramienta
|
|
llamada *Online Account*. El token que obtiene se escribe en `master.dat` en la
|
|
raíz de datos, un nivel por encima de la carpeta de datos de cada juego, así que
|
|
todos los juegos de la máquina publican sus puntuaciones con la misma sesión.
|
|
|
|
Tu juego nunca pide una dirección de correo electrónico y nunca pide una
|
|
contraseña. No hay ninguna función para hacerlo, y es a propósito: un jugador
|
|
que escribe una contraseña en un juego que ha descargado no tiene forma de saber
|
|
adónde va.
|
|
|
|
Lo que aparece de un jugador en una tabla de récords es un *alias*: de tres a
|
|
veinte caracteres entre letras, dígitos, guiones y guiones bajos, empezando y
|
|
terminando con una letra o un dígito. Sin espacios, sin puntos y sin `@`, así
|
|
que un alias ni siquiera puede parecer una dirección de correo electrónico. Se
|
|
guarda aparte de la cuenta, es lo único de un jugador que ven los demás
|
|
jugadores, y se elige una vez -- un operador puede cambiarlo, un jugador no,
|
|
porque una tabla de récords cuyos nombres no paran de cambiar no es realmente la
|
|
historia de nadie. Hasta que se ha elegido un alias, las puntuaciones se
|
|
rechazan, ya que una puntuación sin el nombre de nadie no está en una tabla de
|
|
récords en ningún sentido útil.
|
|
|
|
La herramienta Online Account muestra el nombre con el que publica esta
|
|
máquina, y tu juego lo lee con `scorePlayerName()`, que responde `nil` cuando
|
|
no hay ninguno. Conviene saberlo antes de que te desconcierte: una puntuación
|
|
enviada sin alias detrás la rechaza el *servicio*, no `scoreSubmit`, así que a
|
|
tu juego no se le dice nada. Si `scorePlayerName()` es `nil`, dilo en pantalla
|
|
mientras todavía haya alguien delante para leerlo.
|
|
|
|
[#_your_game_needs_an_id]
|
|
=== Tu juego necesita un identificador
|
|
|
|
Una tabla de récords tiene que guardarse bajo algo. Ese algo es `GAME_ID` en la
|
|
entrada de tu juego en `games.dat`, el archivo que hiciste en la lección trece:
|
|
|
|
[source,lua]
|
|
----
|
|
GAME_ID = "6f1e7b62-0a4e-4d9c-9b2f-1c7a5e3d8a10"
|
|
----
|
|
|
|
Es un UUID, exactamente con esa forma: ocho dígitos hexadecimales, tres grupos
|
|
de cuatro y luego doce, separados por guiones. Genera uno como quieras y pégalo;
|
|
no importa cuál te salga, solo que no cambie nunca más. La entrada de la
|
|
referencia para `singeGetGameId` explica por qué no se acepta nada más.
|
|
|
|
`singeGetGameId()` lo devuelve, o `nil` cuando la entrada no tiene ninguno. Un
|
|
juego sin identificador se puede jugar pero no puede entrar en ninguna
|
|
clasificación, y preguntar primero es la forma de no ofrecer una tabla de
|
|
récords que no puede funcionar nunca:
|
|
|
|
[source,lua]
|
|
----
|
|
if singeGetGameId() then
|
|
-- showLeaderboardButton es una función tuya, no una de las de Singe.
|
|
showLeaderboardButton()
|
|
end
|
|
----
|
|
|
|
[#_the_six_calls]
|
|
=== Las seis llamadas
|
|
|
|
Todo lo que hace un juego con el servicio son seis funciones, y vienen de
|
|
`Singe/Framework.singe`, así que aquí el `dofile` del principio de tu script no
|
|
es opcional:
|
|
|
|
[source,lua]
|
|
----
|
|
dofile("Singe/Framework.singe")
|
|
----
|
|
|
|
`scoreBegin()` dice que empieza una partida. `scoreSubmit(value)` pone una
|
|
puntuación en la cola. `scoreBoard(onDone)` trae la tabla de récords.
|
|
`scorePlayerName()` es el alias. `scoreWaiting()` es cuántas puntuaciones no
|
|
han salido todavía. Y `scoreUpdate()` tiene que ejecutarse una vez por
|
|
fotograma, desde `onOverlayUpdate`, o ninguna de las demás termina nunca.
|
|
|
|
Esa última es todo el truco de esta lección, así que se dice dos veces. Nada de
|
|
esto habla con la red cuando lo llamas. `scoreSubmit` escribe en una cola y
|
|
vuelve enseguida. `scoreBoard` recuerda lo que quieres y vuelve enseguida. El
|
|
trabajo de verdad se hace a trocitos dentro de `scoreUpdate`, entre tus
|
|
fotogramas, y por eso tu juego nunca da tirones mientras la conexión va lenta.
|
|
Quita `scoreUpdate` y nada se rompe, nada da error, y nunca pasa nada.
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
scoreUpdate()
|
|
overlayClear()
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
[#_posting_a_score]
|
|
=== Publicar una puntuación
|
|
|
|
Al final de una partida:
|
|
|
|
[source,lua]
|
|
----
|
|
if scoreSubmit(score) then
|
|
fetchBoard()
|
|
else
|
|
saying = "Kept here only: " .. whyNotPosted()
|
|
end
|
|
----
|
|
|
|
`scoreSubmit` responde `false` cuando no hay cuenta o el juego no tiene
|
|
`GAME_ID`, y `true` cuando la puntuación ha entrado en la cola. Fíjate en lo que
|
|
*no* hace el `else`: no tira la puntuación, no se deshace en disculpas y no
|
|
impide que el jugador empiece otra partida. Dice una línea corta, porque el
|
|
jugador no ha hecho nada mal y no puede arreglarlo desde aquí.
|
|
|
|
La cola es la razón de que esto sea seguro. Una puntuación en la cola se escribe
|
|
en disco, así que sobrevive a que se apague la máquina, y se entrega al servicio
|
|
la próxima vez que haya conexión -- una hora después, o el martes que viene. Por
|
|
eso una recreativa en un local con un wifi malo acaba igualmente en la tabla de
|
|
récords.
|
|
|
|
Sé sincero contigo mismo sobre el límite de esa promesa: la cola guarda
|
|
puntuaciones de una máquina que *tiene* cuenta. Si nadie inició sesión nunca,
|
|
`scoreSubmit` responde `false` y la puntuación no se guarda para más tarde,
|
|
porque no hay nadie para quien guardarla. `scoreWaiting()` te dice cuántas
|
|
quedan en la cola, y mostrar ese número en algún sitio es un detalle: es la
|
|
diferencia entre «el servicio está roto» y «tres de tus puntuaciones están
|
|
esperando a que vuelva internet».
|
|
|
|
`scoreBegin()` merece un párrafo propio. Llámala cuando empiece una partida, y
|
|
el servicio apunta el momento según su propio reloj. La puntuación que venga
|
|
después lleva cuánto duró la partida, medido en el servidor, que es el único
|
|
número de un envío que no ha pasado por la máquina del jugador. No se rechaza
|
|
nada por él: se le muestra a quien gestiona el servicio, junto a la puntuación,
|
|
y decide una persona. Una máquina que estaba sin conexión cuando empezó la
|
|
partida no registra ningún tiempo, lo cual es sincero, no un fallo.
|
|
|
|
Y ya que estamos siendo sinceros: una puntuación enviada es una *afirmación*. El
|
|
juego se ejecuta en el ordenador del jugador, así que nada que pueda hacer el
|
|
cliente la convierte en algo más, y una clave secreta incluida en tu juego solo
|
|
aparentaría seguridad. El servicio registra quién afirmó qué, limita lo deprisa
|
|
que pueden llegar las puntuaciones y marca las disparatadas para que las mire
|
|
una persona. Diseña tu tabla de récords sabiéndolo.
|
|
|
|
[#_reading_the_board_back]
|
|
=== Leer la tabla de récords
|
|
|
|
`scoreBoard` es la primera función de este libro que recibe una función como
|
|
argumento.
|
|
|
|
[source,lua]
|
|
----
|
|
scoreBoard(function(ok, result)
|
|
...
|
|
end)
|
|
----
|
|
|
|
Escribiste funciones en la lección cinco y les diste nombre. Esta no tiene
|
|
nombre: se escribe donde se usa, se le pasa directamente a `scoreBoard` y se
|
|
llama más tarde -- quizá segundos más tarde -- cuando llega la respuesta. Eso es
|
|
otra vez un *callback*, la misma idea que `onOverlayUpdate`, salvo que esta vez
|
|
eliges tú quién lo llama.
|
|
|
|
`ok` es `true` o `false`. Cuando es `false`, `result` es una cadena corta que
|
|
dice por qué. Cuando es `true`, `result` es una tabla con dos partes:
|
|
|
|
* `result.top`, una lista de filas, cada una con un `name` y un `value`;
|
|
* `result.standing`, el `value` y el `rank` propios de este jugador y el número
|
|
de `players` de la tabla de récords -- que puede faltar, si este jugador no
|
|
tiene nada en ella.
|
|
|
|
Entre la llamada y el callback tu juego sigue funcionando y dibujando. Ese hueco
|
|
es real y el jugador lo ve, así que pon algo en él. El script de abajo pone una
|
|
línea de texto en `"Asking the service..."` antes de llamar, y el callback
|
|
sustituye esa línea por la tabla de récords o por el motivo de que no la haya.
|
|
Una pantalla que no dice nada durante cuatro segundos parece rota; una pantalla
|
|
que dice lo que está haciendo solo es lenta.
|
|
|
|
[#_the_catalogue]
|
|
=== El catálogo
|
|
|
|
Lo tercero que hace el servicio es listar juegos. La herramienta *Get Games* del
|
|
menú muestra todo lo que ofrece el servicio, marca lo que ya está instalado en
|
|
esta máquina y lo descarga, lo actualiza o lo quita. Una descarga se comprueba
|
|
con el hash que publicó el catálogo antes de sustituir nada, así que una
|
|
transferencia que llega dañada falla en lugar de instalar un juego que no
|
|
funcionará. Un juego que el servicio ha retirado sigue funcionando y aparece
|
|
como instalado pero ya no ofrecido: lo único que deja de pasar es que reciba
|
|
actualizaciones, porque quien tiene un juego lo tiene.
|
|
|
|
Tu juego no llama a nada de esto, y no debería. El catálogo te importa como
|
|
autor de un juego más que como escritor de un script: es adonde va tu archivo
|
|
`.game` cuando lo publicas, que es la lección treinta, y es otra razón por la
|
|
que tu `games.dat` necesita un `GAME_ID` que no cambie nunca. Si alguna vez
|
|
quieres la lista desde dentro de un script, `Singe/Master.singe` es el módulo
|
|
que hay debajo de todo esto y merece la pena leerlo una vez.
|
|
|
|
[#_what_the_service_knows_about_a_player]
|
|
=== Lo que el servicio sabe de un jugador
|
|
|
|
Si publicas un juego que envía puntuaciones, le estás pasando datos de otra
|
|
persona a un servidor, y eso es responsabilidad tuya aunque no hayas escrito el
|
|
servidor. Así que dilo claramente, en la pantalla de información o en el léeme
|
|
de tu propio juego:
|
|
|
|
* El servicio guarda una *dirección de correo electrónico* y una contraseña para
|
|
la cuenta. La dirección sirve para recuperar una contraseña perdida y para
|
|
nada más. Nunca se muestra a otros jugadores y no se puede cambiar después.
|
|
* El *alias* es lo que aparece en las tablas de récords. Se mantiene aparte de
|
|
la cuenta a propósito para que una dirección no sea nunca lo que ven los demás
|
|
jugadores, y se comprueba contra una lista de nombres que nadie debería
|
|
reclamar y de palabras que no pintan nada en una pantalla en un local público.
|
|
* Una *puntuación* lleva su valor, la tabla de récords en la que está, la hora
|
|
que registró el servicio y el alias. Nada más.
|
|
* El catálogo sabe *qué juegos están instalados* en la máquina, y qué versión,
|
|
porque así es como te ofrece una actualización.
|
|
|
|
Lo que no hace nada de esto es seguir a un jugador de un juego a otro, vender
|
|
nada ni meterse en la máquina a buscar algo que tú no hayas enviado. Tu juego no
|
|
debería añadir nada a la lista. Si adjuntas información extra a una
|
|
puntuación -- `scoreSubmit` acepta un tercer argumento opcional para eso --
|
|
que trate del juego.
|
|
|
|
[#_the_script]
|
|
=== El script
|
|
|
|
Una prueba de reflejos: espera a la palabra, pulsa el botón y puntúa lo que
|
|
quede de mil. Es un juego pequeño, y el resto es la parte en línea.
|
|
|
|
Crea una carpeta llamada `reaction` y pon esto en `reaction.singe`, dentro de
|
|
ella. Dale a la carpeta un `games.dat` como el de la lección trece, que nombre
|
|
`reaction/reaction.singe`, con la línea `GAME_ID` de antes en su entrada.
|
|
|
|
[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
|
|
----
|
|
|
|
Ejecútalo con `Singe -R reaction`. En una máquina que nunca ha iniciado sesión,
|
|
se juega, guarda tu mejor puntuación y dice
|
|
`Kept here only: this machine is not signed in.` Esa es la ejecución importante, y es la que tendrán la mayoría de tus jugadores.
|
|
|
|
[#_what_just_happened_23]
|
|
=== Qué acaba de pasar
|
|
|
|
[source,lua]
|
|
----
|
|
local top = nil
|
|
local standing = nil
|
|
----
|
|
|
|
Dos variables que son `nil` casi todo el tiempo. `nil` significa «aquí no hay
|
|
nada», y es lo que contiene una variable antes de que se meta nada en ella.
|
|
Estas guardan la tabla de récords cuando la hay, y `nil` cuando no: mientras la
|
|
petición está en camino, después de un fallo y antes de la primera partida. El
|
|
código de dibujo pregunta `if
|
|
top then` y no dibuja nada cuando no hay nada, que es la forma de casi cualquier
|
|
trozo de código de red que escribas.
|
|
|
|
[source,lua]
|
|
----
|
|
saying = "Asking the service..."
|
|
scoreBoard(function(ok, result)
|
|
----
|
|
|
|
El orden importa. La línea de texto se pone *antes* de la llamada, porque la
|
|
llamada vuelve enseguida y el callback puede tardar segundos en ejecutarse. Si
|
|
pones el texto solo dentro del callback, la pantalla muestra el mensaje de la
|
|
partida anterior hasta que llega la respuesta, que es exactamente el mensaje
|
|
equivocado en exactamente el momento equivocado.
|
|
|
|
[source,lua]
|
|
----
|
|
if not ok then
|
|
saying = "No board: " .. tostring(result)
|
|
return
|
|
end
|
|
----
|
|
|
|
El fallo va primero, y es corto. `tostring` convierte lo que haya vuelto en
|
|
texto para que unirlo con `..` no pueda fallar; `result` es una cadena cuando
|
|
`ok` es false, pero una línea de código de dibujo que se fía del tipo de algo
|
|
que le ha llegado de un servidor es una línea esperando a romperse. El jugador
|
|
ve una frase y el juego sigue.
|
|
|
|
[source,lua]
|
|
----
|
|
if scoreSubmit(score) then
|
|
fetchBoard()
|
|
else
|
|
saying = "Kept here only: " .. whyNotPosted()
|
|
end
|
|
----
|
|
|
|
Que `scoreSubmit` responda `false` no es un error. Es un hecho sobre esta
|
|
máquina, y merece la pena distinguir los dos hechos que puede significar, que es
|
|
lo que hace `whyNotPosted`: que falte `GAME_ID` es error *tuyo*, y una máquina
|
|
sin sesión iniciada no es error de nadie. La mejor puntuación local ya se
|
|
guardó, dos líneas más arriba, antes de intentar nada de esto. Primero guarda,
|
|
luego publica, siempre.
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(2, 6, "Waiting to send: " .. scoreWaiting())
|
|
----
|
|
|
|
La cola, en pantalla. En una máquina con conexión esto marca `0` para siempre y
|
|
nadie se fija. En una máquina sin ella sube, y el jugador sabe que el juego le
|
|
está guardando las puntuaciones en lugar de comérselas.
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
scoreUpdate()
|
|
----
|
|
|
|
La primera línea del fotograma, antes que nada. Ponla en algún sitio donde un
|
|
`if` no pueda saltársela, porque un `scoreUpdate` que solo se ejecuta en la
|
|
pantalla de fin de partida es una cola que solo se vacía en la pantalla de fin
|
|
de partida.
|
|
|
|
[#_try_it_28]
|
|
=== Pruébalo
|
|
|
|
. *Desenchúfalo.* Desconecta la máquina de la red, juega unas cuantas partidas
|
|
y mira el número `Waiting to send`. Vuelve a conectarla y mira cómo baja sin
|
|
que hagas nada.
|
|
. *Borra el identificador.* Quita `GAME_ID` de tu `games.dat` y vuelve a
|
|
ejecutarlo. El mensaje cambia. Vuelve a ponerlo.
|
|
. *Muestra más de la tabla de récords.* El bucle se para en cinco filas. El
|
|
overlay mide dieciocho filas de caracteres de alto por defecto, así que
|
|
calcula cuántas te caben de verdad, y mueve la línea de `standing` si hace
|
|
falta.
|
|
. *Ponle nombre a la tabla.* `scoreSubmit(score, "reaction")` y
|
|
`scoreBoard(onDone, "reaction")` usan una tabla de récords con ese nombre en
|
|
lugar de la que lleva el nombre de tu script. Publica en dos tablas desde el mismo juego -- por ejemplo,
|
|
una para la puntuación y otra para la reacción más rápida -- y deja que el
|
|
botón 2 cambie cuál se muestra.
|
|
. *Haz visible la espera.* Entre la llamada a `scoreBoard` y la ejecución del
|
|
callback, dibuja un punto que se mueva. Luego averigua cuánto dura eso de
|
|
verdad con tu conexión, y si merecía la pena dibujarlo.
|
|
|
|
[#_break_it_on_purpose_27]
|
|
=== Rómpelo a propósito
|
|
|
|
Quita la primera línea de `onOverlayUpdate`, para que desaparezca la llamada a
|
|
`scoreUpdate()`, y juega una partida.
|
|
|
|
No pasa nada. Ni error, ni mensaje, ni tabla de récords, y el número
|
|
`Waiting to send` sube y se queda arriba para siempre. El juego funciona
|
|
perfectamente.
|
|
|
|
Este es el peor tipo de fallo y merece la pena encontrárselo una vez a
|
|
propósito: un paso que falta y que produce silencio en lugar de una queja. No
|
|
hay número de línea que mirar, porque no ha ido nada mal -- pediste trabajo y
|
|
luego nunca le diste ocasión de hacerse. Cuando algo de Singe que recibe un
|
|
callback no te devuelve nunca la llamada, la primera pregunta es siempre si lo
|
|
que lo mueve se está ejecutando en cada fotograma.
|
|
|
|
El otro que te encontrarás hace más ruido. Vuelve a poner `scoreUpdate()`, borra
|
|
la línea `dofile` del principio y ejecútalo:
|
|
|
|
----
|
|
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 línea 89 es el `scoreUpdate()` que volviste a poner, una línea más arriba
|
|
ahora que el `dofile` ya no está. Lee la primera línea como siempre: la función a la que estaba llamando el motor, el
|
|
archivo, la línea y la queja. `nil` otra vez -- el nombre `scoreUpdate` no tiene
|
|
nada dentro, porque el archivo que lo define nunca se cargó. Las seis llamadas
|
|
viven en `Singe/Framework.singe`.
|
|
|
|
[#_what_you_learned_28]
|
|
=== Lo que has aprendido
|
|
|
|
* El servicio guarda una cuenta, un catálogo y tablas de récords. No es
|
|
multijugador, no es una nube de partidas guardadas y nunca es obligatorio.
|
|
* La cuenta pertenece a la máquina y se inicia sesión con ella desde las
|
|
herramientas de servicio del menú. Un juego nunca pide una contraseña.
|
|
* Una tabla de récords se guarda bajo el `GAME_ID` de `games.dat`, un UUID que
|
|
no debe cambiar nunca. `singeGetGameId()` lo lee.
|
|
* `scoreSubmit` pone una puntuación en la cola en disco y vuelve enseguida; la
|
|
cola sobrevive a que se apague la máquina.
|
|
* `scoreUpdate()` tiene que ejecutarse en cada fotograma o nada de lo que hace
|
|
el servicio termina nunca.
|
|
* `scoreBoard` le da su respuesta a una función que escribes tú, que se ejecuta
|
|
más tarde. Dibuja algo mientras tanto.
|
|
* Cualquier llamada de red puede fallar, y fallar no es un error. Di una línea
|
|
corta y sigue jugando.
|
|
* Una puntuación publicada es una afirmación, y el servicio la trata como tal.
|
|
* Conserva el récord local. Es el que siempre funciona.
|
|
|
|
[#_next_time_28]
|
|
=== La próxima vez
|
|
|
|
Un récord es un número, y la partida guardada de la lección doce es el sitio
|
|
adecuado para él. La lección veintinueve es para todo lo demás que un juego
|
|
quiere guardar: cada ronda que juega cualquiera, las fases que incluyes en el
|
|
juego y las preguntas que quieres hacerles a las dos cosas. Coge esta prueba de
|
|
reflejos y le da una base de datos.
|