542 lines
24 KiB
Text
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.
|