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

542 lines
24 KiB
Text

[#_lesson_28_online]
== Lektion 28: Online
image::learn/28-online.png[Die fertige Lektion, 480]
In Lektion zwölf hast du einen Highscore gespeichert. Am nächsten Morgen ist er immer noch da, und
dafür ist ein Highscore größtenteils da, aber es ist ein Highscore auf einem einzigen Rechner. Diese
Lektion bringt einen Punktestand auf eine Bestenliste, die andere Leute sehen können.
Es ist auch die Lektion über das Scheitern. Alles andere, was du geschrieben hast, scheitert aus
Gründen, die du finden kannst: Eine Datei fehlt, eine Zahl ist falsch, eine Funktion ist falsch
geschrieben. Das Netzwerk scheitert aus Gründen, die niemand im Raum sehen kann. In einem Gebäude,
in dem du nie warst, wurde ein Kabel herausgezogen, und dein Spiel muss weiterlaufen. Das meiste von
dem, was folgt, handelt davon.
[#_what_the_service_is]
=== Was der Dienst ist
Singe kann mit einem *Master-Dienst* sprechen, einem Server, der drei Dinge verwaltet:
* ein *Konto*, das zum Rechner gehört und nicht zu einem einzelnen Spiel;
* einen *Katalog* von Spielen zum Herunterladen und Aktualisieren;
* *Highscore-Bestenlisten*, eine oder mehrere pro Spiel.
Der Standarddienst ist `master.singeengine.com`, und ein Betreiber, der seinen eigenen betreibt,
richtet den Rechner woandershin aus. Der Abschnitt über den Online-Dienst im Handbuch beschreibt
beide Seiten.
Jetzt der Teil, den die Leute überspringen. Der Dienst ist *kein* Mehrspielerdienst: Es gibt keine
Möglichkeit, dass zwei Spieler im selben Spiel sind. Er ist keine Cloud für Spielstände; der
Spielstand aus Lektion zwölf bleibt auf dem Rechner. Er ist kein Kopierschutz, und nichts darin
hindert irgendwen daran, dein Spiel zu spielen. Er ist keine Pflicht: Ein Spiel, das sich nie
anmeldet, läuft genau so, wie es immer gelaufen ist, und das ist der einzige vernünftige Standard für
etwas, das eine funktionierende Verbindung braucht.
Die Bestenliste ist also ein Extra. Behalte den lokalen Highscore aus Lektion zwölf, zeig ihn
weiter an, und lass die Online-Bestenliste das sein, was manchmal da ist.
[#_signing_in_happens_once_and_not_in_your_game]
=== Anmelden passiert einmal, und nicht in deinem Spiel
Das Konto gehört zum Rechner. Es wird im mitgelieferten Menü angelegt und angemeldet, hinter der
Taste, die mit `INPUT_SERVICE` belegt ist, im Tool namens *Online Account*. Das Token, das es dabei
bekommt, wird in `master.dat` im obersten Datenordner geschrieben, eine Ebene über dem
eigenen Datenordner jedes Spiels, also reicht jedes Spiel auf dem Rechner seine Punkte unter
derselben Anmeldung ein.
Dein Spiel fragt nie nach einer E-Mail-Adresse und nie nach einem Passwort. Es gibt keine Funktion
dafür, und das ist Absicht: Ein Spieler, der ein Passwort in ein Spiel tippt, das er heruntergeladen
hat, kann nicht wissen, wohin es geht.
Als was ein Spieler auf einer Bestenliste erscheint, ist ein *Handle*: drei bis zwanzig Zeichen aus
Buchstaben, Ziffern, Bindestrichen und Unterstrichen, beginnend und endend mit einem Buchstaben oder
einer Ziffer. Keine Leerzeichen, keine Punkte und kein `@`, sodass ein Handle nicht einmal wie eine
E-Mail-Adresse aussehen kann. Es wird getrennt vom Konto geführt, es ist das Einzige an einem
Spieler, das andere Spieler sehen, und es wird einmal gewählt -- ein Betreiber kann eins ändern, ein
Spieler nicht, denn eine Bestenliste, deren Namen sich ständig ändern, ist eigentlich niemandes
Geschichte. Solange kein Handle gewählt ist, werden Punktestände abgelehnt, denn ein Punktestand,
auf dem kein Name steht, steht in keinem sinnvollen Sinn auf einer Bestenliste.
Das Tool Online Account zeigt den Namen, unter dem dieser Rechner einreicht, und dein Spiel liest
ihn mit `scorePlayerName()`, das `nil` antwortet, wenn es keinen gibt. Gut zu wissen, bevor es dich
verwirrt: Ein Punktestand, hinter dem kein Handle steht, wird vom *Dienst* abgelehnt, nicht von
`scoreSubmit`, also erfährt dein Spiel nichts davon. Wenn `scorePlayerName()` `nil` ist, sag das auf
dem Bildschirm, solange noch jemand da ist, der es lesen kann.
[#_your_game_needs_an_id]
=== Dein Spiel braucht eine ID
Eine Bestenliste muss unter irgendetwas geführt werden. Dieses Etwas ist `GAME_ID` im Eintrag
deines Spiels in `games.dat`, der Datei, die du in Lektion dreizehn angelegt hast:
[source,lua]
----
GAME_ID = "6f1e7b62-0a4e-4d9c-9b2f-1c7a5e3d8a10"
----
Es ist eine UUID, in genau dieser Form: acht hexadezimale Ziffern, drei Gruppen zu vier, dann
zwölf, getrennt durch Bindestriche. Erzeug dir eine, wie du willst, und füg sie ein; es ist egal,
welche du bekommst, nur darf sie sich nie wieder ändern. Der Eintrag zu `singeGetGameId` in der Referenz
erklärt, warum nichts anderes akzeptiert wird.
`singeGetGameId()` gibt sie zurück, oder `nil`, wenn der Eintrag keine hat. Ein Spiel ohne ID kann
gespielt, aber in keine Rangliste aufgenommen werden, und vorher zu fragen ist der Weg, keine
Bestenliste anzubieten, die nie funktionieren kann:
[source,lua]
----
if singeGetGameId() then
-- showLeaderboardButton ist deine eigene Funktion, keine von Singe.
showLeaderboardButton()
end
----
[#_the_six_calls]
=== Die sechs Aufrufe
Alles, was ein Spiel mit dem Dienst macht, sind sechs Funktionen, und sie kommen aus
`Singe/Framework.singe`, also ist das `dofile` oben in deinem Skript hier keine Kür:
[source,lua]
----
dofile("Singe/Framework.singe")
----
`scoreBegin()` sagt, dass eine Partie beginnt. `scoreSubmit(value)` stellt einen Punktestand in die
Warteschlange. `scoreBoard(onDone)` holt die Bestenliste. `scorePlayerName()` ist das Handle.
`scoreWaiting()` ist die Zahl der Punktestände, die noch nicht hinausgegangen sind. Und
`scoreUpdate()` muss einmal pro Frame laufen, aus `onOverlayUpdate`, sonst wird keiner der anderen
je fertig.
Der letzte ist der ganze Trick dieser Lektion, darum wird er zweimal gesagt. Nichts hier spricht mit
dem Netzwerk, wenn du es aufrufst. `scoreSubmit` schreibt in eine Warteschlange und kehrt sofort
zurück. `scoreBoard` merkt sich, was du willst, und kehrt sofort zurück. Die eigentliche Arbeit
passiert Stück für Stück in `scoreUpdate`, zwischen deinen Frames, und darum ruckelt dein Spiel nie,
während die Leitung langsam ist. Lass `scoreUpdate` weg, und nichts geht kaputt, nichts meldet einen
Fehler, und nie passiert irgendetwas.
[source,lua]
----
function onOverlayUpdate()
scoreUpdate()
overlayClear()
return OVERLAY_UPDATED
end
----
[#_posting_a_score]
=== Einen Punktestand einreichen
Am Ende eines Spiels:
[source,lua]
----
if scoreSubmit(score) then
fetchBoard()
else
saying = "Kept here only: " .. whyNotPosted()
end
----
`scoreSubmit` antwortet `false`, wenn es kein Konto gibt oder das Spiel keine `GAME_ID` hat, und
`true`, wenn der Punktestand in die Warteschlange gegangen ist. Beachte, was das `else` *nicht* tut:
Es wirft den Punktestand nicht weg, es entschuldigt sich nicht lang und breit, und es hält den
Spieler nicht davon ab, ein neues Spiel zu starten. Es sagt eine kurze Zeile, denn der Spieler hat
nichts falsch gemacht und kann es von hier aus nicht beheben.
Die Warteschlange ist der Grund, warum das sicher ist. Ein Punktestand in der Warteschlange wird auf
die Festplatte geschrieben, also übersteht er das Ausschalten des Rechners, und er wird dem Dienst
übergeben, sobald es das nächste Mal eine Verbindung gibt -- eine Stunde später oder nächsten
Dienstag. Darum landet auch ein Automat in einem Raum mit schlechtem WLAN am Ende auf der
Bestenliste.
Sei ehrlich zu dir, was die Grenze dieses Versprechens angeht: Die Warteschlange hält Punktestände
für einen Rechner, der ein Konto *hat*. Hat sich nie jemand angemeldet, antwortet `scoreSubmit`
`false`, und der Punktestand wird nicht für später aufgehoben, weil es niemanden gibt, für den man
ihn aufheben könnte. `scoreWaiting()` sagt dir, wie viele noch in der Warteschlange sind, und diese
Zahl irgendwo zu zeigen ist eine Freundlichkeit: Sie ist der Unterschied zwischen „der Dienst ist
kaputt“ und „drei deiner Punktestände warten darauf, dass das Internet zurückkommt“.
`scoreBegin()` verdient einen eigenen Absatz. Ruf es auf, wenn eine Partie beginnt, und der Dienst
notiert den Moment nach seiner eigenen Uhr. Der Punktestand, der folgt, trägt mit sich, wie lange
die Partie gedauert hat, gemessen auf dem Server, und das ist die einzige Zahl in einer Einreichung,
die nicht durch den Rechner des Spielers gegangen ist. Aufgrund dieser Zahl wird nichts abgelehnt:
Sie wird dem, der den Dienst betreibt, neben dem Punktestand gezeigt, und ein Mensch entscheidet. Ein
Rechner, der offline war, als die Partie begann, zeichnet keine Zeit auf, und das ist ehrlich, nicht
kaputt.
Und wo wir schon ehrlich sind: Ein eingereichter Punktestand ist eine *Behauptung*. Das Spiel läuft
auf dem Computer des Spielers, also kann nichts, was der Client tut, mehr daraus machen, und ein
geheimer Schlüssel, der in deinem Spiel mitgeliefert wird, sähe nur nach Sicherheit aus. Der Dienst
zeichnet auf, wer was behauptet hat, begrenzt, wie schnell Punktestände ankommen können, und
markiert die wilden, damit ein Mensch sie sich ansieht. Entwirf deine Bestenliste mit diesem
Wissen.
[#_reading_the_board_back]
=== Die Bestenliste zurücklesen
`scoreBoard` ist die erste Funktion in diesem Buch, die eine Funktion als Argument nimmt.
[source,lua]
----
scoreBoard(function(ok, result)
...
end)
----
Du hast in Lektion fünf Funktionen geschrieben und ihnen Namen gegeben. Diese hier hat keinen Namen:
Sie wird dort geschrieben, wo sie gebraucht wird, direkt an `scoreBoard` übergeben und später
aufgerufen -- vielleicht Sekunden später -- wenn die Antwort ankommt. Das ist wieder ein *Callback*,
dieselbe Idee wie `onOverlayUpdate`, nur dass du diesmal bestimmst, wer ihn aufruft.
`ok` ist `true` oder `false`. Ist es `false`, ist `result` ein kurzer String, der sagt, warum. Ist
es `true`, ist `result` eine Tabelle mit zwei Teilen:
* `result.top`, eine Liste von Zeilen, jede mit einem `name` und einem `value`;
* `result.standing`, mit dem eigenen `value` dieses Spielers, seinem `rank` und der Zahl der
`players` auf der Bestenliste -- dieser Teil kann fehlen, wenn der Spieler nichts darauf hat.
Zwischen dem Aufruf und dem Callback läuft dein Spiel weiter und zeichnet. Diese Lücke ist echt, und
der Spieler kann sie sehen, also füll sie mit etwas. Das Skript unten setzt eine Textzeile auf
`"Asking the service..."`, bevor es aufruft, und der Callback ersetzt diese Zeile entweder durch die
Bestenliste oder durch den Grund, warum es keine gibt. Ein Bildschirm, der vier Sekunden lang nichts
sagt, sieht kaputt aus; ein Bildschirm, der sagt, was er tut, ist nur langsam.
[#_the_catalogue]
=== Der Katalog
Das Dritte, was der Dienst tut, ist, Spiele aufzulisten. Das Tool *Get Games* im Menü zeigt alles,
was der Dienst anbietet, markiert, was auf diesem Rechner schon installiert ist, und lädt es
herunter, aktualisiert oder entfernt es. Ein Download wird mit der Prüfsumme verglichen, die der
Katalog veröffentlicht hat, bevor er irgendetwas ersetzt, also schlägt eine Übertragung, die
beschädigt ankommt, fehl, statt ein Spiel zu installieren, das nicht läuft. Ein Spiel, das der
Dienst zurückgezogen hat, funktioniert weiter und wird als installiert, aber nicht mehr angeboten
angezeigt: Es bekommt nur keine Updates mehr, denn wer ein Spiel hat, der hat es.
Dein Spiel ruft nichts davon auf, und das sollte es auch nicht. Der Katalog ist für dich als Autor
eines Spiels wichtig, nicht als Schreiber eines Skripts: Dorthin kommt deine `.game`-Datei, wenn du
sie veröffentlichst, und das ist Lektion dreißig, und er ist ein weiterer Grund, warum deine
`games.dat` eine `GAME_ID` braucht, die sich nie ändert. Falls du die Liste doch einmal aus einem
Skript heraus willst: `Singe/Master.singe` ist das Modul, das unter alledem liegt, und es lohnt sich,
es einmal zu lesen.
[#_what_the_service_knows_about_a_player]
=== Was der Dienst über einen Spieler weiß
Wenn du ein Spiel auslieferst, das Punktestände einreicht, übergibst du die Daten von jemand anderem
an einen Server, und das ist deine Verantwortung, auch wenn du den Server nicht geschrieben hast.
Also sag es klar und deutlich, im Info-Bildschirm oder in der Liesmich-Datei deines Spiels:
* Der Dienst speichert eine *E-Mail-Adresse* und ein Passwort für das Konto. Mit der Adresse wird ein
verlorenes Passwort wiederhergestellt, und für nichts anderes. Sie wird anderen Spielern nie
gezeigt und kann nachträglich nicht geändert werden.
* Das *Handle* ist das, was auf Bestenlisten erscheint. Es wird absichtlich vom Konto getrennt
geführt, damit andere Spieler nie eine Adresse zu sehen bekommen, und es wird mit einer Liste von
Namen, die niemand beanspruchen sollte, und von Wörtern abgeglichen, die nicht auf einen
Bildschirm in einem öffentlichen Raum gehören.
* Ein *Punktestand* trägt seinen Wert, die Bestenliste, auf der er steht, die Zeit, die der Dienst
aufgezeichnet hat, und das Handle. Das ist alles.
* Der Katalog weiß, *welche Spiele* auf dem Rechner *installiert sind* und in welcher Version, denn
so bietet er dir ein Update an.
Was nichts davon tut, ist, einem Spieler von Spiel zu Spiel zu folgen, irgendetwas zu verkaufen
oder in den Rechner nach etwas zu greifen, das du nicht geschickt hast. Dein Spiel sollte der Liste
nichts hinzufügen. Wenn du einem Punktestand zusätzliche Informationen anhängst -- `scoreSubmit`
nimmt dafür ein optionales drittes Argument -- dann sollten sie nur vom Spiel handeln.
[#_the_script]
=== Das Skript
Ein Reaktionstest: Warte auf das Wort, drück den Knopf, und dein Punktestand ist, was von tausend
übrig bleibt. Es ist ein kleines Spiel, und der Rest davon ist der Online-Teil.
Leg einen Ordner namens `reaction` an und schreib das hier in `reaction.singe` darin. Gib dem
Ordner eine `games.dat` wie die aus Lektion dreizehn, die `reaction/reaction.singe` nennt, mit der
Zeile `GAME_ID` von oben in ihrem Eintrag.
[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
----
Starte es mit `Singe -R reaction`. Auf einem Rechner, der nie angemeldet war, läuft das Spiel, es behält deinen besten
Punktestand und sagt `Kept here only: this machine is not signed in.` Das ist der wichtige
Durchlauf, und es ist der, den die meisten deiner Spieler haben werden.
[#_what_just_happened_23]
=== Was gerade passiert ist
[source,lua]
----
local top = nil
local standing = nil
----
Zwei Variablen, die meistens `nil` sind. `nil` heißt „hier ist nichts“, und das steht in einer
Variablen, bevor irgendetwas hineingelegt wird. Diese beiden enthalten die Bestenliste, wenn es eine
gibt, und `nil`, wenn nicht: während die Anfrage unterwegs ist, nach einem Fehlschlag und vor dem
ersten Spiel. Der Zeichencode fragt `if
top then` und zeichnet nichts, wenn nichts da ist, und das ist die Form von fast jedem Stück
Netzwerkcode, das du schreiben wirst.
[source,lua]
----
saying = "Asking the service..."
scoreBoard(function(ok, result)
----
Die Reihenfolge ist wichtig. Die Textzeile wird *vor* dem Aufruf gesetzt, denn der Aufruf kehrt
sofort zurück, und der Callback läuft vielleicht erst nach Sekunden. Wenn du den Text nur im
Callback setzt, zeigt der Bildschirm bis zur Antwort die Meldung des letzten Spiels, und das ist
genau die falsche Meldung im genau falschen Moment.
[source,lua]
----
if not ok then
saying = "No board: " .. tostring(result)
return
end
----
Der Fehlschlag kommt zuerst, und er ist kurz. `tostring` macht aus allem, was zurückkam, Text, damit
das Zusammenfügen mit `..` nicht fehlschlagen kann; `result` ist ein String, wenn `ok` falsch ist,
aber eine Zeile Zeichencode, die dem Typ von etwas vertraut, das sie von einem Server bekommen hat,
ist eine Zeile, die auf ihren Absturz wartet. Der Spieler sieht einen Satz, und das Spiel läuft
weiter.
[source,lua]
----
if scoreSubmit(score) then
fetchBoard()
else
saying = "Kept here only: " .. whyNotPosted()
end
----
Dass `scoreSubmit` `false` antwortet, ist kein Fehler. Es ist eine Tatsache über diesen Rechner, und
die beiden Tatsachen, die es bedeuten kann, sollte man auseinanderhalten, und genau das tut
`whyNotPosted`: Eine fehlende `GAME_ID` ist *dein* Fehler, und ein nicht angemeldeter Rechner ist
niemandes Fehler. Der lokale beste Punktestand wurde schon gespeichert, zwei Zeilen darüber, bevor
irgendetwas davon versucht wurde. Erst speichern, dann einreichen, immer.
[source,lua]
----
overlayPrint(2, 6, "Waiting to send: " .. scoreWaiting())
----
Die Warteschlange, auf dem Bildschirm. Auf einem Rechner mit Verbindung steht hier für immer `0`,
und niemand bemerkt es. Auf einem Rechner ohne Verbindung steigt die Zahl, und der Spieler weiß,
dass das Spiel seine Punktestände aufbewahrt, statt sie zu fressen.
[source,lua]
----
function onOverlayUpdate()
scoreUpdate()
----
Erste Zeile des Frames, vor allem anderen. Setz es dorthin, wo kein `if` es überspringen kann, denn
ein `scoreUpdate`, das nur auf dem Game-Over-Bildschirm läuft, ist eine Warteschlange, die sich nur
auf dem Game-Over-Bildschirm leert.
[#_try_it_28]
=== Probier es aus
. *Zieh den Stecker.* Trenn den Rechner vom Netzwerk, spiel ein paar Spiele und beobachte die Zahl
bei `Waiting to send`. Steck ihn wieder ein und sieh zu, wie sie sinkt, ohne dass du etwas tust.
. *Lösch die ID.* Nimm `GAME_ID` aus deiner `games.dat` heraus und starte es noch einmal. Die
Meldung ändert sich. Setz sie wieder ein.
. *Zeig mehr von der Bestenliste.* Die Schleife hört nach fünf Zeilen auf. Das Overlay ist
standardmäßig achtzehn Zeichenzeilen hoch, also rechne aus, wie viele du wirklich unterbringst,
und verschieb die Zeile mit `standing`, wenn nötig.
. *Gib der Bestenliste einen Namen.* `scoreSubmit(score, "reaction")` und
`scoreBoard(onDone, "reaction")` benutzen eine Bestenliste mit diesem Namen statt der, die nach
deinem Skript benannt ist. Reich aus demselben Spiel auf zwei Bestenlisten ein -- sagen wir, eine
für den Punktestand und eine für die schnellste einzelne Reaktion -- und lass Knopf 2 umschalten,
welche gezeigt wird.
. *Mach das Warten sichtbar.* Zeichne zwischen dem Aufruf von `scoreBoard` und dem Moment, in dem
der Callback läuft, einen Punkt, der sich bewegt. Dann finde heraus, wie lange das bei deiner
Verbindung wirklich dauert und ob es sich gelohnt hat, ihn zu zeichnen.
[#_break_it_on_purpose_27]
=== Mach es absichtlich kaputt
Nimm die erste Zeile aus `onOverlayUpdate` heraus, sodass der Aufruf `scoreUpdate()` fehlt, und
spiel ein Spiel.
Nichts passiert. Kein Fehler, keine Meldung, keine Bestenliste, und die Zahl bei `Waiting to send`
steigt und bleibt für immer oben. Das Spiel läuft einwandfrei.
Das ist die schlimmste Art von Bug, und es lohnt sich, ihr einmal absichtlich zu begegnen: ein
fehlender Schritt, der Stille erzeugt statt einer Beschwerde. Es gibt keine Zeilennummer, die du dir
ansehen kannst, denn nichts ist schiefgegangen -- du hast um Arbeit gebeten und ihr dann nie eine
Gelegenheit gegeben, zu passieren. Wenn etwas in Singe, das einen Callback nimmt, dich nie
zurückruft, ist die erste Frage immer, ob das, was es antreibt, in jedem Frame läuft.
Der andere Fehler, dem du begegnen wirst, ist lauter. Setz `scoreUpdate()` wieder ein, lösch oben die
Zeile mit `dofile` und starte es:
----
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'
----
Zeile 89 ist das `scoreUpdate()`, das du wieder eingesetzt hast, eine Zeile weiter oben, jetzt wo
das `dofile` fehlt. Lies wie immer die erste Zeile: die Funktion, die die Engine aufgerufen hat, die Datei, die Zeile und die Beschwerde.
Wieder `nil` -- im Namen `scoreUpdate` steckt nichts, weil die Datei, die ihn definiert, nie geladen
wurde. Alle sechs dieser Aufrufe wohnen in `Singe/Framework.singe`.
[#_what_you_learned_28]
=== Was du gelernt hast
* Der Dienst verwaltet ein Konto, einen Katalog und Highscore-Bestenlisten. Er ist kein
Mehrspielerdienst, keine Cloud für Spielstände und nie Pflicht.
* Das Konto gehört zum Rechner und wird in den Service-Tools des Menüs angemeldet. Ein Spiel fragt
nie nach einem Passwort.
* Eine Bestenliste wird unter `GAME_ID` in `games.dat` geführt, einer UUID, die sich nie ändern
darf. `singeGetGameId()` liest sie.
* `scoreSubmit` stellt einen Punktestand in eine Warteschlange auf der Festplatte und kehrt sofort
zurück; die Warteschlange übersteht das Ausschalten des Rechners.
* `scoreUpdate()` muss in jedem Frame laufen, sonst wird nichts von dem, was der Dienst tut, je
fertig.
* `scoreBoard` übergibt seine Antwort an eine Funktion, die du schreibst und die später läuft.
Zeichne in der Zwischenzeit etwas.
* Jeder Netzwerkaufruf kann fehlschlagen, und ein Fehlschlag ist kein Fehler. Sag eine kurze Zeile
und spiel weiter.
* Ein eingereichter Punktestand ist eine Behauptung, und der Dienst behandelt ihn auch so.
* Behalte den lokalen Highscore. Er ist der, der immer funktioniert.
[#_next_time_28]
=== Beim nächsten Mal
Ein Highscore ist eine einzige Zahl, und der Spielstand aus Lektion zwölf ist der richtige Ort
dafür. Lektion neunundzwanzig ist für alles andere, was ein Spiel aufbewahren will: jede Runde, die
irgendwer spielt, die Level, die du mitlieferst, und die Fragen, die du beiden stellen willst. Sie
nimmt diesen Reaktionstest und gibt ihm eine Datenbank.