[#_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]
----
Pause
Paused
Score 000000
Arrow keys move, Return chooses.
----
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,
`
`, endet mit demselben Namen und einem Schrägstrich, `
`, und was dazwischen steht,
ist darin. Manche Elemente haben nichts darin und schließen sich selbst mit einem Schrägstrich am
Ende, wie das `` 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 `` 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 `` des Dokuments an. Eine Zeile erledigt den größten Teil der
Arbeit:
[source,html]
----
----
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 `