[#_lesson_21_subtitles] == Lección 21: Subtítulos image::learn/21-subtitles.png[La lección terminada, 480] Alguien que no puede oír va a jugar a tu juego. Si tu juego tiene voces y no tiene subtítulos, no podrá. No es que «le cueste más» -- llegará a la primera escena en la que un personaje dice lo que le explica al jugador qué tiene que hacer, y ahí se quedará. Eso es todo, y es la razón de que esta lección exista en lugar de ser un párrafo de la lección diecinueve. La otra razón es práctica. Meterle subtítulos a un juego ya terminado es un suplicio. Tienes que encontrar cada frase hablada, averiguar en qué fotograma está y descubrir que la mitad de los tiempos están mal porque tu vídeo se volvió a codificar a mitad del proyecto. Si lo haces mientras escribes el juego, es cosa de una tarde. Singe te da un sistema que hace la parte difícil, y esta lección explica cómo usarlo. [#_it_is_not_ordinary_text_drawing] === No es dibujar texto sin más Podrías hacer los subtítulos tú mismo. Guardas una lista de frases con sus tiempos de inicio y de fin, miras el reloj en `onOverlayUpdate` y dibujas la actual con `fontToSprite`. Hay quien lo ha hecho. Esta es la razón por la que tú no deberías. Los subtítulos se sincronizan con *el disco*, no con un reloj. Cuando `srtLoad` lee tu archivo, convierte cada marca de tiempo en un número de fotograma del disco, en ese mismo momento, usando la tasa de fotogramas del disco que esté cargado. A partir de entonces el motor no mira ningún reloj; en cada fotograma dibujado pregunta qué entrada cubre el fotograma en el que está el disco. Esa única decisión te da lo que un sistema hecho a mano nunca consigue hacer bien: *un subtítulo sobrevive a un salto*. Manda el disco a cualquier sitio -- `discSearch`, `discSkipForward`, un paso, una ramificación a otro segmento -- y la frase correcta está en pantalla en el siguiente fotograma dibujado, hacia delante o hacia atrás, sin recargar nada y sin poner a cero ningún reloj. En un juego con ramificaciones como el de la lección dieciséis, donde las decisiones del jugador hacen saltar el disco de un lado a otro sin parar, esa es la diferencia entre funcionar y casi funcionar. Además se dibuja en el sitio correcto. El documento de los subtítulos está sobre la imagen, por encima de tu overlay y por debajo de cualquier GUI en espacio de pantalla, y sigue a la imagen allí donde la pongan los ajustes del jugador -- una imagen encogida, una desplazada, una recreativa girada, un hueco en un bezel. No acepta entrada del ratón, así que un disparo de pistola de luz lo atraviesa sin más. Hacer todo eso bien por tu cuenta es una semana que no tenías prevista. [#_write_the_file] === Escribe el archivo Los subtítulos van en un archivo SubRip, que termina en `.srt` y es texto plano que puedes escribir en cualquier editor. Es uno de los formatos más sencillos que existen. Crea un archivo llamado `subtitles.srt` junto a tu script: ---- 1 00:00:01,000 --> 00:00:04,000 They told us the cave was empty. 2 00:00:04,200 --> 00:00:08,000 They were wrong about a great many things. 3 00:00:08,500 --> 00:00:13,000 Keep your light on the floor. ---- Una entrada son cuatro cosas. Un número en una línea propia. Una línea con dos marcas de tiempo y, entre ellas, una flecha hecha de dos guiones y un signo de mayor que. Una o más líneas de texto. Una línea en blanco para terminar. Una marca de tiempo es `HH:MM:SS,mmm` -- horas, minutos y segundos separados por dos puntos, luego una coma y luego los milisegundos. La coma no es una errata, y un punto no sirve. `00:00:04,200` son cuatro segundos y un quinto desde el principio. Los números de las entradas se leen y se tiran, así que no tienen por qué estar en orden, ni siquiera ser correctos. Numéralas de todos modos; en algún momento estarás buscando la entrada cuarenta y uno. La entrada dos muestra cómo es una entrada de dos líneas. Las líneas se muestran tal como las escribiste, así que eres *tú* quien decide dónde se parte una línea larga, lo cual importa más de lo que parece y se explica más abajo. El analizador es tolerante a propósito con las cosas que salen mal en un archivo editado a mano. Los finales de línea de Windows se cargan sin cambios, y los espacios del principio y del final se quitan de cada línea. Una entrada cuyas marcas de tiempo no se pueden analizar se salta, y el resto del archivo se sigue cargando, lo cual es muy amable justo hasta que te preguntas dónde ha ido a parar la línea doce. Algunas cosas no están soportadas: las etiquetas `` y `` que escriben algunas herramientas se muestran como texto en lugar de obedecerse, y las coordenadas de posición que algunas herramientas añaden a la línea de tiempo se ignoran. La sección de subtítulos de la referencia tiene la lista completa. [#_load_it_and_switch_it_on] === Cárgalo y actívalo Tres llamadas, y el orden importa. [source,lua] ---- if srtLoad(DIR .. "subtitles.srt") then srtPosition(80) srtEnable(true) end ---- `srtLoad` lee el archivo y responde `true` o `false`. Responde `false`, sin haber cargado nada, en tres casos: el archivo no se puede leer, no hay nada en él que se pueda analizar o *no hay disco*. En los tres imprime en la consola un aviso que nombra el archivo. Es un buen diseño y deberías aprovecharlo: un archivo de subtítulos que falta no merece que el juego muera por él, pero publicar un juego sin subtítulos sin enterarte sí merece que lo notes. El caso de «no hay disco» es el que te va a pillar. Las marcas de tiempo tienen que convertirse en números de fotograma a la tasa de fotogramas del disco, y mientras no hay un disco abierto no hay tasa de fotogramas que usar. Así que carga los subtítulos *después* del disco, lo que en un script normal significa desde el cuerpo del script, donde se ejecuta todo lo demás que has escrito fuera de las funciones. De ahí se sigue que un `.srt` preparado para un vídeo a una tasa de fotogramas cae en el sitio equivocado en un vídeo a otra. Sincroniza tu archivo con el vídeo que de verdad vas a publicar. `srtEnable(true)` activa las entradas cargadas. Un script empieza con los subtítulos desactivados, siempre, así que un juego que los carga tiene que activarlos. Desactivarlos otra vez quita lo que se esté mostrando, y volver a activarlos pone lo que cubra el fotograma en el que está el disco ahora mismo -- sin buscar nada, sin estado que guardar. Tener los subtítulos activados es un ajuste, no una decisión que tomas tú por el jugador. Ponlo en tu página de opciones de la lección veinte, guárdalo con `saveSet` de la lección doce y pásale a `srtEnable` lo último que haya elegido el jugador. [#_where_they_sit] === Dónde se colocan [source,lua] ---- srtPosition(70) ---- `srtPosition` es un porcentaje hacia abajo de la imagen, de `1` a `95`, y por defecto es `80`. Se aplica a la entrada que está en pantalla ahora mismo y a todas las que vienen después. Un número fuera del rango se ignora en lugar de recortarse al límite, lo que significa que una errata deja la posición exactamente donde estaba y no dice nada, así que revisa tus números. El ochenta por ciento queda bajo, y queda bajo a propósito: la parte de abajo de la imagen suele ser la menos interesante del plano. Súbelos cuando la parte de abajo *no* sea aburrida -- cuando tu juego dibuja un marcador a lo largo del borde inferior, o cuando el punto de mira de la pistola de luz del jugador vive ahí abajo. [#_taking_one_down] === Quitar uno [source,lua] ---- srtClear() ---- `srtClear` quita de golpe lo que haya en pantalla, y deja en paz las entradas cargadas y el estado de activación. La línea que se estaba mostrando sigue quitada hasta que el disco llega a la siguiente entrada. Úsalo cuando una escena termina antes de tiempo y no quieres que la última frase del diálogo se quede colgando sobre el plano siguiente. Hay una llamada más que comparte la maquinaria de los subtítulos, y puede que ya la hayas usado: [source,lua] ---- overlayBanner("Extra life!", 20) ---- `overlayBanner` muestra un mensaje corto sobre la imagen durante unos treinta fotogramas dibujados y luego lo quita. Lo dibuja el mismo documento que los subtítulos, así que un rótulo parece un subtítulo, y mientras está en pantalla ocupa el lugar de cualquier subtítulo, que vuelve cuando se le acaba el tiempo al rótulo. Su propio argumento de altura es independiente de `srtPosition` y por defecto vale `47`, el centro de la imagen. Los mensajes de más de sesenta caracteres se ignoran por completo, así que uno largo parece no funcionar en absoluto. [#_subtitles_already_inside_the_video] === Subtítulos que ya están dentro del vídeo Algunos archivos de vídeo llevan sus propias pistas de subtítulos. Si el tuyo las lleva, no necesitas ningún `.srt`: [source,lua] ---- if discGetSubtitleTracks() > 0 then srtLoadTrack(0) else srtLoad(DIR .. "subtitles.srt") end srtEnable(true) ---- `srtLoadTrack` recibe un número de pista de `0` a `discGetSubtitleTracks() - 1` y todo lo que viene después de cargar es idéntico. `discGetSubtitleLanguage(track)` te dice con qué idioma está etiquetada una pista, como un código de tres letras del tipo `eng` o `fra`, así que un juego puede ofrecerle al jugador lo que su vídeo resulte llevar. Dos advertencias. Leer una pista recorre una vez todo el archivo de vídeo, sin decodificar ninguna imagen, así que un disco tan largo como una película tarda un momento: hazlo mientras hay otra cosa en pantalla, no entre dos fotogramas de juego. Y las pistas de subtítulos que guardan *imágenes* de las palabras en lugar de las palabras -- las que usan los DVD y los Blu-ray -- no se pueden leer. Se cuentan, para que la numeración coincida con la que muestran otros reproductores, pero `srtLoadTrack` responde `false` para ellas. [#_the_whole_script_3] === El script completo Este necesita un vídeo. Ya tienes uno: `Singe/menuBackground.mkv`, que el motor desempaquetó en tu carpeta de trabajo la primera vez que ejecutaste algo. Dile a Singe que lo use como disco con `--framefile`, que además enciende el disco: ---- Singe --framefile=Singe/menuBackground.mkv subtitles ---- [source,lua] ---- dofile("Singe/Framework.singe") overlaySetResolution(discGetWidth(), discGetHeight()) local HUD_X = 16 local STEP = 5 local TOP_LIMIT = 10 local LOW_LIMIT = 95 local hudFont = fontLoad("Singe/FreeSansBold.ttf", 18) local loaded = false local showing = false local height = 80 if discGetSubtitleTracks() > 0 then loaded = srtLoadTrack(0) end if not loaded then loaded = srtLoad(DIR .. "subtitles.srt") end if loaded then srtPosition(height) showing = true srtEnable(true) end discPlay() function onInputPressed(what) if not loaded then return end if what == SWITCH_BUTTON1 then showing = not showing srtEnable(showing) if showing then overlayBanner("Subtitles on", 20) else overlayBanner("Subtitles off", 20) end elseif what == SWITCH_UP then height = math.max(TOP_LIMIT, height - STEP) srtPosition(height) elseif what == SWITCH_DOWN then height = math.min(LOW_LIMIT, height + STEP) srtPosition(height) elseif what == SWITCH_LEFT then srtClear() discSearch(0) discPlay() end end function onOverlayUpdate() overlayClear() fontSelect(hudFont) colorForeground(255, 255, 255) if loaded then if showing then fontPrint(HUD_X, 16, "Frame " .. discGetFrame() .. " subtitles ON at " .. height .. "%") else fontPrint(HUD_X, 16, "Frame " .. discGetFrame() .. " subtitles OFF") end fontPrint(HUD_X, 40, "Fire toggles them, up and down move them, left rewinds") else fontPrint(HUD_X, 16, "No subtitles loaded.") fontPrint(HUD_X, 40, "Run with --framefile and check subtitles.srt is beside the script.") end return OVERLAY_UPDATED end function onShutdown() fontUnload(hudFont) end ---- Pulsa el disparo para activarlos y desactivarlos, arriba y abajo para mover las líneas, e izquierda para rebobinar hasta el principio. El rebobinado es el que hay que mirar: el disco salta atrás al fotograma cero y la entrada correcta está en pantalla justo en el fotograma siguiente, sin que tú hagas nada al respecto. [#_what_just_happened_17] === Qué acaba de pasar [source,lua] ---- if discGetSubtitleTracks() > 0 then loaded = srtLoadTrack(0) end if not loaded then loaded = srtLoad(DIR .. "subtitles.srt") end ---- Usa los subtítulos del propio vídeo cuando los tiene, recurre al archivo que hay junto al script cuando no, y termina sabiendo cuál ha funcionado -- si es que ha funcionado alguno. Las dos llamadas responden con un booleano, así que el plan de reserva son dos sentencias `if` y ningún caso especial. `menuBackground.mkv` no lleva ninguna pista de subtítulos, así que lo que vas a obtener de verdad es el archivo que escribiste. [source,lua] ---- function onInputPressed(what) if not loaded then return end ---- `return` sin nada detrás sale de la función inmediatamente. Sin subtítulos cargados, ninguna de estas teclas tiene nada que hacer, y salir pronto es más claro que envolver todo el cuerpo en un `if`. Llamar a `srtEnable` sin nada cargado no hace daño, pero escribir código que solo tiene sentido cuando no hace nada es una costumbre que más vale no coger. [source,lua] ---- elseif what == SWITCH_LEFT then srtClear() discSearch(0) discPlay() ---- El `srtClear` no es necesario. El motor pondrá la entrada correcta para el fotograma cero en el siguiente fotograma dibujado de todos modos. Está ahí para que puedas borrarlo y mirar qué pasa: la línea vieja se queda en pantalla una fracción de segundo mientras el disco llega a donde va. En un salto lento ese destello de la línea equivocada se ve, y limpiar primero es como lo evitas. [#_making_text_readable_over_a_moving_picture] === Texto legible sobre una imagen en movimiento El sistema pone las palabras en pantalla. Que alguien pueda leerlas es problema tuyo, y vale para cada línea de texto que dibujes sobre vídeo -- una indicación de la lección dieciocho, una puntuación, una elección de rama -- no solo para los subtítulos. *No te acerques a los bordes.* Los televisores llevan setenta años recortando los bordes de la imagen, y los monitores de las recreativas son peores, no mejores. Trabaja suponiendo que el cinco por ciento exterior de la imagen, en cada lado, puede no estar en absoluto en la pantalla de alguien. Mantén dentro de ese margen todo lo que el jugador tenga que leer. Por eso `srtPosition` se detiene en `95` y no en `100`. *El contraste no es una elección de color, es una garantía.* Un texto blanco sobre una imagen es un texto blanco sobre lo que la imagen esté haciendo en ese momento, y dentro de dos segundos eso puede ser un campo nevado. El estilo de subtítulos incluido lo resuelve como lo hacen los subtítulos de cine: un panel oscuro que se ciñe al texto, más una sombra de un píxel. Si dibujas tu propio texto sobre vídeo, haz una cosa o la otra -- un `overlayBox` detrás o una copia oscura del texto desplazada un píxel. No te fíes de la imagen. *Parte tú las líneas.* Dos líneas cortas se leen más rápido que una larga, y una línea que se parte donde tú no has elegido suele partirse por el sitio equivocado. Apunta a unos cuarenta caracteres por línea y no más de dos líneas a la vez. Parte en una pausa natural: `They were wrong about / a great many things` se lee bien; `They were wrong / about a great many things` tropieza. *Déjalas el tiempo suficiente.* Una velocidad de lectura cómoda anda por los quince caracteres por segundo, así que una línea de cuarenta caracteres pide unos tres segundos. Mira la entrada uno del archivo de arriba: treinta y dos caracteres, tres segundos. Si una frase del diálogo es corta y rápida, dejar la entrada un momento más allá del final de lo que se dice es más amable que quitarla de golpe. *Ponlos donde no está la acción.* Si lo importante del plano está en la parte de abajo del encuadre, sube los subtítulos para esa escena con `srtPosition`. Puedes llamarla tantas veces como quieras, y el jugador nunca notará que lo has hecho. [#_styling_them] === Darles estilo Las líneas las dibuja un documento, del mismo tipo que conociste en la lección veinte, y dos archivos de tu carpeta `Singe` deciden qué aspecto tienen: * `Singe/subtitle.rml` -- el documento. Tiene un solo elemento, `#slot`, en el que el motor escribe la entrada o el rótulo actual. * `Singe/subtitle.rcss` -- el estilo. `.cue` es el bloque que lleva la altura que pidió `srtPosition`, y `.cue .text` es la barra de texto que hay dentro: blanca, en negrita, sobre un panel oscuro redondeado que se ciñe al texto, con una sombra. Abre `subtitle.rcss` y léelo -- son unas cuarenta líneas. Luego déjalo en paz, porque lo que edites ahí no dura. Cada vez que Singe arranca, vuelve a poner su propia copia de todo lo que haya cambiado en la carpeta `Singe`, y lo dice: ---- >>> Updated File: Singe/subtitle.rcss ---- Esa carpeta es del motor. Tu juego trae su propia copia en su lugar. Copia los dos archivos de `Singe` a tu carpeta `subtitles`, junto al script. Luego abre tu copia de `subtitle.rml` y cambia la única línea de su `` que enlaza el estilo, para que quede así: [source,html] ---- ---- Un nombre dentro de un documento se cuenta desde la propia carpeta del documento, salvo un nombre que empieza por `Singe/`, que siempre quiere decir la carpeta del motor. El enlace tal como lo copiaste, `Singe/subtitle.rcss`, sigue trayendo el estilo del motor. El nuevo trae el tuyo. Cuando Singe encuentra un `subtitle.rml` junto a tu script, dibuja los subtítulos con ese en lugar de con el suyo. Añade `-p` al comando y Singe describe lo que va haciendo, línea a línea: ---- Singe -p --framefile=Singe/menuBackground.mkv subtitles ---- Cuando aparece la primera entrada, una de esas líneas es esta: ---- Using the game's own subtitles/subtitle.rml ---- Ahora cambia el `font-size`, el `color`, el `background-color`, el `border-radius` o el `font-effect` en tu `subtitle.rcss`, guarda y vuelve a ejecutarlo. El motor solo toca el contenido de `#slot` y la altura de `.cue`, así que todo lo demás de tus dos archivos es tuyo, y nada de lo que haga Singe al arrancar los tocará. Si `-p` dice que está usando los tuyos y el aspecto no ha cambiado, vuelve a leer el enlace: una copia que todavía dice `Singe/` está usando sin avisar el estilo del motor. Como `overlayBanner` usa el mismo documento, cambiar el estilo de los subtítulos cambia también el de tus rótulos, lo cual suele ser lo que quieres, y conviene saberlo antes de que te preguntes por qué. Un límite: el documento de los subtítulos es un documento, y los documentos necesitan una GPU. En una máquina sin ella, `srtLoad` sigue leyendo tu archivo y sigue respondiendo con la verdad, y no se dibuja nada. Un aviso lo dice una vez. [#_try_it_21] === Pruébalo . *Añade una cuarta entrada* a `subtitles.srt`, entre los segundos once y trece. Guarda y ejecuta. Luego ponla fuera de orden, como entrada `99`, la primera del archivo, y comprueba que sigue funcionando. . *Dale a una entrada una marca de tiempo mala* -- escribe `00:00:05.500` con un punto en lugar de una coma -- y ejecuta. Mira qué línea desaparece, y fíjate en que no se dice absolutamente nada al respecto. Un archivo que se carga no es un archivo que esté bien. . *Cámbiales el estilo.* En tu propio `subtitle.rcss` -- la copia que está junto a tu script, no la de `Singe` -- cambia `.cue .text` a texto amarillo sobre un fondo totalmente transparente, y luego añade una sombra más gruesa. Después reprodúcelo sobre el vídeo y decide con sinceridad si todavía se lee. . *Rompe la sincronización a propósito.* Cambia todas las marcas de tiempo del archivo para que lleguen dos segundos tarde, y mira cómo un sistema que funciona correctamente entrega unos subtítulos inútiles. Esto es lo que te hace un vídeo recodificado. . *Lleva la opción a un menú.* Coge la página de pausa de la lección veinte, añádele una casilla y conéctala a `srtEnable`. Luego guarda la elección con `saveSet` para que siga activada la próxima vez. Esta es la versión que de verdad quieren tus jugadores. [#_break_it_on_purpose_20] === Rómpelo a propósito Ejecuta el script sin la opción `--framefile`: ---- Singe subtitles ---- Se abre la ventana, negra, con tu mensaje de reserva, y la consola dice: ---- Warning: No subtitles were loaded from subtitles/subtitles.srt. ---- Ni un cierre inesperado ni una ventana de error, porque `srtLoad` ha respondido `false` y tu script lo ha gestionado. Ese es el fallo más valioso de esta lección, porque es el que ocurre sin que nadie se dé cuenta. No hay disco, así que no hay tasa de fotogramas, así que no hay nada en lo que convertir las marcas de tiempo, así que no se carga nada. Si hubieras escrito `srtLoad(...)` sin más y hubieras tirado la respuesta, tu juego habría funcionado perfectamente sin mostrar subtítulos, y te habrías enterado cuando alguien te lo dijera. Usa la respuesta. Siempre. [#_what_you_learned_21] === Lo que has aprendido * Un juego con voces y sin subtítulos es un juego al que algunas personas no pueden jugar en absoluto. * Meter los subtítulos sobre la marcha es cosa de una tarde; añadirlos al final no. * Los subtítulos son un archivo `.srt`: un número, dos marcas de tiempo con `-->` entre ellas, el texto y una línea en blanco. * `srtLoad` convierte las marcas de tiempo en números de fotograma del disco al cargar, y por eso una entrada sobrevive a cualquier salto. * Hay que cargarlo después del disco, y responde `false` cuando no puede cargar, también cuando no hay disco. Comprueba la respuesta. * `srtEnable` los activa, y un script siempre empieza con ellos desactivados. * `srtPosition` los coloca como un porcentaje hacia abajo de la imagen, de `1` a `95`. * `srtClear` quita lo que se esté mostrando, y `overlayBanner` comparte el mismo documento. * `srtLoadTrack` lee los subtítulos del propio archivo de vídeo cuando los lleva. * Singe reescribe su carpeta `Singe` en cada arranque, así que para cambiar el estilo de los subtítulos copias `subtitle.rml` y `subtitle.rcss` junto a tu script, haces que el enlace de la copia apunte a tu propia hoja y cambias esa. * El texto sobre una imagen en movimiento necesita un margen de seguridad, su propio contraste, líneas que hayas partido tú y tiempo para leerse. [#_next_time_21] === La próxima vez Hasta aquí el texto. La cuarta parte sigue sin que tengas que leerla en orden, y la próxima lección abre una puerta que ha estado cerrada durante veinte lecciones: `overlayGetWidth` y `overlayGetHeight` son dos números, y una escena tiene tres. La lección veintidós es el tercero.