singe/docs/es/lessons/15-threaded.adoc

431 lines
15 KiB
Text

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