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