754 lines
28 KiB
Text
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.
|