628 lines
23 KiB
Text
628 lines
23 KiB
Text
[#_lesson_20_menus_and_screens]
|
|
== Lección 20: Menús y pantallas
|
|
|
|
image::learn/20-gui.png[La lección terminada, 480]
|
|
|
|
Puedes construir un menú con lo que ya sabes. Una caja, algo de texto, un
|
|
cursor que se mueve cuando se mueve el stick, un `if` para cada opción, y
|
|
aritmética para que todo siga alineado cuando añadas una cuarta opción.
|
|
Funciona. También son unas cien líneas para una página con tres botones, y cada
|
|
una de esas líneas te toca mantenerla a ti.
|
|
|
|
Singe lleva dentro todo un motor de documentos -- RmlUi -- y un menú escrito
|
|
para él es una página, como una página web, con una hoja de estilo. Reparte él
|
|
solo su texto en líneas. Se vuelve a maquetar solo cuando añades una opción.
|
|
Sabe qué es un botón, qué es un deslizador y a qué control está apuntando el
|
|
jugador. Esta lección construye con él un menú de pausa, sobre un juego que
|
|
sigue funcionando por detrás.
|
|
|
|
[#_two_files]
|
|
=== Dos archivos
|
|
|
|
Una página de GUI son dos cosas: un *documento* que dice qué hay en la página,
|
|
y *Lua* que dice qué hace la página. Viven en archivos separados, y esa es toda
|
|
la gracia del ejercicio -- la maquetación deja de ser código.
|
|
|
|
Empieza con un script llamado `menu.singe` y un juego tan pequeño que cabe en
|
|
un párrafo: una nave que cruza la pantalla y una puntuación que sube cada vez
|
|
que llega al otro lado. Llevas escribiendo este tipo de cosas desde la lección
|
|
siete.
|
|
|
|
[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
|
|
----
|
|
|
|
Eso funciona. Ahora el documento. Junto a tu script, crea un archivo llamado
|
|
`pause.rml`:
|
|
|
|
[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>
|
|
----
|
|
|
|
Si alguna vez has visto el código fuente de una página web, esto te sonará. Si
|
|
no, aquí tienes todo lo que necesitas para leerlo.
|
|
|
|
Un documento está hecho de *elementos*. Un elemento empieza con un nombre entre
|
|
corchetes angulares, `<h1>`, termina con el mismo nombre y una barra, `</h1>`, y
|
|
lo que haya entre los dos está dentro de él. Algunos elementos no tienen nada
|
|
dentro y se cierran solos con una barra al final, como el `<input .../>` de
|
|
arriba. Los elementos se anidan, y la sangría muestra el anidamiento igual que
|
|
lo muestra en Lua.
|
|
|
|
Las palabras que hay dentro de la etiqueta de apertura son *atributos*, y se
|
|
escriben `name="value"`. Dos de ellos te importan más que el resto. `id` le da a
|
|
un elemento un nombre por el que tu script puede encontrarlo, que es como se
|
|
hablan Lua y el documento. `class` mete el elemento en un grupo al que puede
|
|
apuntar la hoja de estilo; `panel`, `muted` y `list` son grupos que la hoja de
|
|
estilo del propio motor ya conoce.
|
|
|
|
El lenguaje de marcado se llama RML y el lenguaje de estilos se llama RCSS. Son
|
|
de RmlUi, no de Singe. El capítulo sobre la GUI de la referencia te remite a la
|
|
documentación de RmlUi para la lista completa de lo que puedes escribir, y
|
|
tarde o temprano la querrás. Todo lo de esta lección funciona sin ella.
|
|
|
|
[#_show_it]
|
|
=== Muéstrala
|
|
|
|
De vuelta al script. Encima de `onOverlayUpdate`, añade:
|
|
|
|
[source,lua]
|
|
----
|
|
local gui = guiNew(overlayGetWidth(), overlayGetHeight())
|
|
local page = guiLoad(gui, DIR .. "pause.rml")
|
|
----
|
|
|
|
`guiNew` crea una *GUI*: un rectángulo de tamaño fijo en el que se maquetan y se
|
|
dibujan los documentos. Devuelve un handle, igual que `fontLoad` y
|
|
`spriteLoad`. Hacerla del tamaño del overlay significa que un píxel del
|
|
documento es un píxel de tu overlay, y así las cuentas que haces de cabeza
|
|
siguen siendo sencillas.
|
|
|
|
`guiLoad` lee un documento en esa GUI y devuelve un segundo handle, para el
|
|
propio documento. Casi todas las demás llamadas reciben los dos: la GUI y el
|
|
documento que hay en ella. Una GUI puede contener hasta treinta y dos
|
|
documentos -- una página de título, una de opciones y una de fin de partida
|
|
pueden compartir una -- y pueden existir hasta dieciséis GUI a la vez.
|
|
|
|
Todavía no hay nada en pantalla, porque cargar un documento no lo dibuja. Añade
|
|
una línea a `onOverlayUpdate`, justo antes del `return`:
|
|
|
|
[source,lua]
|
|
----
|
|
guiDraw(gui)
|
|
----
|
|
|
|
Ejecútalo. Ahí está el panel, encima de la nave, con su título, sus tres
|
|
botones y su deslizador, y no has escrito nada de código de dibujo.
|
|
|
|
`guiDraw` compone la GUI sobre el overlay *solo para este fotograma*, como
|
|
todas las demás llamadas de dibujo, así que va en `onOverlayUpdate` y hay que
|
|
volver a llamarla en el fotograma siguiente. Dónde la pongas decide qué queda
|
|
encima: todo lo que dibujaste antes queda debajo, y todo lo que dibujes después
|
|
va por encima. Un punto de mira dibujado después de `guiDraw` nunca queda tapado
|
|
por el menú.
|
|
|
|
[#_let_the_player_use_it]
|
|
=== Deja que el jugador la use
|
|
|
|
Haz clic en un botón. No pasa nada, y el botón ni siquiera se ilumina.
|
|
|
|
Una GUI nueva es un dibujo, no un panel de control. Es a propósito: un HUD o un
|
|
cartel no deberían tragarse el botón de disparo. Para que una página reciba
|
|
entrada:
|
|
|
|
[source,lua]
|
|
----
|
|
guiSetInput(gui, true)
|
|
----
|
|
|
|
Ahora el ratón funciona, los botones se iluminan bajo el puntero y las teclas de
|
|
flecha saltan de un control a otro, porque el documento lo pidió con
|
|
`nav: auto`. En un mando o en un stick, las cuatro direcciones llegan como las
|
|
teclas de flecha, el primer botón de acción llega como Intro y el segundo llega
|
|
como Esc. Una recreativa con un joystick y dos botones maneja esta página sin
|
|
saber que es un documento.
|
|
|
|
Del control sobre el que actúan las teclas se dice que tiene el *foco*, y un
|
|
botón con el foco lleva un borde dorado. El foco empieza en Resume porque ese
|
|
botón dice `autofocus`: cada vez que se muestra la página, el foco va al control
|
|
marcado así. Si lo quitas, las teclas de flecha no tienen desde dónde moverse
|
|
hasta que el jugador hace clic en algo, lo que en una recreativa sin ratón no
|
|
pasa nunca.
|
|
|
|
Pero hacer clic sigue sin hacer nada, porque no hay nada escuchando.
|
|
|
|
[#_talk_to_it]
|
|
=== Háblale
|
|
|
|
Tres llamadas unen el documento a tu script, y entre las tres lo hacen casi
|
|
todo.
|
|
|
|
[source,lua]
|
|
----
|
|
guiSetHandler(gui, page, "resume", "click", function()
|
|
closeMenu()
|
|
end)
|
|
----
|
|
|
|
`guiSetHandler` dice: cuando el elemento con este `id` haga esto, llama a esta
|
|
función. El `id` es el del documento. El nombre del evento es de RmlUi:
|
|
`"click"` para los botones, `"change"` para cualquier cosa que el jugador
|
|
ajuste, y una larga lista de otros que puedes consultar cuando los necesites. Se
|
|
guarda una función por elemento y por evento, así que poner otra sustituye a la
|
|
anterior, y pasar `nil` en lugar de una función la quita.
|
|
|
|
Tu función se llama con cinco argumentos -- la GUI, el documento, el id, el
|
|
evento y el valor actual del elemento como cadena. Recoge los que quieras e
|
|
ignora el resto, igual que ya ignoras argumentos en Lua.
|
|
|
|
[source,lua]
|
|
----
|
|
guiSetHandler(gui, page, "speed", "change", function(g, d, id, event, value)
|
|
speed = math.floor(tonumber(value))
|
|
end)
|
|
----
|
|
|
|
El deslizador entrega su valor como cadena, y RmlUi escribe los números a su
|
|
manera, así que `4` llega como `"4.000000"`. `tonumber` convierte la cadena en
|
|
un número y `math.floor` tira los decimales. Saltarte ese paso y usar la cadena
|
|
como si fuera un número es un error que cometerás una vez.
|
|
|
|
[source,lua]
|
|
----
|
|
guiSetValue(gui, page, "score", string.format("%06d", score))
|
|
----
|
|
|
|
`guiSetValue` escribe en un elemento. En un control de formulario pone el valor
|
|
del control; en cualquier otra cosa -- un párrafo, un `div`, el
|
|
`<span id="score">` de tu documento -- sustituye lo que hay dentro del elemento,
|
|
y la página se vuelve a maquetar en el fotograma siguiente. `guiGetValue` es la
|
|
misma llamada al revés, y responde `nil` para un id que no tiene ningún
|
|
elemento, así que puedes preguntar por un elemento que quizá no exista sin
|
|
morir en el intento.
|
|
|
|
Esa es toda la API del día a día: `guiSetHandler`, `guiSetValue`, `guiGetValue`.
|
|
Hay muchísimo más a tu alcance a través de los propios objetos Lua de RmlUi, a
|
|
los que se llega desde la variable global `rmlui`, y el capítulo sobre la GUI de
|
|
la referencia explica cómo. Llegarás muy lejos antes de necesitarlo.
|
|
|
|
[#_hide_it_again]
|
|
=== Vuelve a esconderla
|
|
|
|
Un menú de pausa no está a la vista todo el tiempo. `guiHide` quita un
|
|
documento de la pantalla y `guiShow` lo vuelve a poner, conservando todo lo que
|
|
el jugador haya escrito o elegido.
|
|
|
|
La pausa en sí es `singeSetPauseFlag(true)`, que detiene el disco, los vídeos y
|
|
los sonidos pero *sigue llamando a tus callbacks*, así que todavía puedes
|
|
dibujar. El motor tiene su propia tecla de pausa, que congela el script por
|
|
completo, y las dos se pelearían; `singeSetPauseKeyEnabled(false)` le quita esa
|
|
tecla al motor para que `SWITCH_PAUSE` llegue a `onInputPressed` como cualquier
|
|
otro interruptor. En un teclado, esa tecla es la *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
|
|
----
|
|
|
|
Apagar la entrada junto con la página no es decoración. Una página escondida
|
|
que sigue recibiendo entrada se come las teclas de flecha del jugador durante
|
|
el resto de la partida.
|
|
|
|
[#_what_the_page_does_not_use_you_still_get]
|
|
=== Lo que la página no usa te sigue llegando
|
|
|
|
Esta es la regla que hace que sea seguro dejar una GUI a la vista durante la
|
|
partida, y vale la pena decirla claramente: *un evento que ha usado un elemento
|
|
nunca llega a tus callbacks.* Un Intro que ha pulsado el botón que tiene el
|
|
foco no llega como `onInputPressed`. Una flecha que ha movido el foco no llega
|
|
como `SWITCH_LEFT`. Un clic en un botón no es un interruptor del ratón.
|
|
|
|
Todo lo que la página no ha usado la atraviesa y te llega como siempre. Por eso
|
|
`P` cierra este menú: ningún elemento del documento usa `P`, así que la tecla
|
|
llega a `onInputPressed` exactamente igual que cuando el menú no está. También
|
|
es por eso que un HUD que recibe entrada sigue dejando pasar el botón de
|
|
disparo.
|
|
|
|
Hay una tecla que no sigue la regla. RmlUi usa la tecla Tab para mover el foco y
|
|
nunca la devuelve, así que Singe solo le pasa Tab a una GUI mientras alguien
|
|
está escribiendo de verdad en un campo de texto. Un juego que asigna Tab a algo
|
|
la conserva.
|
|
|
|
[#_style_it]
|
|
=== Dale estilo
|
|
|
|
Vuelve a mirar el `<head>` del documento. Una sola línea hace casi todo el
|
|
trabajo:
|
|
|
|
[source,html]
|
|
----
|
|
<link type="text/rcss" href="Singe/gui.rcss"/>
|
|
----
|
|
|
|
Ese es el tema que trae el motor, y es la razón de que tu panel tenga un borde,
|
|
un degradado y un título dorado sin que tú hayas elegido nada de eso. Sin una
|
|
hoja de estilo, RmlUi casi no dibuja nada, porque todos los elementos empiezan
|
|
en línea y del tamaño de su contenido, así que enlazar esta primero y
|
|
sobrescribir lo que no te guste es el camino más rápido hacia una página que
|
|
parezca algo.
|
|
|
|
El tema te da `.panel` (una caja con borde y esquinas redondeadas), `.list`
|
|
(una caja con desplazamiento cuyas filas `div` hijas se resaltan y reciben el
|
|
foco), `.muted` (texto más tenue para las pistas), títulos, y todos los
|
|
controles de formulario con sus estados al pasar el ratón, con el foco y
|
|
pulsados. El capítulo sobre la GUI de la referencia los enumera.
|
|
|
|
Todo lo que va después del enlace es tuyo. El bloque `<style>` de `pause.rml`
|
|
sobrescribe el tema para esta página: las reglas posteriores ganan a las
|
|
anteriores, y una regla que apunta a un `id` gana a una que apunta a una
|
|
`class`. En un juego de verdad, pon tus propias reglas en un archivo `.rcss`
|
|
junto al documento y enlázalo en segundo lugar, para que tu aspecto sea un solo
|
|
archivo para todas las páginas.
|
|
|
|
La unidad `dp` es un píxel del tamaño propio de la GUI -- tu GUI es del tamaño
|
|
del overlay, así que `380dp` son 380 píxeles del overlay. `margin: 80dp auto`
|
|
es el truco que centra un bloque de anchura conocida: `auto` reparte lo que
|
|
sobra a partes iguales entre los dos lados. No hay que medir nada ni hacer
|
|
cuentas, cosa que, después de la lección diecinueve, quizá te resulte un poco
|
|
molesta.
|
|
|
|
Para usar tu propia fuente en un documento, cárgala con `guiLoadFont` antes de
|
|
los documentos que la usan, y nómbrala en RCSS por el nombre de familia que hay
|
|
dentro del archivo de la fuente, no por el nombre del archivo.
|
|
|
|
[#_when_not_to_use_any_of_this]
|
|
=== Cuándo no usar nada de esto
|
|
|
|
Una GUI es una página. Es la respuesta correcta para un menú, una pantalla de
|
|
opciones, una tabla de récords, una pantalla de fin de partida o un HUD con una
|
|
maquetación de verdad.
|
|
|
|
Es la respuesta equivocada para una puntuación en una esquina. Eso son dos
|
|
líneas de `fontPrint` de la lección diecinueve, y meterlo en un documento no te
|
|
da nada y te cuesta una textura. Lo mismo vale para un punto de mira, una barra
|
|
de vida que has dibujado con `overlayBox` y una línea de texto de depuración.
|
|
|
|
Tres hechos lo deciden por ti.
|
|
|
|
Una GUI necesita una GPU. En una máquina sin ella -- un ordenador viejo, una Pi
|
|
pequeña, una máquina sin ningún controlador de Vulkan, Direct3D 12 o Metal --
|
|
no hay GUI, y `guiNew` no responde educadamente `false`: termina tu juego. La
|
|
escena 3D de la lección veintidós va por el mismo camino, así que no será la
|
|
última vez que lo preguntes. Pregunta antes a `singeHasGpu()` si tu juego tiene
|
|
que funcionar en los dos sitios, y ten una versión con el overlay de siempre a
|
|
la que recurrir. El menú del propio motor hace exactamente eso.
|
|
|
|
Una GUI cuesta una textura y una pasada de maquetación *en cada fotograma, la
|
|
dibujes o no*. Una GUI con la que has terminado debería borrarse con
|
|
`guiDelete`, no solo esconderse. Esconder es para una página que vas a volver a
|
|
mostrar dentro de un minuto.
|
|
|
|
Y una recarga se lleva por delante las GUI. `F5`, `-R` y cualquier cosa que
|
|
reinicie el script se llevan todas las GUI consigo, y el script las vuelve a
|
|
crear cuando se ejecuta. Eso está bien, siempre que las construyas en el cuerpo
|
|
de tu script y no en algún rincón que solo se ejecuta una vez.
|
|
|
|
[#_the_whole_script_2]
|
|
=== El script completo
|
|
|
|
[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]
|
|
=== Qué acaba de pasar
|
|
|
|
[source,lua]
|
|
----
|
|
if singeHasGpu() then
|
|
gui = guiNew(overlayGetWidth(), overlayGetHeight())
|
|
page = guiLoad(gui, DIR .. "pause.rml")
|
|
----
|
|
|
|
Toda la GUI se construye dentro de ese `if`, y `gui` se queda en `nil` en una
|
|
máquina sin GPU. Cada llamada que toca la GUI está protegida con
|
|
`if gui ~= nil`, y la pausa sigue funcionando sin menú. Son cuatro líneas de
|
|
más, y son la diferencia entre «no tiene menú en esa máquina» y «no arranca en
|
|
esa máquina».
|
|
|
|
[source,lua]
|
|
----
|
|
guiHide(gui, page)
|
|
guiSetInput(gui, false)
|
|
----
|
|
|
|
`guiLoad` muestra el documento que ha cargado, así que una página que tiene que
|
|
empezar escondida hay que esconderla enseguida. En una GUI nueva la entrada ya
|
|
está apagada, pero apagarla aquí hace que `openMenu` y `closeMenu` sean
|
|
exactamente la una el reflejo de la otra, y las parejas que se reflejan así son
|
|
mucho más difíciles de estropear.
|
|
|
|
[source,lua]
|
|
----
|
|
guiSetHandler(gui, page, "restart", "click", function()
|
|
score = 0
|
|
shipX = 0
|
|
closeMenu()
|
|
end)
|
|
----
|
|
|
|
El manejador es una función sin nombre, escrita donde se usa. Lua te deja
|
|
escribir `function(...) ... end` en cualquier sitio donde se espera un valor, y
|
|
como `guiSetHandler` espera una función, puedes darle una en el momento en
|
|
lugar de ponerle nombre primero y pasar el nombre. Llega a `score`, `shipX` y
|
|
`closeMenu` porque están declarados encima de ella en el mismo archivo, y esa es
|
|
justamente la razón de que los manejadores se preparen debajo de las cosas que
|
|
tocan.
|
|
|
|
[source,lua]
|
|
----
|
|
if not paused then
|
|
shipX = shipX + speed
|
|
----
|
|
|
|
El indicador de pausa detiene el disco y los sonidos, no tus cuentas.
|
|
`onOverlayUpdate` se sigue llamando -- de eso se trata, porque si no el menú no
|
|
se podría dibujar -- así que lo tuyo que tenga que pararse durante la pausa lo
|
|
tienes que parar tú.
|
|
|
|
[#_try_it_20]
|
|
=== Pruébalo
|
|
|
|
. *Añade un cuarto botón.* Copia la línea `<button id="quit">`, dale un id nuevo
|
|
y un texto nuevo, y añádele un manejador. Fíjate en que no ha hecho falta
|
|
volver a medir ni desplazar nada.
|
|
. *Cámbiale el estilo.* En el bloque `<style>`, cambia `#frame` por
|
|
`width: 260dp; margin: 20dp;` y mira cómo el panel se mueve y el texto se
|
|
vuelve a repartir en líneas él solo.
|
|
. *Quita el `guiSetInput(gui, true)` de `openMenu`.* La página sigue
|
|
apareciendo. Averigua, solo con lo que ves en la pantalla, qué has perdido.
|
|
. *Deja la página a la vista todo el tiempo.* Borra las llamadas a `guiHide`,
|
|
cambia las dos llamadas `guiSetInput(gui, false)` a `true` y dibuja la GUI en
|
|
cada fotograma en lugar de solo durante la pausa. Luego pulsa las teclas de
|
|
flecha e Intro mientras vuela la nave. La página se las queda: el foco se
|
|
mueve, el deslizador cambia la velocidad e Intro pulsa el botón que tenga el
|
|
foco. Un juego que se manejara con esas teclas no las oiría nunca. Por eso un
|
|
HUD lleva `guiSetInput(gui, false)`.
|
|
. *Pon la puntuación en la página en lugar de en el HUD.* Llama a `guiSetValue`
|
|
para `score` en cada fotograma en lugar de solo en `openMenu`, y borra la
|
|
línea de `fontPrint`. Luego decide cuál de las dos prefieres de verdad, y por
|
|
qué.
|
|
|
|
[#_break_it_on_purpose_19]
|
|
=== Rómpelo a propósito
|
|
|
|
Cambia un id en `pause.rml`, de `id="resume"` a `id="continue"`, y deja el
|
|
script como está. Singe arranca y muere:
|
|
|
|
----
|
|
52:guiSetHandler: No element "resume" in document 0 of GUI 0.
|
|
----
|
|
|
|
Léelo como lees cualquier error del motor. La línea, la llamada que se ha
|
|
quejado y luego la queja -- y esta queja es precisa. No dice que el documento
|
|
esté roto; el documento se ha cargado perfectamente. Dice que has pedido un
|
|
elemento por un nombre que no tiene nada de lo que hay en él.
|
|
|
|
`guiSetHandler` y `guiSetValue` mueren las dos con un id que no existe, porque
|
|
un manejador que querías poner y no pusiste es un fallo del que quieres
|
|
enterarte en el acto. `guiGetValue` es la excepción: responde `nil`, así que
|
|
puedes preguntar si algo está ahí.
|
|
|
|
El otro error que vale la pena conocer es el que te sale cuando el propio
|
|
documento no se carga. Cambia el nombre de `pause.rml` y ejecútalo. Esta vez
|
|
Singe nombra el archivo y cita el motivo que da RmlUi, y las quejas de RmlUi
|
|
sobre tu marcado y tus estilos van a la consola tanto si la carga sale bien
|
|
como si no. Lee esa consola la primera vez que una página salga en blanco;
|
|
normalmente ya habrá dicho por qué.
|
|
|
|
[#_what_you_learned_20]
|
|
=== Lo que has aprendido
|
|
|
|
* Una GUI es un rectángulo de tamaño fijo en el que se maquetan y se dibujan
|
|
documentos; `guiNew` crea una y `guiDelete` la libera.
|
|
* `guiLoad` pone un documento RML en una GUI y lo muestra; `guiHide`, `guiShow`
|
|
y `guiClose` lo gestionan después.
|
|
* `guiDraw` compone la GUI sobre el overlay durante un fotograma, así que va en
|
|
`onOverlayUpdate` como todas las demás llamadas de dibujo.
|
|
* Una GUI nueva solo muestra; `guiSetInput` es lo que la hace usable.
|
|
* `guiSetHandler` ejecuta tu función cuando se hace clic en un elemento o se
|
|
cambia, `guiSetValue` escribe en un elemento y `guiGetValue` lee uno.
|
|
* Un evento que ha usado un elemento nunca llega a tus propios callbacks; todo
|
|
lo demás pasa como siempre.
|
|
* `Singe/gui.rcss` es el tema incluido; enlázalo primero y sobrescríbelo en un
|
|
bloque `<style>` o en una hoja tuya.
|
|
* Una GUI necesita una GPU, así que pregunta a `singeHasGpu()` si tu juego
|
|
tiene que funcionar sin ella.
|
|
* Cada GUI se renderiza en cada fotograma la dibujes o no, así que borra las que
|
|
ya no necesites.
|
|
* Una puntuación en la esquina son dos líneas de `fontPrint`, no un documento.
|
|
|
|
[#_next_time_20]
|
|
=== La próxima vez
|
|
|
|
Hay un tipo más de texto en pantalla, y tiene su propio sistema por un motivo
|
|
que no tiene nada que ver con su aspecto. La lección veintiuno trata de los
|
|
subtítulos, y de los jugadores que no pueden jugar a tu juego sin ellos.
|