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