singe/docs/de/lessons/20-gui.adoc

593 lines
24 KiB
Text

[#_lesson_20_menus_and_screens]
== Lektion 20: Menüs und Bildschirme
image::learn/20-gui.png[Die fertige Lektion, 480]
Ein Menü kannst du aus dem bauen, was du schon kennst. Ein Kasten, etwas Text, ein Cursor,
der sich bewegt, wenn sich der Stick bewegt, ein `if` für jede Auswahl und Rechnerei, damit
alles in einer Reihe bleibt, wenn du eine vierte Option hinzufügst. Das funktioniert. Es sind
aber auch rund hundert Zeilen für eine Seite mit drei Buttons darauf, und jede dieser Zeilen
musst du pflegen.
Singe hat eine ganze Dokument-Engine eingebaut -- RmlUi -- und ein Menü, das dafür
geschrieben ist, ist eine Seite, wie eine Webseite, mit einem Stylesheet. Sie bricht ihren Text
selbst um. Sie ordnet sich selbst neu an, wenn du eine Option hinzufügst. Sie weiß, was ein
Button ist, was ein Schieberegler ist und auf welches Bedienelement der Spieler gerade zeigt.
Diese Lektion baut damit ein Pausenmenü, über einem Spiel, das dahinter weiterläuft.
[#_two_files]
=== Zwei Dateien
Eine GUI-Seite besteht aus zwei Dingen: einem *Dokument*, das sagt, was auf der Seite ist, und
*Lua*, das sagt, was die Seite tut. Sie liegen in getrennten Dateien, und genau darum geht es
bei der ganzen Übung -- das Layout ist kein Code mehr.
Fang mit einem Skript namens `menu.singe` an und mit einem Spiel, das so klein ist, dass es in
einen Absatz passt: ein Raumschiff, das über den Bildschirm fliegt, und eine Punktzahl, die
jedes Mal steigt, wenn es auf der anderen Seite ankommt. So etwas schreibst du seit Lektion
sieben.
[source,lua]
----
dofile("Singe/Framework.singe")
overlaySetResolution(discGetWidth(), discGetHeight())
local ship = spriteLoad(DIR .. "art/spaceship.png")
local shipX = 0
local speed = 2
local score = 0
function onOverlayUpdate()
overlayClear()
shipX = shipX + speed
if shipX > overlayGetWidth() then
shipX = -spriteGetWidth(ship)
score = score + 10
end
spriteDraw(ship, shipX, 300)
return OVERLAY_UPDATED
end
----
Das läuft. Jetzt das Dokument. Leg neben deinem Skript eine Datei namens `pause.rml` an:
[source,html]
----
<rml>
<head>
<title>Pause</title>
<link type="text/rcss" href="Singe/gui.rcss"/>
<style>
body { width: 100%; height: 100%; }
#frame { width: 380dp; margin: 80dp auto; }
.row { display: block; margin: 8dp 0; }
label { display: inline-block; width: 100dp; }
button { nav: auto; }
input.range { nav: auto; width: 200dp; }
#status { min-height: 28dp; }
</style>
</head>
<body id="pause">
<div id="frame" class="panel">
<h1>Paused</h1>
<p class="row">Score <span id="score">000000</span></p>
<div class="row"><label>Speed</label><input type="range" id="speed" min="1" max="8" step="1" value="2"/></div>
<div class="row">
<button id="resume" autofocus>Resume</button>
<button id="restart">Restart</button>
<button id="quit">Quit</button>
</div>
<p id="status" class="muted">Arrow keys move, Return chooses.</p>
</div>
</body>
</rml>
----
Wenn du schon einmal den Quelltext einer Webseite gesehen hast, kommt dir das bekannt vor. Wenn
nicht, hier ist alles, was du brauchst, um es zu lesen.
Ein Dokument besteht aus *Elementen*. Ein Element beginnt mit einem Namen in spitzen Klammern,
`<h1>`, endet mit demselben Namen und einem Schrägstrich, `</h1>`, und was dazwischen steht,
ist darin. Manche Elemente haben nichts darin und schließen sich selbst mit einem Schrägstrich am
Ende, wie das `<input .../>` oben. Elemente lassen sich ineinander verschachteln, und die
Einrückung zeigt die Verschachtelung genauso, wie sie es in Lua tut.
Die Wörter im öffnenden Tag sind *Attribute*, geschrieben als `name="value"`. Zwei davon sind
für dich wichtiger als der Rest. `id` gibt einem Element einen Namen, über den dein Skript es
finden kann, und so reden Lua und das Dokument miteinander. `class` steckt das Element in eine
Gruppe, auf die das Stylesheet zielen kann; `panel`, `muted` und `list` sind Gruppen, die das
eigene Stylesheet der Engine schon kennt.
Die Auszeichnungssprache heißt RML und die Stilsprache RCSS. Sie gehören zu RmlUi, nicht zu
Singe. Das GUI-Kapitel der Referenz verweist für die vollständige Liste dessen, was du
schreiben kannst, auf die eigene Dokumentation von RmlUi, und die wirst du irgendwann haben
wollen. Alles in dieser Lektion funktioniert ohne sie.
[#_show_it]
=== Zeig sie an
Zurück ins Skript. Füg über `onOverlayUpdate` Folgendes ein:
[source,lua]
----
local gui = guiNew(overlayGetWidth(), overlayGetHeight())
local page = guiLoad(gui, DIR .. "pause.rml")
----
`guiNew` erzeugt eine *GUI*: ein Rechteck fester Größe, in das Dokumente gesetzt und gezeichnet
werden. Es gibt ein Handle zurück, so wie `fontLoad` und `spriteLoad`. Wenn du sie so groß wie
das Overlay machst, ist ein Pixel des Dokuments ein Pixel deines Overlays, und das hält die
Rechnerei in deinem Kopf einfach.
`guiLoad` liest ein Dokument in diese GUI und gibt ein zweites Handle zurück, für das Dokument
selbst. Fast jeder andere Aufruf nimmt beide: die GUI und das Dokument darin. Eine GUI kann bis
zu zweiunddreißig Dokumente aufnehmen -- eine Titelseite, eine Optionsseite und eine
Game-Over-Seite können sich eine teilen -- und es können bis zu sechzehn GUIs gleichzeitig
existieren.
Noch ist nichts auf dem Bildschirm, denn ein Dokument zu laden zeichnet es nicht. Füg in
`onOverlayUpdate` direkt vor dem `return` eine Zeile ein:
[source,lua]
----
guiDraw(gui)
----
Starte es. Das Panel ist da, über dem Raumschiff, mit seiner Überschrift, seinen drei Buttons
und seinem Schieberegler, und du hast überhaupt keinen Zeichencode geschrieben.
`guiDraw` legt die GUI *nur für diesen Frame* über das Overlay, wie jeder andere Zeichenaufruf,
also gehört es in `onOverlayUpdate` und muss im nächsten Frame wieder aufgerufen werden. Wo du es
hinsetzt, entscheidet, was oben liegt: Alles, was du davor gezeichnet hast, liegt darunter, und
alles, was du danach zeichnest, kommt darüber. Ein Fadenkreuz, das nach `guiDraw` gezeichnet
wird, wird nie vom Menü verdeckt.
[#_let_the_player_use_it]
=== Lass den Spieler sie benutzen
Klick auf einen Button. Nichts passiert, und der Button leuchtet nicht einmal auf.
Eine neue GUI ist ein Bild, kein Bedienfeld. Das ist Absicht: Ein HUD oder ein Schild soll nicht
den Feuerknopf verschlucken. So nimmt eine Seite Eingaben an:
[source,lua]
----
guiSetInput(gui, true)
----
Jetzt funktioniert die Maus, die Buttons leuchten unter dem Zeiger auf, und die Pfeiltasten
wechseln zwischen den Bedienelementen, weil das Dokument das mit `nav: auto` verlangt hat. Auf
einem Gamepad oder einem Stick kommen die vier Richtungen als Pfeiltasten an, der erste
Aktionsknopf als Eingabetaste und der zweite als Esc. Ein Automat mit einem Joystick und zwei
Knöpfen bedient diese Seite, ohne zu wissen, dass sie ein Dokument ist.
Das Bedienelement, auf das die Tasten gerade wirken, hat den *Fokus*, und ein fokussierter Button
trägt einen goldenen Rahmen. Der Fokus beginnt auf Resume, weil dieser Button `autofocus` sagt:
Jedes Mal, wenn die Seite gezeigt wird, geht der Fokus an das so markierte Bedienelement. Lass es
weg, und die Pfeiltasten haben nichts, von dem aus sie sich bewegen können, bis der Spieler etwas
anklickt, und das passiert auf einem Automaten ohne Maus nie.
Klicken bewirkt aber immer noch nichts, weil niemand zuhört.
[#_talk_to_it]
=== Sprich mit ihr
Drei Aufrufe verbinden das Dokument mit deinem Skript, und zusammen erledigen sie fast alles.
[source,lua]
----
guiSetHandler(gui, page, "resume", "click", function()
closeMenu()
end)
----
`guiSetHandler` sagt: Wenn das Element mit dieser `id` dieses Ding tut, ruf diese Funktion auf.
Die `id` ist die aus dem Dokument. Der Name des Ereignisses ist einer von RmlUi: `"click"` für
Buttons, `"change"` für alles, was der Spieler verstellt, und eine lange Liste weiterer, die du
nachschlagen kannst, wenn du sie brauchst. Pro Element und pro Ereignis wird eine Funktion
behalten, also ersetzt eine neue die alte, und `nil` statt einer Funktion entfernt sie.
Deine Funktion wird mit fünf Argumenten aufgerufen -- der GUI, dem Dokument, der id, dem
Ereignis und dem aktuellen Wert des Elements als String. Nimm so viele, wie du willst, und
ignorier den Rest, so wie du in Lua sowieso schon Argumente ignorierst.
[source,lua]
----
guiSetHandler(gui, page, "speed", "change", function(g, d, id, event, value)
speed = math.floor(tonumber(value))
end)
----
Der Schieberegler übergibt seinen Wert als String, und RmlUi formatiert Zahlen auf seine eigene
Art, also kommt `4` als `"4.000000"` an. `tonumber` macht aus dem String eine Zahl, und
`math.floor` wirft die Nachkommastellen weg. Diesen Schritt auszulassen und den String als Zahl
zu benutzen ist ein Fehler, den du genau einmal machst.
[source,lua]
----
guiSetValue(gui, page, "score", string.format("%06d", score))
----
`guiSetValue` schreibt in ein Element. Bei einem Formularelement setzt es dessen Wert; bei allem
anderen -- einem Absatz, einem `div`, dem `<span id="score">` in deinem Dokument -- ersetzt es,
was im Element steht, und die Seite ordnet sich im nächsten Frame neu an. `guiGetValue` ist
derselbe Aufruf rückwärts, und es antwortet `nil` für eine id, die nichts hat, also kannst du
nach einem Element fragen, das vielleicht nicht da ist, ohne abzustürzen.
Das ist die ganze Alltags-API: `guiSetHandler`, `guiSetValue`, `guiGetValue`. Über die eigenen
Lua-Objekte von RmlUi, erreichbar über die globale Variable `rmlui`, gibt es noch viel mehr, und
das GUI-Kapitel der Referenz zeigt, wie. Du kommst weit, bevor du das brauchst.
[#_hide_it_again]
=== Versteck sie wieder
Ein Pausenmenü ist nicht die ganze Zeit zu sehen. `guiHide` nimmt ein Dokument vom Bildschirm,
und `guiShow` holt es zurück, mit allem, was der Spieler getippt oder gewählt hat.
Die Pause selbst ist `singeSetPauseFlag(true)`. Das hält die Disc, die Videos und die Sounds
an, *ruft aber deine Callbacks weiter auf*, also kannst du weiter zeichnen. Die Engine hat ihre
eigene Pausetaste, die das Skript komplett einfriert, und die beiden würden sich in die Quere
kommen; `singeSetPauseKeyEnabled(false)` nimmt der Engine diese Taste weg, sodass `SWITCH_PAUSE`
wie jeder andere Schalter bei `onInputPressed` ankommt. Auf einer Tastatur ist diese Taste *P*.
[source,lua]
----
local function closeMenu()
paused = false
singeSetPauseFlag(false)
guiHide(gui, page)
guiSetInput(gui, false)
end
local function openMenu()
paused = true
singeSetPauseFlag(true)
guiSetValue(gui, page, "score", string.format("%06d", score))
guiShow(gui, page)
guiSetInput(gui, true)
end
----
Die Eingabe zusammen mit der Seite abzuschalten ist keine Verzierung. Eine versteckte Seite, die
noch Eingaben annimmt, frisst dem Spieler für den Rest des Spiels die Pfeiltasten.
[#_what_the_page_does_not_use_you_still_get]
=== Was die Seite nicht benutzt, bekommst du trotzdem
Das ist die Regel, durch die man eine GUI gefahrlos während des Spiels stehen lassen kann, und
es lohnt sich, sie klar auszusprechen: *Ein Ereignis, das ein Element benutzt hat, erreicht nie
deine Callbacks.* Eine Eingabetaste, die den fokussierten Button gedrückt hat, kommt nicht als
`onInputPressed` an. Ein Pfeil, der den Fokus bewegt hat, kommt nicht als `SWITCH_LEFT` an. Ein
Klick auf einen Button ist kein Mausschalter.
Alles, was die Seite nicht benutzt hat, fällt wie gewohnt direkt durch. Darum schließt `P`
dieses Menü: Kein Element im Dokument benutzt `P`, also kommt die Taste genauso bei
`onInputPressed` an wie dann, wenn das Menü weg ist. Und darum lässt ein HUD, das Eingaben
annimmt, den Feuerknopf trotzdem durch.
Eine Taste hält sich nicht an die Regel. RmlUi benutzt Tab, um den Fokus zu bewegen, und gibt
die Taste nie zurück, also gibt Singe Tab nur dann an eine GUI, wenn gerade wirklich in ein
Textfeld getippt wird. Ein Spiel, das Tab mit etwas belegt, behält die Taste.
[#_style_it]
=== Gestalte sie
Sieh dir noch einmal den `<head>` des Dokuments an. Eine Zeile erledigt den größten Teil der
Arbeit:
[source,html]
----
<link type="text/rcss" href="Singe/gui.rcss"/>
----
Das ist das Theme, das die Engine mitliefert, und darum hat dein Panel einen Rahmen, einen
Farbverlauf und eine goldene Überschrift, ohne dass du irgendetwas davon ausgesucht hast. Ohne
Stylesheet zeichnet RmlUi fast nichts, weil jedes Element zuerst inline ist und so groß wie sein
Inhalt. Dieses Theme zuerst einzubinden und zu überschreiben, was dir nicht gefällt, ist also der
schnellste Weg zu einer Seite, die nach etwas aussieht.
Das Theme gibt dir `.panel` (einen gerahmten Kasten mit runden Ecken), `.list` (einen
scrollenden Kasten, dessen `div`-Zeilen aufleuchten und den Fokus annehmen), `.muted` (blasseren
Text für Hinweise), Überschriften und jedes Formularelement mit seinen Zuständen für Hover, Fokus
und gedrückt. Das GUI-Kapitel der Referenz listet sie auf.
Alles nach dem Link gehört dir. Der `<style>`-Block in `pause.rml` überschreibt das Theme für
diese Seite, wobei spätere Regeln über frühere gewinnen und eine Regel für eine `id` eine Regel
für eine `class` schlägt. Für ein echtes Spiel schreib deine eigenen Regeln in eine
`.rcss`-Datei neben dem Dokument und binde sie als zweite ein, dann ist dein Aussehen eine
einzige Datei für jede Seite.
Die Einheit `dp` ist ein Pixel in der eigenen Größe der GUI -- deine GUI ist so groß wie das
Overlay, also sind `380dp` 380 Overlay-Pixel. `margin: 80dp auto` ist der Trick, mit dem man
einen Block bekannter Breite zentriert: `auto` verteilt, was übrig bleibt, gleichmäßig auf beide
Seiten. Es gibt kein Ausmessen und keine Rechnerei, was du nach Lektion neunzehn vielleicht ein
bisschen ärgerlich findest.
Um deine eigene Schriftart in einem Dokument zu benutzen, lade sie mit `guiLoadFont` vor den
Dokumenten, die sie benutzen, und nenn sie in RCSS beim Familiennamen, der in der Schriftdatei
steht, nicht bei ihrem Dateinamen.
[#_when_not_to_use_any_of_this]
=== Wann du nichts davon benutzen solltest
Eine GUI ist eine Seite. Sie ist die richtige Antwort für ein Menü, einen Optionsbildschirm,
eine Highscore-Tabelle, einen Game-Over-Bildschirm oder ein HUD mit einem echten Layout.
Für eine Punktzahl in der Ecke ist sie die falsche Antwort. Das sind zwei Zeilen `fontPrint` aus
Lektion neunzehn, und sie in ein Dokument zu packen bringt dir nichts und kostet dich eine
Textur. Dasselbe gilt für ein Fadenkreuz, eine Lebensleiste, die du mit `overlayBox` gezeichnet
hast, und eine Zeile Debug-Text.
Drei Tatsachen entscheiden das für dich.
Eine GUI braucht eine GPU. Auf einem Rechner ohne -- einer alten Kiste, einem kleinen Pi, einem
Rechner ganz ohne Treiber für Vulkan, Direct3D 12 oder Metal -- gibt es keine GUIs, und `guiNew`
antwortet nicht höflich mit `false`, sondern beendet dein Spiel. Der 3D-Szene aus Lektion
zweiundzwanzig geht es genauso, also fragst du das nicht zum letzten Mal. Frag zuerst
`singeHasGpu()`, wenn dein Spiel an beiden Orten laufen muss, und halte eine schlichte
Overlay-Version bereit, auf die du zurückfallen kannst. Das eigene Menü der Engine macht genau
das.
Eine GUI kostet eine Textur und einen Layout-Durchgang *in jedem Frame, ob du sie zeichnest oder
nicht*. Eine GUI, mit der du fertig bist, solltest du mit `guiDelete` löschen, nicht nur
verstecken. Verstecken ist für eine Seite, die du in einer Minute wieder zeigst.
Und ein Neuladen reißt eine GUI ab. `F5`, `-R` und alles, was das Skript neu startet, nehmen
jede GUI mit, und das Skript erzeugt sie neu, wenn es läuft. Das ist in Ordnung, solange du sie
im Hauptteil deines Skripts baust und nicht in irgendeiner Ecke, die nur einmal läuft.
[#_the_whole_script_2]
=== Das ganze Skript
[source,lua]
----
dofile("Singe/Framework.singe")
overlaySetResolution(discGetWidth(), discGetHeight())
singeSetPauseKeyEnabled(false)
local SHIP_Y = 300
local ship = spriteLoad(DIR .. "art/spaceship.png")
local hudFont = fontLoad("Singe/FreeSansBold.ttf", 20)
local shipX = 0
local speed = 2
local score = 0
local paused = false
local gui = nil
local page = nil
local function say(text)
if gui ~= nil then
guiSetValue(gui, page, "status", text)
end
end
local function closeMenu()
paused = false
singeSetPauseFlag(false)
if gui ~= nil then
guiHide(gui, page)
guiSetInput(gui, false)
end
end
local function openMenu()
paused = true
singeSetPauseFlag(true)
if gui ~= nil then
guiSetValue(gui, page, "score", string.format("%06d", score))
guiShow(gui, page)
guiSetInput(gui, true)
end
end
if singeHasGpu() then
gui = guiNew(overlayGetWidth(), overlayGetHeight())
page = guiLoad(gui, DIR .. "pause.rml")
guiHide(gui, page)
guiSetInput(gui, false)
guiSetHandler(gui, page, "resume", "click", function()
closeMenu()
end)
guiSetHandler(gui, page, "restart", "click", function()
score = 0
shipX = 0
closeMenu()
end)
guiSetHandler(gui, page, "quit", "click", function()
singeQuit()
end)
guiSetHandler(gui, page, "speed", "change", function(g, d, id, event, value)
speed = math.floor(tonumber(value))
say("Speed " .. speed)
end)
end
function onInputPressed(what)
if what == SWITCH_PAUSE then
if paused then
closeMenu()
else
openMenu()
end
end
end
function onOverlayUpdate()
overlayClear()
if not paused then
shipX = shipX + speed
if shipX > overlayGetWidth() then
shipX = -spriteGetWidth(ship)
score = score + 10
end
end
spriteDraw(ship, shipX, SHIP_Y)
fontSelect(hudFont)
colorForeground(255, 255, 255)
fontPrint(16, 16, string.format("SCORE %06d", score))
fontPrint(16, 40, "P pauses")
if paused then
if gui ~= nil then
guiDraw(gui)
else
colorForeground(255, 211, 90)
fontPrint(16, 70, "PAUSED -- no GPU, so no menu")
end
end
return OVERLAY_UPDATED
end
function onShutdown()
if gui ~= nil then
guiDelete(gui)
end
spriteUnload(ship)
fontUnload(hudFont)
end
----
[#_what_just_happened_16]
=== Was gerade passiert ist
[source,lua]
----
if singeHasGpu() then
gui = guiNew(overlayGetWidth(), overlayGetHeight())
page = guiLoad(gui, DIR .. "pause.rml")
----
Die ganze GUI wird in diesem `if` gebaut, und auf einem Rechner ohne GPU bleibt `gui` `nil`.
Jeder Aufruf, der die GUI anfasst, ist mit `if gui ~= nil` abgesichert, und die Pause
funktioniert auch ohne Menü. Das sind vier zusätzliche Zeilen, und sie machen den Unterschied
zwischen „hat auf diesem Rechner kein Menü“ und „startet auf diesem Rechner nicht“.
[source,lua]
----
guiHide(gui, page)
guiSetInput(gui, false)
----
`guiLoad` zeigt das Dokument, das es geladen hat, also muss eine Seite, die versteckt anfangen
soll, sofort versteckt werden. Bei einer neuen GUI ist die Eingabe schon aus, aber wenn du sie
hier abschaltest, sind `openMenu` und `closeMenu` genaue Spiegelbilder voneinander, und bei
gespiegelten Paaren macht man viel schwerer etwas falsch.
[source,lua]
----
guiSetHandler(gui, page, "restart", "click", function()
score = 0
shipX = 0
closeMenu()
end)
----
Der Handler ist eine Funktion ohne Namen, geschrieben an der Stelle, an der sie benutzt wird.
Lua lässt dich `function(...) ... end` überall schreiben, wo ein Wert erwartet wird, und weil
`guiSetHandler` eine Funktion will, kannst du ihm gleich an Ort und Stelle eine geben, statt sie
erst zu benennen und dann den Namen zu übergeben. Sie erreicht `score`, `shipX` und
`closeMenu`, weil diese weiter oben in derselben Datei deklariert sind, und genau darum werden
die Handler unterhalb der Dinge eingerichtet, die sie anfassen.
[source,lua]
----
if not paused then
shipX = shipX + speed
----
Das Pause-Flag hält die Disc und die Sounds an, nicht deine Rechnerei. `onOverlayUpdate` wird
weiter aufgerufen -- genau darum geht es, denn sonst könnte das Menü nicht gezeichnet werden --
also musst du alles von dir, was während der Pause anhalten soll, selbst anhalten.
[#_try_it_20]
=== Probier es aus
. *Füg einen vierten Button hinzu.* Kopier die Zeile `<button id="quit">`, gib ihr eine neue id
und neuen Text, und füg einen Handler dafür hinzu. Achte darauf, dass nichts neu ausgemessen
oder verschoben werden musste.
. *Gestalte sie um.* Ändere im `<style>`-Block `#frame` in `width: 260dp; margin: 20dp;` und
sieh zu, wie das Panel wandert und der Text sich von selbst neu umbricht.
. *Nimm das `guiSetInput(gui, true)` in `openMenu` heraus.* Die Seite erscheint trotzdem. Finde
allein am Bildschirm heraus, was du verloren hast.
. *Lass die Seite die ganze Zeit sichtbar.* Lösch die Aufrufe von `guiHide`, ändere beide Aufrufe
`guiSetInput(gui, false)` in `true` und zeichne die GUI in jedem Frame statt nur während der
Pause. Dann drück die Pfeiltasten und die Eingabetaste, während das Raumschiff fliegt. Die Seite
nimmt sie für sich: Der Fokus wandert, der Schieberegler ändert die Geschwindigkeit, und die
Eingabetaste drückt den Button, der gerade den Fokus hat. Ein Spiel, das mit diesen Tasten
steuert, würde sie nie zu hören bekommen. Darum ist ein HUD `guiSetInput(gui, false)`.
. *Setz die Punktzahl auf die Seite statt ins HUD.* Ruf `guiSetValue` für `score` in jedem Frame
auf statt nur in `openMenu`, und lösch die `fontPrint`-Zeile. Dann entscheide, welche der
beiden Varianten dir wirklich lieber ist, und warum.
[#_break_it_on_purpose_19]
=== Mach es absichtlich kaputt
Ändere eine id in `pause.rml`, von `id="resume"` in `id="continue"`, und lass das Skript in
Ruhe. Singe startet und stirbt:
----
52:guiSetHandler: No element "resume" in document 0 of GUI 0.
----
Lies sie, wie du jeden Fehler der Engine liest. Die Zeile, der Aufruf, der sich beschwert hat,
dann die Beschwerde -- und diese Beschwerde ist genau. Sie sagt nicht, dass das Dokument kaputt
ist; das Dokument wurde einwandfrei geladen. Sie sagt, dass du nach einem Element mit einem
Namen gefragt hast, den nichts darin hat.
`guiSetHandler` und `guiSetValue` sterben beide an einer fehlenden id, weil ein Handler, den du
anhängen wolltest und nicht angehängt hast, ein Fehler ist, von dem du sofort hören willst.
`guiGetValue` ist die Ausnahme: Es antwortet stattdessen mit `nil`, also kannst du fragen, ob
etwas da ist.
Der andere Fehler, den du kennenlernen solltest, ist der, den du bekommst, wenn das Dokument
selbst nicht lädt. Benenn `pause.rml` um und starte es. Diesmal nennt Singe die Datei und zitiert
den eigenen Grund von RmlUi, und das Gemecker von RmlUi über dein Markup und deine Stile landet
in der Konsole, ob das Laden klappt oder nicht. Lies diese Konsole, wenn eine Seite zum ersten
Mal leer bleibt; meistens hat sie schon gesagt, warum.
[#_what_you_learned_20]
=== Was du gelernt hast
* Eine GUI ist ein Rechteck fester Größe, in das Dokumente gesetzt und gezeichnet werden;
`guiNew` erzeugt eine, und `guiDelete` gibt sie frei.
* `guiLoad` legt ein RML-Dokument in eine GUI und zeigt es an; `guiHide`, `guiShow` und
`guiClose` verwalten es danach.
* `guiDraw` legt die GUI für einen Frame über das Overlay, also kommt es wie jeder andere
Zeichenaufruf in `onOverlayUpdate`.
* Eine neue GUI zeigt nur an; erst `guiSetInput` macht sie bedienbar.
* `guiSetHandler` führt deine Funktion aus, wenn ein Element angeklickt oder verändert wird,
`guiSetValue` schreibt in ein Element, und `guiGetValue` liest eines.
* Ein Ereignis, das ein Element benutzt hat, erreicht nie deine eigenen Callbacks; alles andere
fällt wie gewohnt durch.
* `Singe/gui.rcss` ist das mitgelieferte Theme; binde es zuerst ein und überschreib es in einem
`<style>`-Block oder einem eigenen Stylesheet.
* Eine GUI braucht eine GPU, also frag `singeHasGpu()`, wenn dein Spiel auch ohne laufen muss.
* Jede GUI rendert in jedem Frame, ob du sie zeichnest oder nicht, also lösch die, mit denen du
fertig bist.
* Eine Punktzahl in der Ecke sind zwei Zeilen `fontPrint`, kein Dokument.
[#_next_time_20]
=== Beim nächsten Mal
Es gibt noch eine weitere Art von Text auf dem Bildschirm, und sie hat ein eigenes System, aus
einem Grund, der nichts damit zu tun hat, wie sie aussieht. In Lektion einundzwanzig geht es um
Untertitel und um die Spieler, die dein Spiel ohne sie nicht spielen können.