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