singe/docs/es/lessons/14-video.adoc

502 lines
20 KiB
Text

[#_lesson_14_playing_video]
== Lección 14: Reproducir vídeo
image::learn/14-video.png[La lección terminada, 480]
Singe puede reproducir una película y ejecutar tu juego dentro de ella. La
imagen se convierte en el mundo, y el overlay en el que llevas dibujando desde
la lección uno se coloca encima. En esta lección pondrás en marcha un vídeo,
averiguarás qué fotograma se está mostrando, lo pararás y saltarás a cualquier
momento que quieras.
Antes de nada de eso, hay algo que conviene decir claramente, porque decide si
deberías leer siquiera esta parte del libro.
[#_a_game_plays_video_only_if_it_asks_to]
=== Un juego solo reproduce vídeo si lo pide
Singe es un motor de juegos. Todo lo que has construido en las partes primera y
segunda -- los sprites, el sonido, las colisiones, la puntuación, la carpeta del
juego -- es Singe entero, y nada de ello tiene que ver con el vídeo. Lo que
añade la tercera parte es un extra: un juego puede pedir que haya una película
reproduciéndose detrás, y entonces la película es el mundo en lugar de un lienzo
negro.
Nada más cambia. A `onOverlayUpdate` se le sigue llamando sesenta veces por
segundo. `overlayPrint` sigue contando en celdas de carácter. Tus sprites se
dibujan donde se dibujaban antes. Un juego con vídeo y un juego sin él se
escriben igual, con las mismas funciones, y comparten el mismo libro.
Así que, si el juego que quieres hacer no tiene vídeo, puedes cerrar esta parte
ahora e ir a la cuarta. Nada de la cuarta parte depende de nada de la tercera, y
las únicas lecciones que necesitan esta son las cuatro que vienen detrás.
¿Sigues aquí? Bien.
[#_you_already_have_a_video]
=== Ya tienes un vídeo
No necesitas buscar uno, ni comprarlo, ni hacerlo. La primera vez que ejecutaste
Singe en la lección uno, apareció una carpeta llamada `Singe` en tu carpeta de
trabajo. Ábrela y mira: entre los archivos que desempaquetó el motor está
`menuBackground.mkv`. Es el fondo que usa el propio menú del motor, mide 720 por
480 píxeles y dura 420 fotogramas -- unos catorce segundos.
Ese es el vídeo de esta lección y de las dos siguientes.
Crea una carpeta llamada `movie` en tu carpeta de trabajo, junto a las carpetas
de las otras lecciones, y dentro un archivo llamado `movie.singe`. No hay que
preparar nada más. Ejecutas Singe desde la carpeta de trabajo, como llevas
haciendo desde la lección uno, así que la carpeta `Singe`, vídeo incluido, está
justo ahí al lado de tu juego, y `Singe/menuBackground.mkv` es el nombre que le
darás al motor dentro de un momento. No hace falta copiar nada.
[#_type_this_in_2]
=== Escribe esto
[source,lua]
----
dofile("Singe/Framework.singe")
function onOverlayUpdate()
overlayClear()
overlayPrint(2, 2, "Frame " .. discGetFrame())
return OVERLAY_UPDATED
end
discPlay()
----
Guárdalo.
[#_run_it_2]
=== Ejecútalo
El vídeo no forma parte de tu script, así que no lo cargas en tu script. Se lo
das al motor en la línea de comandos, con `-v`:
----
Singe -R -v Singe/menuBackground.mkv movie
----
`-v` es la forma corta de `--framefile`, y nombra el vídeo que tiene que
reproducir el juego. El nombre de tu script va al final, como siempre. `-R` es
el mismo `-R` que llevas usando desde la lección uno, y sigue reiniciando el
juego cuando guardas.
Pedirlo es todo el sentido de `-v`. Si dejas un archivo de vídeo junto a tu
script y no dices nada, el motor lo ve, lo dice en la consola y ejecuta tu juego
sin él. Un juego solo es un juego de vídeo cuando alguien dice que lo es.
Se abre una ventana. La llena una bola de fuego naranja, un dragón se forma a
partir del fuego, la palabra `SINGE` aparece debajo, y luego una cuadrícula
magenta se aleja rodando bajo un sol naranja. En la esquina superior izquierda,
por encima de todo, hay un número que va subiendo.
Míralo hasta el final. Luego mira qué pasa al final.
[#_what_just_happened_10]
=== Qué acaba de pasar
[source,lua]
----
discPlay()
----
Singe llama al vídeo *el disco*, y todas las funciones que lo manejan empiezan
por `disc`. El nombre es histórico: el motor nació reproduciendo juegos que
guardaban su película en un laserdisc, y el nombre se quedó pegado a la idea. Es
un archivo de vídeo en tu disco duro y nada más.
Un juego tiene como mucho un disco, así que ninguna de las funciones `disc`
recibe un argumento que diga cuál. Solo hay uno.
Esta línea no está dentro de ninguna función. Está al final del archivo, sola, y
se ejecuta una vez cuando Singe carga tu script -- antes del primer
`onOverlayUpdate`, antes que cualquier otra cosa. Singe no tiene un callback
`onStartup` porque no lo necesita: un script de Lua *es* una lista de
instrucciones, y todo lo que escribas fuera de una función ocurre mientras se
lee el archivo. Por convención, el código de arranque va al final, debajo de las
funciones.
Necesitas `discPlay()` porque el disco no arranca solo. Cuando empieza el juego,
el motor aparca el disco en el fotograma uno y lo pausa ahí. Nada se mueve hasta
que lo pides.
[source,lua]
----
overlayPrint(2, 2, "Frame " .. discGetFrame())
----
`discGetFrame` responde a una pregunta: ¿qué fotograma se está mostrando ahora
mismo? No recibe argumentos y devuelve un número.
Fíjate en dónde se dibuja ese número. Columna 2, fila 2 -- exactamente donde fue
`Hello!` en la lección uno, y cae en el mismo sitio de la pantalla. El overlay
en el que dibujas mide la mitad que el vídeo en cada lado, lo que para un vídeo
de 720 por 480 son 360 por 240, que es exactamente el tamaño sobre el que
llevas dibujando trece lecciones. Tus coordenadas no se han movido. Si
alguna vez reproduces un vídeo de otro tamaño, el overlay cambia de tamaño con
él, y `overlaySetResolution` lo pone al tamaño que quieras; su entrada en la
referencia tiene los detalles.
Esta es toda la idea del overlay. El vídeo está debajo. Tu dibujo está encima, y
allí donde no has dibujado se ve la película. Por eso `overlayClear` importa
ahora todavía más que antes: limpia el overlay y estarás viendo la película.
[#_a_frame_is_an_address]
=== Un fotograma es una dirección
El número que devuelve `discGetFrame` es un número de *fotograma*. Un fotograma
es una imagen fija. Este vídeo tiene 420 y muestra unos treinta cada segundo,
así que dura unos catorce segundos.
Los fotogramas se cuentan desde cero, así que este vídeo tiene los fotogramas
del 0 al 419. La cantidad y el último número no son lo mismo, y eso pilla a todo
el mundo una vez. `discGetFrameCount` te dice la cantidad -- 420 -- y el
fotograma más alto que puedes pedir es uno menos.
Lo importante de un número de fotograma es que nunca cambia y nunca se desvía.
El fotograma 110 es el momento en que el dragón echa fuego, en esta copia del
archivo, en este ordenador, hoy y el año que viene. Los segundos se desvían,
porque un vídeo no siempre se reproduce exactamente a la velocidad que crees. Un
número de fotograma es la dirección de un momento, y los juegos de vídeo se
construyen con direcciones: apuntas el fotograma en el que aparece el dragón, y
a partir de ahí puedes mandar la película allí cuando quieras.
Encontrar esos números es un trabajo que haces una vez por vídeo, y el script
que has escrito es la herramienta para hacerlo: pon la película, mira el
contador, apunta los fotogramas en los que pasan cosas. Todo lo de la lección
dieciséis se construye sobre una lista de números encontrados exactamente así.
[#_what_happened_at_the_end]
=== Qué pasó al final
El contador llegó a 419, y luego volvió a ser 0 y el fuego regresó.
Cuando el disco se sale del final, el motor lo vuelve a empezar desde el
fotograma cero. No se para, y no te lo dice. Un juego de verdad nunca deja que
llegue ahí: vigila el número de fotograma y manda el disco a otro sitio antes de
que llegue el final. Olvidarse de hacerlo es la razón de que un juego de vídeo a
medio hacer reproduzca de repente su propia secuencia de título en mitad de una
pelea.
[#_stopping_jumping_and_asking]
=== Parar, saltar y preguntar
Añade esta función encima de `onOverlayUpdate`:
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_BUTTON1 then
if discGetState() == DISC_PLAYING then
discPause()
else
discPlay()
end
elseif what == SWITCH_LEFT then
discSearch(0)
elseif what == SWITCH_RIGHT then
discSkipForward(30)
elseif what == SWITCH_START1 then
discStop()
end
end
----
Guarda y prueba cada una. La barra espaciadora reproduce y pausa. La izquierda
vuelve al principio. La derecha salta hacia delante más o menos un segundo. La
tecla `1` para el disco del todo, y lo que obtienes es una pantalla azul -- el
color que mostraba un reproductor de laserdisc cuando estaba parado, que se ha
conservado porque muchísimos juegos dibujaban encima su tabla de puntuaciones.
Ahí hay cinco llamadas nuevas, y se dividen en dos tipos.
*Preguntar.* `discGetState` devuelve uno de cuatro nombres que el motor ya ha
definido por ti: `DISC_PLAYING`, `DISC_PAUSED`, `DISC_STOPPED` y
`DISC_EJECTED`. Son como `OVERLAY_UPDATED` -- sin comillas, porque son nombres
de valores y no texto. Compáralos con `==` como comparabas números en la lección
tres.
*Ordenar.* `discPlay` empieza o reanuda. `discPause` se congela en el fotograma
que se está mostrando y lo deja en pantalla. `discStop` es la más contundente:
la imagen se sustituye por la pantalla azul, `discGetFrame` responde `0` hasta
que el disco vuelve a ponerse en marcha, y `discPause` y las llamadas de salto
se ignoran mientras está parado. Cualquiera de `discPlay`, `discSearch` o
`discSkipToFrame` lo saca de la parada.
Dos llamadas mandan el disco a un fotograma, y la diferencia entre ellas es lo
único de esta lección que vale la pena aprenderse de memoria:
* `discSearch(frame)` va allí y se *pausa* en él.
* `discSkipToFrame(frame)` va allí y *reproduce* desde él.
Busca cuando quieras aparcar en una imagen y esperar. Salta cuando quieras que
empiece una escena. `discSkipForward(30)` y `discSkipBackward(30)` son la misma
idea medida desde donde estás en lugar de desde el principio, y dejan el disco
reproduciéndose si se estaba reproduciendo y pausado si estaba pausado.
También existe `discPauseAtFrame`, que hace exactamente lo mismo que
`discSearch` con un nombre más antiguo. La referencia lo marca como alias heredado.
Lo verás en scripts de otras personas; en los tuyos, escribe `discSearch`.
[#_waiting_is_harder_than_it_sounds]
=== Esperar es más difícil de lo que parece
Esta es la frase de la que está hecho todo juego de vídeo:
____
Reproduce del fotograma 0 al fotograma 120 y después hazle una pregunta al
jugador.
____
Intenta escribirla. No puedes escribir «reproduce, después espera, después
pregunta», porque no tienes dónde escribirlo. Tienes `onOverlayUpdate`, a la que
Singe llama sesenta veces por segundo y que tiene que volver inmediatamente cada
una de esas veces. Una función que se sentara a esperar pararía el motor en
seco: ni dibujo, ni entrada, ni vídeo.
Así que no puedes decir «después». Tienes que convertir «después» en una
variable que recuerde por dónde vas, y comprobarla en cada una de esas sesenta
llamadas:
[source,lua]
----
local asking = false
function onOverlayUpdate()
overlayClear()
if not asking and discGetFrame() >= 120 then
discPause()
asking = true
end
if asking then
overlayPrint(2, 4, "Left or right?")
end
return OVERLAY_UPDATED
end
----
Eso funciona. Léelo y verás que funciona. Pero fíjate en lo que ha costado: una
variable, cuyo único trabajo es recordar en qué parte de la frase estás, y dos
`if` para mantenerla en orden.
Ahora añade el resto de la escena. El jugador responde, así que a continuación
se reproduce una de dos cosas. Cada una de ellas termina en algún sitio y hace
otra pregunta. Un juego con ramificaciones modesto tiene cuarenta de estos
momentos, y cada uno quiere su propia variable y su propio `if`. Acabas con un
script en el que la historia no está escrita en ninguna parte -- está repartida
entre una docena de indicadores, y para averiguar qué pasa después del dragón
tienes que leerlos todos.
Quédate con esa idea. La lección quince no es más que el arreglo para eso.
[#_when_there_is_no_video]
=== Cuando no hay vídeo
Vuelve a ejecutar tu script de la forma normal, sin `-v`:
----
Singe -R movie
----
No se cuelga nada. Te sale una ventana negra con `Frame 0` en la esquina.
Vale la pena entender esto, porque es la mitad tranquilizadora de la promesa del
principio de esta lección. Un juego sin disco sigue ejecutando cada llamada
`disc` que hagas; solo que no hacen nada útil. El motor documenta exactamente
qué responden, y eso nunca cambia: `discGetFrame` y `discGetFrameCount`
devuelven `0`, `discGetState` devuelve `DISC_EJECTED`, `discGetWidth` y
`discGetHeight` devuelven el tamaño del lienzo, y `discPlay`, `discPause`,
`discSearch` y las demás se apuntan en el registro y por lo demás se ignoran.
No puedes romper un script llamando a una función de disco cuando no hay disco.
Cuando un script necesita *saberlo*, pregunta a `SINGE_DISC`. Es otro nombre que
el motor define antes de que se ejecute tu script, y vale `true` cuando el juego
tiene vídeo y `false` cuando el mundo es un lienzo sin más. Es la forma correcta
de escribir un juego que pueda funcionar de las dos maneras:
[source,lua]
----
if not SINGE_DISC then
overlayPrint(2, 2, "This game has no video.")
overlayPrint(2, 4, "Run it again with -v Singe/menuBackground.mkv")
return OVERLAY_UPDATED
end
----
`not` le da la vuelta a un valor verdadero o falso, como en la lección tres.
[#_the_whole_thing_7]
=== Todo junto
Todo junto, con una pequeña función que convierte el estado en palabras y un
fotograma en el que detenerse, este es el script terminado. Es un buscador de
fotogramas: la herramienta que usarás cada vez que te encuentres con un vídeo
nuevo.
[source,lua]
----
dofile("Singe/Framework.singe")
local stopAt = 400
local held = false
local function stateName()
local state = discGetState()
if state == DISC_PLAYING then
return "playing"
elseif state == DISC_PAUSED then
return "paused"
elseif state == DISC_STOPPED then
return "stopped"
end
return "not there"
end
function onInputPressed(what)
if what == SWITCH_BUTTON1 then
if discGetState() == DISC_PLAYING then
discPause()
else
discPlay()
end
elseif what == SWITCH_LEFT then
discSearch(0)
held = false
elseif what == SWITCH_RIGHT then
discSkipForward(30)
elseif what == SWITCH_START1 then
discStop()
held = false
end
end
function onOverlayUpdate()
local frame = discGetFrame()
overlayClear()
if not SINGE_DISC then
overlayPrint(2, 2, "This game has no video.")
overlayPrint(2, 4, "Run it again with -v Singe/menuBackground.mkv")
return OVERLAY_UPDATED
end
if not held and frame >= stopAt then
discPause()
held = true
end
overlayPrint(2, 2, "Frame " .. frame .. " of " .. discGetFrameCount())
overlayPrint(2, 4, "The disc is " .. stateName() .. ".")
overlayPrint(2, 6, "Space plays and pauses. Left rewinds.")
overlayPrint(2, 7, "Right jumps on a second. 1 stops the disc.")
if held then
overlayPrint(2, 9, "Holding at frame " .. stopAt .. ".")
end
return OVERLAY_UPDATED
end
discPlay()
----
`held` es el mismo tipo de variable que `asking` más arriba, y está aquí por una
razón que puedes comprobar: quítala, deja `if frame >= stopAt then discPause()
end`, y luego intenta pulsar la barra espaciadora para seguir más allá del
fotograma 400. No puedes. El disco se reanuda durante una sexagésima de segundo
y el siguiente `onOverlayUpdate` lo vuelve a pausar, para siempre. `held` es la
forma de decir «esto ya lo he hecho una vez».
[#_try_it_14]
=== Pruébalo
. *Encuentra el dragón.* Ejecútalo, mira el contador y apunta el fotograma en el
que aparece el dragón por primera vez, el fotograma en el que echa fuego y el
fotograma en el que la cuadrícula se adueña de la pantalla. Querrás tener
estos tres números en la lección dieciséis.
. *Avanza paso a paso.* Cambia `discSkipForward(30)` por `discSkipForward(1)` y
mantén pulsada la flecha derecha. Ahora cambia `stopAt` a un fotograma en
mitad del dragón y usa `discSearch` y la tecla de flecha para caer exactamente
en él.
. *Haz que se repita.* Cambia la parada para que, en lugar de pausar en el
fotograma 400, llame a `discSkipToFrame(0)`. Has construido un modo de
atracción: una escena que se reproduce sin fin hasta que alguien pulsa un
botón.
. *Pide un fotograma que no existe.* Cambia la línea de `SWITCH_RIGHT` por
`discSkipToFrame(100000)`. No se cuelga. Lee la entrada de `discSkipToFrame`
en la referencia y busca la frase que dice por qué.
. *Juégalo sin el vídeo.* Ejecuta `Singe -R movie` y luego pulsa todas las
teclas. Nada de lo que pulses puede romperlo.
[#_break_it_on_purpose_13]
=== Rómpelo a propósito
El error que más tiempo cuesta aquí no es un error de Lua. Ejecuta esto:
----
Singe -R -v menuBackground.mkv movie
----
La ruta está mal -- se cuenta desde la carpeta de trabajo, y el vídeo está ahí
en la carpeta `Singe`, no suelto -- y Singe no arranca. Lo que hace en su lugar es imprimir su lista entera de
opciones de línea de comandos, lo que asusta la primera vez, y luego esto como
última línea:
----
Error: Unable to locate video.
----
Un muro de opciones así siempre significa lo mismo: el motor no llegó ni a tu
juego. Ignora el muro y lee la última línea.
Ese es fácil. La versión traicionera es ejecutarlo sin `-v` y olvidar que lo has
hecho. No hay ningún error, porque un juego sin vídeo es un juego perfectamente
bueno. Te sale una pantalla negra y un contador clavado en `0`, y te pasas
veinte minutos buscando el fallo en tu script.
Para eso está el bloque de `SINGE_DISC` del script terminado. Tres líneas, y el
juego te dice qué pasa en lugar de enfurruñarse. Pon una comprobación así en
cada script que escribas que necesite un vídeo.
[#_what_you_learned_14]
=== Lo que has aprendido
* Un juego solo reproduce vídeo cuando lo pide, y todo lo demás de Singe
funciona igual en los dos casos.
* Singe llama al vídeo el disco. Un juego tiene uno, y las funciones `disc` no
reciben ningún handle porque no hay nada entre lo que elegir.
* El vídeo va en la línea de comandos con `-v`, no en el script.
* El overlay se dibuja sobre la imagen, y la película se ve allí donde no has
dibujado.
* Un fotograma es una imagen fija, se cuentan desde cero, y un número de
fotograma es la dirección de un momento.
* `discGetFrame` pregunta dónde está el disco; `discGetFrameCount` dice cuántos
fotogramas hay; `discGetState` dice qué está haciendo el disco.
* `discPlay`, `discPause` y `discStop` lo manejan. `discSearch` va a un
fotograma y pausa; `discSkipToFrame` va a un fotograma y reproduce.
* Un disco que se sale del final vuelve a empezar en el fotograma cero sin
decírtelo.
* Sin disco, toda llamada `disc` es inofensiva, y `SINGE_DISC` es como un script
sabe en qué tipo de juego está.
* No puedes esperar dentro de `onOverlayUpdate`, así que el «después» hay que
escribirlo como una variable.
[#_next_time_14]
=== La próxima vez
Ese último punto es el que duele. Un juego de vídeo es una lista de pasos --
reproduce esto, pregunta aquello, reproduce lo otro -- y acabas de ver que el
modelo de callbacks no puede decir «después» sin un indicador y un `if` para
cada paso. La lección quince te enseña la otra forma que te deja Singe de
escribir un script, en la que «después» es una línea nueva.