431 lines
15 KiB
Text
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.
|