[#_lesson_15_the_other_way_to_write_it] == Lección 15: La otra forma de escribirlo image::learn/15-threaded.png[La lección terminada, 480] Hasta ahora, todas las lecciones se han escrito de la misma manera. Escribes funciones cuyos nombres empiezan por `on`, Singe las llama cuando le parece, y tú nunca dices en qué orden pasa nada. Ese es el modelo *dirigido por eventos*, y es como funcionan la mayoría de los motores de juegos. Singe tiene un segundo modelo. En él escribes tu juego como una lista de pasos, de la primera línea a la última, y se te permite esperar. En esta lección escribirás la misma secuencia corta de vídeo de las dos formas y verás la diferencia con tus propios ojos. [#_the_sequence] === La secuencia Esto es lo que hace el juego, dicho en español: . Empieza la película desde el principio y reprodúcela hasta el fotograma 120. . Para ahí y pide al jugador que pulse la barra espaciadora. . Cuando la pulse, sigue reproduciendo hasta el fotograma 300. . Para ahí, di «The end.», espera cuatro segundos y sal. Cuatro pasos. Vuelve a leerlos y fíjate en que todos llevan la palabra «después», dicha o no. [#_the_way_you_know] === La forma que ya conoces Crea un archivo llamado `events.singe` en la carpeta `movie` de la lección catorce, y escribe esto: [source,lua] ---- dofile("Singe/Framework.singe") local step = 1 local endedAt = 0 function onInputPressed(what) if what == SWITCH_BUTTON1 and step == 2 then step = 3 discPlay() end end function onOverlayUpdate() local frame = discGetFrame() overlayClear() overlayPrint(2, 2, "Frame " .. frame) if step == 1 then if frame >= 120 then discPause() step = 2 end elseif step == 2 then overlayPrint(2, 4, "Press the space bar to go on.") elseif step == 3 then if frame >= 300 then discPause() endedAt = singeGetTicks() step = 4 end elseif step == 4 then overlayPrint(2, 4, "The end.") if singeGetTicks() - endedAt > 4000 then singeQuit() end end return OVERLAY_UPDATED end discSearch(0) discPlay() ---- Ejecútalo. Nombrar la carpeta ejecutaría `movie.singe`, así que nombra el script en su lugar, como te enseñó la lección uno: ---- Singe -R -v Singe/menuBackground.mkv movie/events.singe ---- Funciona. Hace exactamente los cuatro pasos. `singeGetTicks` es nueva -- te devuelve el número de milisegundos que han pasado desde que arrancó el motor, que es la forma normal de medir un rato -- y `singeQuit` termina el juego, que es lo que el motor hace por ti cuando pulsas Esc. Ahora lee el script e intenta encontrar en él los cuatro pasos. Están, pero del revés. El orden de la historia está escrito en los *valores* de `step`, no en el orden de las líneas, y `step` se cambia en una función y se lee en otra. Para responder a «¿qué pasa después de que el jugador pulse la barra espaciadora?» tienes que tener todo el asunto en la cabeza a la vez. Y esta es la versión fácil. Tiene dos decisiones. [#_the_other_way] === La otra forma Empieza un archivo nuevo, `steps.singe`, y escribe esto: [source,lua] ---- local message = "" local pressed = false local function drawFrame() overlayClear() overlayPrint(2, 2, "Frame " .. discGetFrame()) overlayPrint(2, 4, message) end local function playTo(frame) while discGetFrame() < frame do drawFrame() singeYield() end end local function waitForButton() pressed = false while not pressed do drawFrame() singeYield() end end local function waitSeconds(seconds) local stopTime = singeGetTicks() + seconds * 1000 while singeGetTicks() < stopTime do drawFrame() singeYield() end end function onInputPressed(what) if what == SWITCH_BUTTON1 then pressed = true end end function singeMain() discSearch(0) discPlay() playTo(120) discPause() message = "Press the space bar to go on." waitForButton() message = "" discPlay() playTo(300) discPause() message = "The end." waitSeconds(4) end dofile("Singe/Framework.singe") ---- Ejecútalo igual que antes, terminando en `movie/steps.singe`. Hace las mismas cuatro cosas. Ahora lee `singeMain`. Los cuatro pasos son los cuatro párrafos, en orden, de arriba abajo, y no hay ninguna variable `step` por ningún lado, porque la posición en la historia *es* la posición en la función. De eso trata toda esta lección. [#_what_just_happened_11] === Qué acaba de pasar [source,lua] ---- function singeMain() ---- `singeMain` es un nombre que Singe busca, como `onOverlayUpdate`. Si tu script la define, el framework ejecuta tu juego desde ella: se llama una vez, al principio, y tu juego dura lo que dure. Cuando vuelve, el juego se cierra. [source,lua] ---- singeYield() ---- Esta es la llamada que lo hace posible, y significa «deja que pase un fotograma». Singe no está haciendo dos cosas a la vez -- aquí nada se ejecuta en paralelo, por mucho que lo sugiera el nombre de modelo *con hilo* («threaded»). Lo que pasa es que `singeYield` detiene tu función donde está, le devuelve el control al motor y recuerda el sitio exacto. El motor dibuja el fotograma, lee la entrada, hace avanzar el vídeo y luego vuelve a poner en marcha tu función desde la línea que sigue al `singeYield`, con todas las variables exactamente como las dejaste. Así que un bucle `while` con un `singeYield` dentro es una forma de esperar. Es la única forma de esperar, y tienes que poner uno en cada bucle que esté esperando algo. Es un trato que haces con el motor: puedes sentarte a esperar, siempre que le devuelvas el fotograma sesenta veces por segundo. [source,lua] ---- local function playTo(frame) while discGetFrame() < frame do drawFrame() singeYield() end end ---- Singe no tiene una función de «espera hasta que el vídeo llegue al fotograma 120». No le hace falta, porque puedes escribirla en cuatro líneas, y acabas de hacerlo. Sigue mientras al disco le falte para llegar al fotograma que quieres; devuelve un fotograma en cada vuelta. `waitForButton` tiene la misma forma con otra pregunta, y `waitSeconds` es otra vez la misma forma con un reloj. Tres funciones pequeñas, escritas una vez, y gracias a ellas `singeMain` se lee como las frases en español del principio de la lección. Esto es exactamente para lo que, según la lección cinco, servían tus propias funciones. [source,lua] ---- drawFrame() singeYield() ---- Cada bucle de espera dibuja antes de ceder el fotograma, y esta es la parte que sorprende a la gente. En el modelo con hilo no hay un `onOverlayUpdate` tuyo, así que no se dibuja nada a menos que lo dibujes tú. Lo que dibujes antes de un `singeYield` es lo que aparece en ese fotograma. Deja de dibujar y el overlay deja de cambiar. Por eso existe `message`. `singeMain` le da valor, `drawFrame` lo imprime, y los tres bucles de espera llaman a `drawFrame`. Es un precio pequeño por recuperar el orden de la historia. [source,lua] ---- dofile("Singe/Framework.singe") ---- Al *final*. Esto importa más que cualquier otra cosa de la lección, así que vale la pena saber por qué. `dofile` ejecuta otro script dentro del tuyo, y una de las cosas que hace el framework mientras se ejecuta es buscar una función llamada `singeMain`. Si la encuentra, envuelve tu juego alrededor de ella y escribe un `onOverlayUpdate` propio, cuyo único trabajo es volver a poner en marcha tu `singeMain` una vez por fotograma. Si no la encuentra, no hace nada de eso, y tu juego es un juego normal dirigido por eventos. Así que el framework tiene que cargarse *después* de que exista `singeMain`. Cárgalo arriba, como en todas las lecciones desde la séptima, y en el momento en que mira todavía no hay ninguna `singeMain`, y te queda un juego sin juego dentro. [#_the_rules] === Las reglas Hay cuatro, y entre todas cubren cada error que puedes cometer aquí. *El framework va al final.* Por la razón de arriba. *No escribas tu propio `onOverlayUpdate`.* El framework ya ha escrito uno, y solo puede haber uno: gana el que se escriba el último, y el otro se descarta sin decir nada. Encima del `dofile`, el tuyo desaparece y lo que dibujas no aparece nunca. Debajo, desaparece el del framework y tu `singeMain` no vuelve a ejecutarse. En lugar de eso, dibuja desde dentro de `singeMain`. *Cada bucle que espera necesita un `singeYield`.* Un bucle sin él no devuelve nunca el fotograma. La ventana deja de redibujarse, deja de responder, y el juego se ha perdido; para salir tienes que cerrar la terminal. No hay mensaje de error, porque desde el punto de vista del motor tu función todavía no ha terminado. *Volver de `singeMain` cierra el juego.* Lo cual es muy limpio cuando es lo que quieres y una sorpresa cuando no. Un juego que tiene que seguir para siempre acaba con un bucle que nunca termina: [source,lua] ---- while true do drawFrame() singeYield() end ---- [#_input_still_arrives] === La entrada sigue llegando Mira otra vez dónde se cambia `pressed`: [source,lua] ---- function onInputPressed(what) if what == SWITCH_BUTTON1 then pressed = true end end ---- Eso es un callback, en un script escrito como una lista de pasos. Los dos modelos funcionan a la vez, y así tiene que ser. Cuando existe `singeMain`, solo se te quita `onOverlayUpdate`. Todo lo demás -- `onInputPressed`, `onInputReleased`, `onKeyPressed`, `onSoundCompleted`, `onShutdown`, todos ellos -- se dispara exactamente igual que siempre, en los huecos entre tus cesiones. Así que la forma habitual de escribir un juego en este modelo es la que acabas de usar: la historia en `singeMain`, y un callback pequeño que atrapa un evento y deja una nota para que la historia la encuentre. `waitForButton` pone `pressed` a `false` antes de empezar a esperar, y así tira todo lo que el jugador haya aporreado antes. Quita esa línea y un botón pulsado durante el vídeo cuenta como la respuesta a una pregunta que todavía no se ha hecho. [#_which_one_to_use] === Cuál usar Ningún modelo es mejor que el otro. Sirven para trabajos distintos, y un juego de verdad usa el que encaje. El modelo con hilo va bien con todo lo que sea una *secuencia*. Reproduce esto, luego pregunta, luego reproduce aquello. Una introducción. Una cinemática. Un tutorial. Un combate contra un jefe con tres fases. Cualquier cosa que describirías de forma natural como una lista numerada es una `singeMain` esperando a que la escriban, y el vídeo es lo más secuencial que existe, y por eso la tercera parte llega a este modelo aquí. El modelo dirigido por eventos va bien con todo lo que sea una *situación*. Diez enemigos y un jugador, todos moviéndose, todos chocando, ninguno en ningún orden. El juego que escribiste en la lección siete no tiene ninguna secuencia: cada fotograma es la misma pregunta hecha otra vez, que es exactamente la forma de `onOverlayUpdate`. Escríbelo como una lista de pasos y no ganas nada y pierdes la claridad. Cuando un juego es las dos cosas -- y la mayoría lo son -- se escribe de las dos formas. `singeMain` recorre los niveles en orden, y dentro de cada nivel un bucle hace el trabajo de la lección siete sesenta veces por segundo hasta que el nivel termina. [#_try_it_15] === Pruébalo . *Añade un paso.* Después de `waitForButton`, haz que la película salte hacia atrás en lugar de seguir: `discSkipToFrame(0)`, luego `playTo(60)`, y después sigue como antes. Dos líneas nuevas en un solo sitio, y nada más del script tiene que enterarse. . *Añade el mismo paso al otro.* Ahora hazlo en `events.singe`. Cuenta cuántos sitios has tenido que tocar. . *Escribe `waitFrames`.* Una cuarta función auxiliar, junto a las otras tres, que espere un número dado de fotogramas de vídeo en lugar de un número dado de segundos. Usa `discGetFrame` y recuerda dónde empezaste. . *Quita una cesión.* Borra el `singeYield()` de dentro de `playTo` y ejecútalo. Prepárate para cerrar la terminal, porque nada más lo detiene. Ahora vuelve a ponerlo y borra `drawFrame()` en su lugar, y averigua, por lo que ves, por qué el contador de fotogramas se para pero la película no. . *Haz un bucle.* Mete todo `singeMain` dentro de `while true do ... end` para que la secuencia vuelva a empezar en lugar de terminar. Esc te sigue sacando, porque eso nunca ha sido trabajo tuyo. [#_break_it_on_purpose_14] === Rómpelo a propósito Mueve la línea `dofile("Singe/Framework.singe")` del final de `steps.singe` al principio, y ejecútalo. No hay ningún error. No se imprime nada. Se abre la ventana, el primer fotograma de la película se queda ahí, y eso es todo lo que pasa, para siempre. Este es el peor tipo de fallo, y la razón por la que vale la pena conocerlo a propósito. Desde el punto de vista del motor no ha ido nada mal: tu script se ha cargado, el framework se ha cargado, y ninguno de los dos ha definido un `onOverlayUpdate`, así que el motor no tiene nada a lo que llamar. El disco está aparcado en el fotograma uno, en pausa, exactamente como lo dejó el motor, porque el `discPlay` que lo habría puesto en marcha está dentro de una `singeMain` que nadie ejecuta nunca. Cuando un juego con hilo no hace nada de nada, mira primero la última línea del archivo. Casi siempre es eso. [#_what_you_learned_15] === Lo que has aprendido * Singe tiene dos formas de escribir un script, y eliges una definiendo `singeMain` o no. * En el modelo con hilo tu juego es una lista de pasos y se ejecuta de arriba abajo. * `singeYield()` le devuelve un fotograma al motor y sigue desde la línea siguiente. * Esperar es un bucle `while` con un `singeYield` dentro. La función de espera que necesites la escribes tú; el motor no trae ninguna. * No se dibuja nada a menos que lo dibujes tú, antes de ceder, en cada fotograma. * `dofile("Singe/Framework.singe")` va al *final* de un script con hilo. * No escribas tu propio `onOverlayUpdate` cuando existe `singeMain`. * Todos los demás callbacks se siguen disparando, así que los eventos y los pasos se mezclan sin problema. * Volver de `singeMain` termina el juego. * Las secuencias piden el modelo con hilo; las situaciones, el dirigido por eventos. [#_next_time_15] === La próxima vez Ya puedes escribir «reproduce esto, luego pregunta, luego reproduce aquello» en tres líneas. Un juego de vídeo con ramificaciones no es más que esa frase, unos cuantos cientos de veces, con las respuestas decidiendo qué línea viene después. La lección dieciséis construye uno, y resulta que lo interesante no es el vídeo en absoluto. Es guardar el mapa de qué lleva adónde de una forma que todavía puedas leer cuando el juego tenga cuarenta escenas.