[#_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 `