singe/docs/es/lessons/31-cabinet.adoc

754 lines
28 KiB
Text

[#_lesson_31_in_a_cabinet]
== Lección 31: En una recreativa
image::learn/31-cabinet.png[La lección terminada, 480]
Hay una diferencia entre un juego y una máquina recreativa, y no es la madera.
Una máquina se enciende sola por la mañana, juega para una sala vacía, coge la
moneda de alguien que no ha leído ni una palabra sobre ella y sigue funcionando
a medianoche sin que nadie haya tocado un teclado, porque no hay teclado. Todo
lo de esta lección sale de ahí.
No necesitas una recreativa para hacer nada de esto. Un stick y dos botones
encima de una mesa, o un mando, bastan para encontrar todos los errores de esta
lección, y casi todo lo que aprendas aquí también mejora un juego en un
ordenador de sobremesa.
[#_there_is_no_keyboard]
=== No hay teclado
Empieza por lo que falta, porque es lo que rompe los juegos.
Una recreativa tiene un stick, unos cuantos botones, una puerta del monedero, un
botón de inicio y, en algún sitio dentro, un pequeño interruptor al que llega el
operador. Eso es todo. Ni Esc, ni letras, ni ratón, ni forma de escribir. Así
que:
* Todos los menús de tu juego tienen que poder usarse con arriba, abajo y un
botón. Si alguna pantalla de tu juego dice «pulsa Y para continuar», en una
recreativa no funciona.
* Meter las iniciales es cosa de tres botones y una letra que va cambiando, no
de un campo de texto. Por eso los juegos de recreativa tienen nombres de tres
letras.
* Un aviso que nombra una tecla por su letra está mal. Di «botón 1», o dibuja el
botón.
* No hay salida. Un jugador no puede salir, y no debería poder.
Repasa tu juego una vez, pantalla por pantalla, y pregúntate en cada una:
¿podría alguien pasar esto con un stick y un botón? Ese repaso de tu propio
juego vale más que todo el resto de esta lección.
[#_controls_cfg]
=== controls.cfg
Desde la lección tres has estado leyendo la entrada como `SWITCH_UP`,
`SWITCH_BUTTON1`, `SWITCH_COIN1` y demás, y ni una sola vez has dicho qué tecla
son. Esa es precisamente su gracia. En algún punto entre el microinterruptor de
la recreativa y tu `onInputPressed` hay un archivo que lo decide, y tu juego no
forma parte de esa conversación.
El archivo es `controls.cfg`. Es Lua, como tu script, y es una lista de tablas
-- una por interruptor -- que nombran todo lo que puede producirlo:
[source,lua]
----
INPUT_1P_START = { SCANCODE.MAIN_1, GAMEPAD_0.BUTTON_START }
INPUT_1P_COIN = { SCANCODE.MAIN_5, GAMEPAD_0.BUTTON_LEFT_BUMPER }
INPUT_ACTION_1 = { SCANCODE.LCTRL, GAMEPAD_0.BUTTON_A }
INPUT_UP = { SCANCODE.UP, GAMEPAD_0.DPAD_UP, GAMEPAD_0.AXIS_LEFT_Y_U }
----
Cada nombre `INPUT_` del archivo se convierte en el valor `SWITCH_`
correspondiente de tu juego: `INPUT_ACTION_1` es el `SWITCH_BUTTON1` que has
estado comprobando, e `INPUT_1P_COIN` es `SWITCH_COIN1`. Todas las entradas de
una tabla producen el mismo interruptor, así que a un juego le da igual si el
jugador pulsó la tecla, el botón del mando o movió el stick.
No tienes que escribir el archivo entero. Singe empieza con sus propios valores
por defecto y lee `controls.cfg` de cuatro sitios por turno -- la carpeta del
script del juego, la carpeta desde la que se arrancó Singe, la carpeta que está
por encima de la carpeta de datos del juego y la carpeta de datos del juego -- y
cada uno que encuentra sustituye a lo anterior, clave por clave. Así que una
recreativa tiene un archivo arriba del todo para toda la máquina, y un juego que
necesita un botón más tiene un archivo propio de cuatro líneas. En la carpeta
`Singe` hay un `controls.cfg.example` del que copiar.
`DEAD_ZONE` también está ahí: cuánto tiene que moverse un stick antes de contar
como pulsado, en las unidades en bruto que da el hardware, donde el recorrido
completo es `32767`. El valor por defecto es `15000`. Un stick gastado que se
desliza solo necesita un número más grande, y `DEAD_ZONES` fija un eje cada vez
para ese stick que está cansado. La sección del manual sobre cómo personalizar
los controles lo cubre todo.
[#_every_device_is_a_gamepad]
=== Todo dispositivo es un mando
Los botones de una recreativa van cableados a una placa encoder, que el
ordenador ve como algún tipo de joystick, y que no se parece en nada a un mando
de Xbox. Singe escribe él mismo una asignación para un dispositivo así y luego
lo abre como `GAMEPAD_0`, así que llega a `controls.cfg` y a tu juego
exactamente igual que un mando reconocido. Hay una sola familia de dispositivos
y ningún segundo vocabulario que aprender.
La asignación que se inventa es directa: los botones de la placa, en orden,
pasan a ser `BUTTON_A`, `BUTTON_B`, `BUTTON_X`, `BUTTON_Y` y así sucesivamente.
Si los botones de tu recreativa no están cableados en ese orden, saldrán en los
sitios equivocados, y el arreglo no está en tu juego: `--program` escribe en la
traza la asignación que escribió Singe, de una forma que puedes editar y poner
en un `gamecontrollerdb.txt` junto al juego.
Cuando un botón «no hace nada», la herramienta *Input Test* del menú responde a
la pregunta en cinco segundos. Muestra todos los dispositivos que ve el motor y
el último interruptor que habría recibido un juego. O el interruptor llega y tu
juego lo está ignorando, o no llega nunca y el problema es la asignación o el
cable.
[#_four_switches_are_not_yours]
=== Cuatro interruptores no son tuyos
La mayoría de los interruptores van directos a tu juego. Cuatro no: de la pausa,
la salida, la captura de pantalla y atrapar o soltar el ratón se encarga el
propio motor.
La pausa es útil de verdad y gratis. Mientras la tiene el motor, la tecla
asignada a `INPUT_PAUSE` lo congela todo -- el disco, todos los vídeos, todos
los sonidos y tu script -- dibuja su propio indicador y lo descongela todo con
la siguiente pulsación. Todo lo que tu juego creía que estaba pulsado se suelta
como es debido y se vuelve a pulsar después, así que un stick que se mantiene
durante una pausa nunca se queda atascado.
La salida es la que hay que quitar, y eso lo hace una sección posterior.
[#_coins_credits_and_start]
=== Monedas, créditos e inicio
Una máquina recreativa tiene una forma más antigua que cualquiera de nosotros, y
los jugadores la conocen sin que nadie se la cuente:
. *Atracción.* No hay nadie. La máquina muestra lo que es, hace parpadear
`INSERT COIN` y no pide nada.
. *Crédito.* Una moneda llega como `SWITCH_COIN1`. Las monedas se convierten en
créditos al cambio que haya fijado el operador -- no siempre una por uno.
. *Juego.* `SWITCH_START1` gasta un crédito y empieza.
. *Fin de partida.* La puntuación, un momento, y vuelta al modo de atracción por
sí sola. Nadie pulsa nada para salir; el jugador ya se ha ido.
Dos detalles en los que la gente se equivoca. Las monedas se cuentan, no se
gastan: a dos monedas por crédito, una moneda tiene que quedarse ahí esperando a
su pareja, y un jugador que echó una y se fue se la ha dejado al siguiente. Y la
máquina tiene que encontrar sola el camino de vuelta al modo de atracción,
porque lo último que hace un jugador es dejar de jugar.
[#_behind_the_service_switch]
=== Tras el interruptor de servicio
Dentro de la puerta del monedero hay un botón que el jugador no ve nunca y que
el operador usa constantemente. Es `SWITCH_SERVICE`, y el motor te lo pasa
directamente.
Un operador que abre tu menú de servicio espera encontrar, más o menos en este
orden:
* *el precio* -- monedas por crédito, y un ajuste de juego gratis para una
máquina en casa o en una feria;
* *la dificultad*, porque una máquina en un bar y una máquina en un salón
recreativo piden respuestas distintas;
* *una forma de borrar la tabla de récords*, que es lo más pedido de toda esta
lista;
* *qué máquina es* -- la versión del motor, la pantalla, dónde viven los datos
-- para que un informe de avería tenga hechos dentro;
* *una forma de salir*, sin apagar nada.
Todo lo que cambia un operador tiene que sobrevivir a un corte de corriente, que
en un salón recreativo significa un corte en mitad de una partida, sin aviso,
porque alguien tira de un enchufe. `saveSet` escribe al final del fotograma, que
es bastante pronto para una puntuación y no lo bastante para un ajuste que
alguien acaba de cambiar. `saveFlush()` escribe en el acto, y un menú de
servicio es justo para lo que está.
Vale la pena conocer las herramientas de servicio del propio menú incluido, tras
el mismo interruptor, ya que están en todas las máquinas en las que se ejecutará
tu juego: la prueba de entrada, las pruebas de sonido y de pistola de luz, la
geometría de la pantalla, el retardo del audio, la cuenta en línea y una página
que borra los datos guardados de cualquier juego. El tuyo no tiene que
duplicarlas.
[#_a_machine_that_starts_itself_and_never_quits]
=== Una máquina que arranca sola y nunca sale
La recreativa se enciende desde el enchufe. Nadie inicia sesión. Tiene que haber
un juego en la pantalla en un minuto más o menos, y seguir ahí.
Hacer que Singe se ejecute al encender es trabajo de tu sistema operativo, no de
Singe -- la máquina arranca el `Menu.sh` o el `Menu.bat` que el motor escribe
junto a tus juegos, o tu juego directamente. Lo que sí es trabajo tuyo es lo que
pasa durante el resto del día.
Quita la tecla de salida:
[source,lua]
----
singeSetQuitKeyEnabled(false)
----
Entonces el motor deja de hacer caso al interruptor asignado a `INPUT_QUIT` y te
lo entrega como `SWITCH_QUIT`, como cualquier otro, para que puedas preguntar
antes, guardar antes o no hacer nada en absoluto. El botón de cerrar de la
ventana siempre sale, pongas lo que pongas, y eso importará dentro de un
momento.
Además, unas cuantas opciones son de la máquina y no de tu script, y van en un
`settings.cfg` junto a los juegos -- los mismos nombres que las opciones de
línea de comandos, sin los guiones:
[source,lua]
----
fullscreen = true
screen = 2
idleexit = 900
startsilent = true
----
`idleexit` sale tras esa cantidad de segundos sin ninguna entrada de ningún
tipo, para una recreativa que debe devolverle la pantalla a un menú o a un
front-end. `startsilent` arranca en silencio hasta que alguien toca algo, que es
lo que hace soportable una sala llena de máquinas. Hay más; las secciones de
ajustes y de línea de comandos del manual los enumeran todos.
[#_artwork_round_the_picture]
=== Arte alrededor de la imagen
Una recreativa de verdad tiene alrededor de la pantalla un bezel impreso (un
marco decorativo): arte con un hueco por el que se ve el juego. Singe te dibuja
uno.
----
Singe --bezel cabinet.png MyGame.game
----
El archivo se busca en una carpeta `bezels` dentro del juego -- un juego
empaquetado puede llevar el suyo -- y luego en una carpeta `bezels` de la
carpeta de datos, para que un jugador pueda ponerle su propio arte a un juego
sin tocarlo. Junto a la imagen, con su mismo nombre, un pequeño archivo de Lua
dice dónde va la imagen del juego, en los propios píxeles del arte:
[source,lua]
----
-- bezels/cabinet.cfg
CUTOUT = { x = 200, y = 120, width = 800, height = 500 }
----
Como esos números son los del propio arte, siguen siendo correctos con cualquier
tamaño de ventana: la imagen y el hueco se escalan juntos. Sin ese archivo, la
imagen del juego ocupa toda la ventana, como siempre.
Por defecto el arte queda detrás de todo y tu juego tapa el hueco.
`--bezelflip` lo pone delante, así que solo su transparencia deja ver el juego,
que es lo que quieres para bordes suaves o con forma -- y entonces tu overlay
también queda debajo, salvo que digas lo contrario:
[source,lua]
----
if mainBezelLoaded() then
setOverlayOnTop(true)
end
----
`mainBezelLoaded()` también te dice si hay arte o no, y así es como un juego
decide dibujar su propio marco cuando no lo hay.
[#_a_monitor_on_its_side]
=== Un monitor de lado
Muchos monitores de recreativa están montados en vertical. Gira toda la
presentación:
----
Singe --rotate=90 MyGame.game
----
La imagen, el overlay, cualquier GUI, una escena 3D y las partículas giran todos
juntos, y las posiciones del ratón y de la pistola de luz se giran de vuelta,
así que un clic sigue cayendo donde lo ve el jugador. Un cuarto de vuelta
también intercambia la forma del área, así que una ventana alta se llena en
lugar de dejar la imagen metida en una franja.
Solo se aceptan `0`, `90`, `180` y `270`. Un script puede cambiarlo mientras se
ejecuta con `vldpSetRotate`, que responde `false` a cualquier otra cosa en lugar
de terminar tu juego, y `vldpGetRotate()` lo vuelve a leer -- algo que vale la
pena poner en una pantalla de servicio, porque «la imagen está de lado» y «el
monitor está de lado» son averías distintas.
Lo que no hace la rotación es reescribir tu juego. Un juego dibujado para una
pantalla ancha, girado noventa grados, es un juego ancho tumbado. Diseñar para
una pantalla alta significa diseñar para una pantalla alta.
[#_the_script_3]
=== El script
La forma completa: atracción, moneda, crédito, juego, fin de partida, servicio y
una salida que pregunta. El juego del medio es un botón y un temporizador de
quince segundos, porque esta vez lo importante no es el juego -- es la máquina
que lo rodea.
[source,lua]
----
dofile("Singe/Framework.singe")
local ROUND_MS = 15000
local MENU = { "Coins per credit", "Difficulty", "Free play", "Clear high score", "Exit service" }
local HARDNESS = { "Easy", "Normal", "Hard" }
local coinsPerCredit = saveGet("coinsPerCredit", 1)
local difficulty = saveGet("difficulty", 2)
local freePlay = saveGet("freePlay", false)
local best = saveGet("best", 0)
local state = "attract"
local goBackTo = "attract"
local coins = 0
local credits = 0
local score = 0
local endsAt = 0
local item = 1
local saying = ""
local hasBezel = false
local function wrap(value, low, high)
if value < low then
return high
end
if value > high then
return low
end
return value
end
local function canStart()
return freePlay or credits > 0
end
local function insertCoin()
coins = coins + 1
while coins >= coinsPerCredit do
coins = coins - coinsPerCredit
credits = credits + 1
end
end
local function startGame()
if not freePlay then
credits = credits - 1
end
score = 0
endsAt = singeGetTicks() + ROUND_MS
state = "play"
end
local function endGame()
if score > best then
best = score
saveSet("best", best)
end
saveFlush()
endsAt = singeGetTicks() + 5000
state = "over"
end
local function settingOf(which)
if which == 1 then
return tostring(coinsPerCredit)
elseif which == 2 then
return HARDNESS[difficulty]
elseif which == 3 then
if freePlay then
return "on"
end
return "off"
elseif which == 4 then
return tostring(best)
end
return ""
end
local function adjust(step)
if item == 1 then
coinsPerCredit = wrap(coinsPerCredit + step, 1, 4)
saveSet("coinsPerCredit", coinsPerCredit)
elseif item == 2 then
difficulty = wrap(difficulty + step, 1, #HARDNESS)
saveSet("difficulty", difficulty)
elseif item == 3 then
freePlay = not freePlay
saveSet("freePlay", freePlay)
end
saveFlush()
end
local function serviceInput(what)
if what == SWITCH_UP then
item = wrap(item - 1, 1, #MENU)
elseif what == SWITCH_DOWN then
item = wrap(item + 1, 1, #MENU)
elseif what == SWITCH_LEFT then
adjust(-1)
elseif what == SWITCH_RIGHT then
adjust(1)
elseif what == SWITCH_BUTTON1 then
if item == 4 then
best = 0
saveSet("best", 0)
saveFlush()
saying = "High score cleared."
elseif item == 5 then
state = goBackTo
end
elseif what == SWITCH_BUTTON2 or what == SWITCH_SERVICE then
state = goBackTo
end
end
function onInputPressed(what)
if what == SWITCH_QUIT then
if state == "confirm" then
singeQuit()
else
goBackTo = state
state = "confirm"
end
return
end
if state == "confirm" then
if what == SWITCH_BUTTON2 then
state = goBackTo
end
return
end
if state == "service" then
serviceInput(what)
return
end
if what == SWITCH_SERVICE then
goBackTo = state
saying = ""
state = "service"
return
end
if what == SWITCH_COIN1 then
insertCoin()
return
end
if state == "play" then
if what == SWITCH_BUTTON1 then
score = score + 10 * difficulty
end
return
end
if what == SWITCH_START1 and canStart() then
startGame()
end
end
local function drawAttract()
overlayPrint(2, 2, "ROCK DODGER")
if canStart() then
overlayPrint(2, 4, "PRESS START")
elseif (singeGetTicks() // 400) % 2 == 0 then
overlayPrint(2, 4, "INSERT COIN")
end
overlayPrint(2, 6, "High score " .. best)
end
local function drawService()
overlayPrint(2, 0, "SERVICE MENU")
for row, name in ipairs(MENU) do
if row == item then
overlayPrint(1, row + 1, ">")
end
overlayPrint(3, row + 1, name)
overlayPrint(24, row + 1, settingOf(row))
end
overlayPrint(2, 8, saying)
overlayPrint(2, 10, "Singe " .. SINGE_VERSION_STRING .. " rotation " .. vldpGetRotate())
if hasBezel then
overlayPrint(2, 11, "Bezel artwork loaded.")
else
overlayPrint(2, 11, "No bezel artwork.")
end
overlayPrint(2, 12, singeGetDataPath())
overlayPrint(2, 16, "Up/Down choose. Left/Right change. Button 1 does it.")
overlayPrint(2, 17, "Button 2 or the service switch leaves.")
end
function onOverlayUpdate()
overlayClear()
if state == "play" and singeGetTicks() >= endsAt then
endGame()
elseif state == "over" and singeGetTicks() >= endsAt then
state = "attract"
end
if state == "service" then
drawService()
elseif state == "confirm" then
overlayPrint(2, 2, "QUIT THIS GAME?")
overlayPrint(2, 4, "Quit again to quit. Button 2 stays.")
elseif state == "play" then
overlayPrint(2, 2, "SCORE " .. score)
overlayPrint(2, 3, "TIME " .. ((endsAt - singeGetTicks()) // 1000))
overlayPrint(2, 5, "Button 1 scores. " .. HARDNESS[difficulty] .. ".")
elseif state == "over" then
overlayPrint(2, 2, "GAME OVER")
overlayPrint(2, 4, "You scored " .. score .. ".")
overlayPrint(2, 6, "High score " .. best)
else
drawAttract()
end
if state ~= "service" then
overlayPrint(2, 17, "CREDITS " .. credits .. " COINS " .. coins .. "/" .. coinsPerCredit)
end
return OVERLAY_UPDATED
end
singeSetQuitKeyEnabled(false)
hasBezel = mainBezelLoaded()
if hasBezel then
setOverlayOnTop(true)
end
----
Ejecútalo y pulsa `5` para echar una moneda, `1` para empezar y `9` para el
interruptor de servicio -- son los valores por defecto del `controls.cfg` de
serie. Luego pulsa Esc y lee lo que dice antes de volver a pulsarla.
[#_what_just_happened_26]
=== Qué acaba de pasar
[source,lua]
----
local coinsPerCredit = saveGet("coinsPerCredit", 1)
local difficulty = saveGet("difficulty", 2)
local freePlay = saveGet("freePlay", false)
----
Los ajustes del operador se leen una vez, arriba, con un valor razonable para
una máquina que nunca se ha configurado. `saveGet` con un segundo argumento
devuelve ese valor cuando no se ha guardado nada, y así se distingue la primera
ejecución de las siguientes.
[source,lua]
----
local function insertCoin()
coins = coins + 1
while coins >= coinsPerCredit do
coins = coins - coinsPerCredit
credits = credits + 1
end
end
----
Entran monedas, salen créditos, al cambio que haya fijado el operador. Que sea
un `while` y no un `if` no es prudencia ante una entrada imposible -- es el
operador bajando el precio de cuatro monedas a una mientras hay tres monedas
esperando ahí dentro.
[source,lua]
----
if what == SWITCH_QUIT then
if state == "confirm" then
singeQuit()
else
goBackTo = state
state = "confirm"
end
return
end
----
El interruptor de salida solo llega a tu juego por el
`singeSetQuitKeyEnabled(false)` que hay al final del todo del script. Pulsarlo
una vez pregunta; pulsarlo otra vez, desde la pantalla de la pregunta, hace que
llames tú mismo a `singeQuit()`. `goBackTo` recuerda dónde estaba el jugador,
así que decir que no lo devuelve a su partida en lugar de a la pantalla de
atracción.
Este bloque va el primero de la función y hace `return`, así que nada más puede
tragarse el interruptor. Todos los demás estados del juego se tratan debajo.
[source,lua]
----
if state == "service" then
serviceInput(what)
return
end
----
La misma forma otra vez. Mientras el menú de servicio está abierto, todos los
interruptores son suyos -- el stick no mueve el juego de debajo y una moneda no
cae en mitad de un ajuste. Un `if`, pronto, con un `return`. Cualquier pantalla
que se pone encima de otra necesita esto, y un juego al que le salen tres y no
lo tiene produce fallos casi imposibles de describir.
[source,lua]
----
saveSet("coinsPerCredit", coinsPerCredit)
...
saveFlush()
----
Fijar y luego volcar. Sin el volcado, la escritura ocurre al final del
fotograma, que es rápido pero no inmediato. Un operador cambia un ajuste y
desenchufa la máquina diez segundos después, y este es el único sitio de un
juego donde eso pasa de verdad.
[source,lua]
----
elseif (singeGetTicks() // 400) % 2 == 0 then
overlayPrint(2, 4, "INSERT COIN")
end
----
El parpadeo. `singeGetTicks()` son los milisegundos desde que arrancó el motor,
`//` divide y tira el resto, así que el número cambia cada cuatrocientos
milisegundos, y `% 2` hace que vaya alternando entre cero y uno. Ni variable, ni
temporizador, ni nada que reiniciar: el reloj ya estaba contando.
[source,lua]
----
overlayPrint(2, 10, "Singe " .. SINGE_VERSION_STRING .. " rotation " .. vldpGetRotate())
----
La mitad del operador de la página de servicio. Una versión, una rotación, si
hay arte o no, y dónde viven los datos. Nada de eso es para el jugador, y todo
es lo que te gustaría tener cuando alguien te llame por teléfono por una máquina
que está a quinientos kilómetros.
[source,lua]
----
singeSetQuitKeyEnabled(false)
hasBezel = mainBezelLoaded()
----
Al final, fuera de todas las funciones, así que se ejecuta una vez cuando se
carga el script. Ninguna de las dos cosas es para hacerla en cada fotograma, y
`mainBezelLoaded` no puede cambiar mientras el juego se ejecuta.
[#_try_it_31]
=== Pruébalo
. *Cambia el precio.* Abre el menú de servicio, pon dos monedas por crédito y
echa una moneda. Mira cómo no aparece el crédito, y mira el contador de
monedas de la última línea. Luego sal, vuelve a arrancarlo y comprueba que el
precio se ha quedado.
. *Juega gratis.* Activa el juego gratis y comprueba que el inicio funciona sin
monedas y que los créditos no se vuelven negativos.
. *Añade una página.* Pon una segunda página de información de servicio tras el
botón 3 -- los campos de `singeGetSystemInfo()` son un buen comienzo, y su
entrada en la referencia los enumera todos.
. *Gira la pantalla.* Ejecútalo con `--rotate=90` y mira cómo queda tu diseño.
Luego haz que la página de servicio se pueda leer con esa rotación, que es más
difícil de lo que parece y es justo el trabajo que pide una recreativa
vertical.
. *Quita el teclado.* Desenchúfalo, o prométete no tocarlo, y recorre todas las
pantallas con un mando. Cualquier cosa a la que no llegues es un fallo.
[#_break_it_on_purpose_30]
=== Rómpelo a propósito
Borra el bloque de `SWITCH_QUIT` de `onInputPressed`, dejando
`singeSetQuitKeyEnabled(false)` donde está, y ejecútalo.
Ahora no hay nada que salga. Esc no hace nada, `Q` no hace nada y tu juego no
tiene salida, que es exactamente lo que pediste. No es un fallo y no hay ningún
mensaje; el motor ha hecho exactamente lo que le dijeron.
Cierra la ventana con su botón de cerrar. Eso funciona siempre, diga lo que diga
un script, y es la puerta que te dejaste abierta. A pantalla completa,
`Alt-Enter` te devuelve primero a una ventana.
Vale la pena sufrir este en tus propias carnes. Desactivar la tecla de salida es
lo único de este libro que puede dejarte con un programa del que no puedes
salir, y la costumbre que lo evita es sencilla: escribe la salida en la misma
sesión en que desactivas la tecla de salida, nunca más tarde.
[#_what_you_learned_31]
=== Lo que has aprendido
* Una recreativa no tiene teclado, así que todas las pantallas del juego tienen
que funcionar con un stick y un botón.
* `controls.cfg` asigna teclas, botones de mando y direcciones del stick a los
nombres `SWITCH_` que tu juego ya usa, y tu juego nunca sabe cuál se pulsó.
* Se leen por turno cuatro archivos de asignaciones, cada uno sustituyendo al
anterior, así que pueden existir a la vez un archivo para toda la máquina y
uno para cada juego.
* Todo dispositivo que abre Singe es un mando, incluida una placa encoder de
recreativa, y la herramienta Input Test dice lo que está llegando.
* El motor se queda la pausa, la salida, la captura de pantalla y atrapar el
ratón; el resto son tuyos.
* Las monedas se cuentan y se convierten en créditos al cambio del operador, y
una máquina vuelve sola al modo de atracción.
* `SWITCH_SERVICE` es el interruptor del operador, y lo que hay tras él es el
precio, la dificultad, una forma de borrar las puntuaciones y qué máquina es.
* `saveFlush()` escribe un ajuste ya, porque una recreativa se queda sin
corriente sin avisar.
* `singeSetQuitKeyEnabled(false)` le pasa el interruptor de salida a tu juego.
Escribe la salida en ese mismo momento.
* Un bezel es arte con un hueco, y `--rotate` gira toda la presentación para un
monitor vertical.
[#_the_end]
=== Fin
Eso son treinta y una lecciones, y sabes escribir un juego.
No uno perfecto, y todavía no uno grande, pero lo que hay entre aquí y
cualquiera de las dos cosas es tiempo y práctica, no otro libro. Empezaste con
cinco líneas que ponían una palabra en una pantalla negra. Ahora sabes qué es
una variable, para qué sirve una función, cómo dibujar, cómo hacer ruido, cómo
saber cuándo se han tocado dos cosas, cómo guardar una puntuación en algún sitio
donde sobreviva, cómo hacer de una película un mundo, cómo poner un modelo en
una habitación con una luz encima y cómo darle todo a un desconocido.
Lo que necesitas ahora es la _Referencia de Singe_, no este libro. Es la
referencia de cada función, cada argumento y cada valor devuelto, y es lo que de
verdad tendrás abierto mientras trabajas. Viene con este libro, como
`Reference.pdf` y `Reference.html`. Lee la entrada antes de usar la llamada; en la mayoría hay
una nota sobre el error que comete la gente con ella.
Lee también los scripts que vienen con Singe. `Singe/Menu.singe`,
`Singe/Tools.singe` y `Singe/Framework.singe` no son demostraciones escritas para
leerse -- son el menú y las herramientas de servicio que llevas usando todo este
tiempo, haciendo trabajo de verdad con las mismas funciones que tienes tú.
Averiguar cómo hace algo el programa de otra persona es una habilidad en sí
misma, y esos son un sitio amable para practicarla.
Y está Forge, una herramienta aparte con su propio libro, para describir un
juego en lugar de escribirlo. Es otro camino. No lo necesitas, pero ahí está.
El próximo juego es tuyo. Haz primero el pequeño, termínalo y pónselo delante a
alguien. Terminar es la parte que nadie te dice que es la difícil, y es la única
que convierte una carpeta llena de scripts en un juego al que alguien ha jugado.
Ve y constrúyelo.