[#_lesson_19_text_that_looks_good] == Lección 19: Texto con buen aspecto image::learn/19-text.png[La lección terminada, 480] Cada palabra que has puesto en pantalla desde la lección uno ha salido de `overlayPrint`, y `overlayPrint` tiene exactamente una fuente: un bloque de píxeles de ancho fijo integrado en el motor, siempre del mismo tamaño, del color que le toque. Es perfecta para un contador de fotogramas. No sirve para un título, una puntuación, un menú ni nada que el jugador tenga que mirar en lugar de leer de pasada. Esta lección carga una fuente de verdad desde el disco, imprime con ella, le da color, la mide para que puedas centrar las cosas y te enseña lo que cuesta de verdad imprimir texto. Al final tendrás una pantalla de título que parece hecha a propósito y no por casualidad. [#_set_the_overlay_to_a_size_you_can_think_in] === Pon el overlay a un tamaño con el que puedas pensar Empieza un archivo nuevo llamado `text.singe` con dos líneas: [source,lua] ---- dofile("Singe/Framework.singe") overlaySetResolution(discGetWidth(), discGetHeight()) ---- La primera línea la has escrito muchas veces; carga el framework, que define `DIR`, los nombres `SWITCH_*` y los nombres `FONT_QUALITY_*` que estás a punto de usar. La segunda importa más de lo que parece. El overlay -- la superficie sobre la que cae todo lo que dibujas -- empieza con la *mitad* del tamaño del mundo. Un juego sin vídeo tiene un lienzo de 720 por 480, así que el overlay empieza en 360 por 240, y una fuente de 36 puntos en un overlay de 360 píxeles de ancho es enorme. `overlaySetResolution` cambia el overlay por uno del tamaño que pidas, y a partir de ahí cada coordenada que das, y todo lo que te dicen `overlayGetWidth` y `overlayGetHeight`, está en ese espacio. `discGetWidth()` y `discGetHeight()` responden con el tamaño del vídeo cuando lo hay y con el tamaño del lienzo cuando no, así que esa única línea te da un overlay que encaja con el mundo sea cual sea el tipo de juego. Llámala una vez, al principio, antes de dibujar nada. [#_load_a_font] === Carga una fuente Debajo de eso, añade: [source,lua] ---- fontQuality(FONT_QUALITY_BLENDED) local titleFont = fontLoad("Singe/FreeSansBold.ttf", 48) ---- `fontLoad` abre un archivo de fuente TrueType y te devuelve un número -- un *handle* -- que usas cada vez que quieres esa fuente. El segundo argumento es el tamaño en puntos, y queda fijo mientras viva el handle. No hay forma de cambiar después el tamaño de una fuente cargada. Si quieres el mismo tipo de letra en tres tamaños, llamas a `fontLoad` tres veces y guardas tres handles. `Singe/FreeSansBold.ttf` es la fuente que el motor desempaquetó en la carpeta `Singe` de tu carpeta de trabajo la primera vez que ejecutaste algo. Todas las copias de Singe la tienen, así que puedes contar con ella. Cualquier otro archivo `.ttf` funciona igual; ponlo junto a tu script y cárgalo como `DIR .. "myfont.ttf"`. Ahora imprime con ella: [source,lua] ---- function onOverlayUpdate() overlayClear() fontPrint(24, 60, "ASTEROID PATROL") return OVERLAY_UPDATED end ---- Ejecútalo. Letras blancas grandes cerca de la esquina superior izquierda. Fíjate en lo que *no* has hecho: no le has dicho a `fontPrint` qué fuente usar. `fontLoad` deja seleccionada la fuente que acaba de cargar, y `fontPrint` dibuja siempre con la que esté seleccionada. Con más de una fuente cargada, eliges entre ellas con `fontSelect`, que es lo que harás dentro de un momento. Fíjate también en que `24` y `60` son píxeles, no celdas de carácter. Esta es la diferencia entre `fontPrint` y `overlayPrint` que hace tropezar a la gente: `overlayPrint(2, 2, ...)` significa dos letras hacia dentro y dos líneas hacia abajo, mientras que `fontPrint(2, 2, ...)` significa dos píxeles hacia dentro y dos píxeles hacia abajo, que es estar metido en la esquina. La `y` que das es la parte de *arriba* del texto, no la línea base sobre la que se apoya. [#_color] === Color `fontPrint` recibe tres argumentos y ninguno de ellos es un color. El color sale de otro sitio: [source,lua] ---- colorForeground(255, 211, 90) fontPrint(24, 60, "ASTEROID PATROL") ---- `colorForeground` pone el rojo, el verde y el azul de `0` a `255`, y un cuarto número opcional para la opacidad. No es un argumento de nada; es un ajuste que el motor recuerda. Todo lo que se dibuja después sale de ese color hasta que lo vuelvas a cambiar, exactamente igual que ya funcionan `overlayBox` y `overlayLine`. Así que el patrón es siempre el mismo: pon el color y luego dibuja. Esto es lo primero que la gente hace mal con las fuentes, y lo segundo llega dentro de unas páginas. [#_quality] === Calidad `fontQuality` decide cómo se rasterizan las letras -- cómo se convierten en píxeles las curvas de una letra -- y hay tres opciones. Prueba cada una poniéndola encima de la línea de `fontLoad` y mirando el resultado de cerca. `FONT_QUALITY_SOLID` es la más rápida. Cada píxel o es de la letra o no lo es, así que las diagonales y las curvas salen con escalones visibles. A 48 puntos queda barato. A 12 puntos en un overlay pequeño puede verse *más nítida* que las otras, y a veces es justo lo que quieres. `FONT_QUALITY_SHADED` suaviza los bordes, pero lo hace mezclándolos con el color de fondo actual, y dibuja ese fondo como un rectángulo sólido detrás de toda la cadena. Te sale una etiqueta con una caja alrededor, la quisieras o no. Es la respuesta correcta para una lectura de depuración sobre una imagen recargada, y la equivocada para casi todo lo demás. `FONT_QUALITY_BLENDED` suaviza los bordes y los deja transparentes, así que las letras se asientan sobre lo que haya detrás. Es la que más cuesta y es la que quieres casi siempre. Úsala a menos que tengas un motivo para no hacerlo. El ajuste es global, no de cada fuente, y se aplica desde el momento en que lo llamas en adelante. Los sprites que ya has creado conservan la calidad con la que se crearon. La entrada de la referencia de `fontQuality` enumera los tres nombres y sus valores. [#_measuring_so_you_can_center_things] === Medir, para poder centrar las cosas Tarde o temprano -- normalmente a los diez minutos -- querrás el título en el centro de la pantalla. Sabes que la pantalla mide `overlayGetWidth()` de ancho. No sabes cuánto mide de ancho el texto, y no hay ninguna llamada `fontMeasure` a la que preguntar. Lo que hay en su lugar es `fontToSprite`. Dibuja una cadena con la fuente seleccionada, la calidad actual y el color actual, y en lugar de ponerla en pantalla te devuelve un *sprite*: el mismo tipo de handle que te da `spriteLoad`, lo que significa que todas las llamadas `sprite*` que aprendiste en la lección nueve funcionan con él. Incluidas las dos que lo miden. [source,lua] ---- fontSelect(titleFont) colorForeground(255, 211, 90) local titleImage = fontToSprite("ASTEROID PATROL") ---- Y luego, en `onOverlayUpdate`: [source,lua] ---- local x = (overlayGetWidth() - spriteGetWidth(titleImage)) / 2 spriteDraw(titleImage, x, 60) ---- Eso es centrar, y ese es todo el truco. Toma el ancho del espacio, réstale el ancho de la cosa, y la mitad de lo que queda es el hueco de cada lado. Alinear a la derecha es la misma idea con menos cuentas. Para poner texto contra el margen derecho: [source,lua] ---- spriteDraw(scoreImage, overlayGetWidth() - MARGIN - spriteGetWidth(scoreImage), y) ---- `spriteGetHeight` mide la otra dirección, para apilar líneas o centrar en vertical. Hay una trampa en ese primer bloque de código, y es lo segundo que todo el mundo hace mal. *El color se queda grabado cuando se crea el sprite, no cuando se dibuja.* Un `colorForeground` encima de `fontToSprite` cambia el texto. Ponerlo en cambio encima de `spriteDraw` no hace nada de nada, porque `colorForeground` es un ajuste para dibujar texto y líneas, y un sprite se dibuja con sus propios píxeles. Si tu título, coloreado con tanto cuidado, sale blanco, es por esto. Un sprite se puede volver a colorear después de crearlo, pero con una llamada de sprite, no con `colorForeground`. `spriteSetTint(image, red, green, blue)` multiplica cada píxel del sprite por ese color cada vez que se dibuja, así que un texto dibujado en blanco se puede pintar amarillo hoy y rojo mañana sin volver a crearlo. El blanco es el color con el que hay que dibujarlo cuando piensas teñirlo: cualquier cosa multiplicada por blanco se queda como estaba, así que el tinte es exactamente el color que pides. [#_what_printing_costs] === Lo que cuesta imprimir La primera vez que `fontPrint` ve una cadena, trabaja de verdad: rasteriza cada letra a partir de los contornos de la fuente y construye una imagen de toda la cadena. Luego se guarda esa imagen. La siguiente vez que imprimes la misma cadena, con la misma fuente, calidad y colores, se salta todo eso y copia la imagen que ya tiene, que cuesta más o menos lo mismo que dibujar un sprite. Una cadena que dejas de imprimir se suelta un segundo después, así que no se acumula nada. Eso significa que la forma obvia de escribir una pantalla de título es también la rápida. Imprimir `"ASTEROID PATROL"` sesenta veces por segundo la dibuja una vez. Una puntuación que cambia dos veces por segundo se dibuja dos veces por segundo, ella sola, sin que tengas que llevar ninguna cuenta. El único caso que cuesta es el texto que es *distinto* en cada fotograma -- un temporizador en milisegundos, un contador de fotogramas -- porque cada fotograma es una cadena que `fontPrint` no ha visto nunca. En un ordenador de escritorio da igual. En una Raspberry Pi o en una consola portátil, una pantalla llena de eso se notará. El remedio es cambiar el texto menos a menudo: un temporizador en segundos enteros es una cadena nueva una vez por segundo, no sesenta. Entonces, ¿por qué ha usado esta lección `fontToSprite`? Porque un sprite puede hacer cosas que el texto impreso no puede: * *Se puede medir*, que es la única forma de centrar texto o alinearlo contra el lado derecho. * *Se puede escalar y girar* con las llamadas `sprite*` de la lección nueve. * *Puede sobrevivir a su fuente*, lo que importará dentro de un momento. Usa `fontPrint` para el texto, y `fontToSprite` cuando necesites una de esas cosas. La puntuación de esta lección necesita una de ellas: va contra el margen derecho, así que hay que medirla, así que tiene que ser un sprite. Y un sprite de una puntuación solo es correcto hasta que la puntuación cambia. Cuando cambie, crea un sprite nuevo y descarga el viejo -- si se te olvida, pierdes un sprite en cada cambio: [source,lua] ---- local function renderScore() if scoreImage ~= nil then spriteUnload(scoreImage) end fontSelect(scoreFont) colorForeground(255, 255, 255) scoreImage = fontToSprite(string.format("%06d", score)) scoreShown = score end ---- `scoreShown` recuerda qué puntuación dice ese sprite, para que el código de dibujo pueda saber si sigue siendo correcto: [source,lua] ---- if score ~= scoreShown then renderScore() end ---- `string.format("%06d", score)` es Lua normal, no Singe. Convierte un número en una cadena, y `%06d` significa «como número entero, con al menos seis cifras, rellenando con ceros», que es por lo que las puntuaciones de las recreativas llevan ceros delante. Cámbialo por `%d` y los ceros desaparecen. [#_putting_fonts_away] === Recoger las fuentes `fontUnload` cierra una fuente y deja su handle inválido. Los sprites que creaste con `fontToSprite` no se ven afectados -- una vez dibujado, un sprite ya no tiene nada que ver con la fuente -- así que puedes descargar una fuente y seguir dibujando el texto que hiciste con ella. Un borde afilado: si descargas la fuente que estaba seleccionada, después no queda *ninguna* fuente seleccionada, y el siguiente `fontPrint` termina tu juego con un error. Selecciona otra antes si vas a seguir imprimiendo. El sitio ordenado para todo esto es `onShutdown`, un callback que el motor llama una vez cuando el juego se está cerrando: [source,lua] ---- function onShutdown() spriteUnload(titleImage) fontUnload(titleFont) end ---- Singe recoge lo tuyo cuando el juego termina, así que no se quema nada si te saltas esto. Escríbelo de todos modos. Un juego que suelta lo que ha cargado es un juego que puedes cargar dos veces. [#_the_whole_script] === El script completo [source,lua] ---- dofile("Singe/Framework.singe") overlaySetResolution(discGetWidth(), discGetHeight()) local MARGIN = 24 local TITLE_Y = 60 local SCORE_Y = 170 local LABEL_Y = 186 local HINT_Y = 300 local CLOCK_Y = 430 local score = 0 local titleFont = nil local scoreFont = nil local smallFont = nil local titleImage = nil local hintImage = nil local scoreImage = nil local scoreShown = nil local function centered(image) return (overlayGetWidth() - spriteGetWidth(image)) / 2 end local function renderScore() if scoreImage ~= nil then spriteUnload(scoreImage) end fontSelect(scoreFont) colorForeground(255, 255, 255) scoreImage = fontToSprite(string.format("%06d", score)) scoreShown = score end fontQuality(FONT_QUALITY_BLENDED) titleFont = fontLoad("Singe/FreeSansBold.ttf", 48) scoreFont = fontLoad("Singe/FreeSansBold.ttf", 36) smallFont = fontLoad("Singe/FreeSansBold.ttf", 18) fontSelect(titleFont) colorForeground(255, 211, 90) titleImage = fontToSprite("ASTEROID PATROL") fontSelect(smallFont) colorForeground(140, 200, 255) hintImage = fontToSprite("Press the fire button to score") renderScore() function onInputPressed(what) if what == SWITCH_BUTTON1 then score = score + 125 end end function onOverlayUpdate() overlayClear() spriteDraw(titleImage, centered(titleImage), TITLE_Y) if score ~= scoreShown then renderScore() end spriteDraw(scoreImage, overlayGetWidth() - MARGIN - spriteGetWidth(scoreImage), SCORE_Y) fontSelect(smallFont) colorForeground(160, 160, 160) fontPrint(MARGIN, LABEL_Y, "SCORE") fontPrint(MARGIN, CLOCK_Y, "Running for " .. (singeGetTicks() // 1000) .. " seconds") if (singeGetTicks() // 500) % 2 == 0 then spriteDraw(hintImage, centered(hintImage), HINT_Y) end return OVERLAY_UPDATED end function onShutdown() spriteUnload(titleImage) spriteUnload(hintImage) if scoreImage ~= nil then spriteUnload(scoreImage) end fontUnload(titleFont) fontUnload(scoreFont) fontUnload(smallFont) end ---- Ejecútalo y mantén pulsado el botón de disparo. El título y la pista se quedan centrados, la puntuación sube contra el margen derecho y solo se vuelve a dibujar cuando cambia, la pista parpadea, y la etiqueta y el reloj se imprimen con `fontPrint` en cada fotograma -- el reloj en segundos enteros, así que solo es una cadena nueva una vez por segundo. [#_what_just_happened_15] === Qué acaba de pasar [source,lua] ---- titleFont = fontLoad("Singe/FreeSansBold.ttf", 48) scoreFont = fontLoad("Singe/FreeSansBold.ttf", 36) smallFont = fontLoad("Singe/FreeSansBold.ttf", 18) ---- Un archivo, tres tamaños, tres handles. Cada uno cuesta memoria para su propio juego de glifos dibujados, así que carga los tamaños que uses y ni uno más. Tres es normal. Una docena huele mal. Después de estas tres líneas la fuente seleccionada es `smallFont`, porque cargar selecciona. Por eso el código llama a `fontSelect(titleFont)` antes de dibujar el título: lo último que se ha cargado no es lo que quieres primero. [source,lua] ---- local function centered(image) return (overlayGetWidth() - spriteGetWidth(image)) / 2 end ---- Escrita una vez como función porque se usa dos veces, y porque la próxima vez que quieras centrar algo la vas a volver a querer. Ponle nombre a la cuenta y no tendrás que volver a leerla nunca. [source,lua] ---- if (singeGetTicks() // 500) % 2 == 0 then spriteDraw(hintImage, centered(hintImage), HINT_Y) end ---- `singeGetTicks()` es el número de milisegundos desde que arrancó el motor. `//` es una división que tira el resto, así que `singeGetTicks() // 500` cuenta medios segundos, y `% 2` es el resto de dividir entre dos, que es `0`, `1`, `0`, `1`... Así que la pista se dibuja durante medio segundo y se salta durante medio segundo. No dibujar nada es como se hace que algo parpadee. [source,lua] ---- fontSelect(smallFont) colorForeground(160, 160, 160) fontPrint(MARGIN, LABEL_Y, "SCORE") ---- Aquí hacen falta los dos ajustes, y vale la pena tener claro por qué. Dibujar un sprite no cambia la fuente seleccionada ni el color, así que las dos llamadas `spriteDraw` de arriba los dejan exactamente como los encontraron. Pero el código de preparación seleccionó tres fuentes distintas una tras otra, y `renderScore` selecciona otra cada vez que se mueve la puntuación, así que la selección cuando se ejecuta esta línea es la que haya tocado elegir la última. No intentes seguirle la pista. Selecciona la fuente y pon el color justo antes de imprimir, siempre. Son dos líneas, y te quitan de encima toda una categoría de tardes de desconcierto. [#_try_it_19] === Pruébalo . *Cambia la calidad.* Pon `FONT_QUALITY_SOLID` en la línea de `fontQuality` y mira la curva de la `S` de `ASTEROID`. Luego prueba `FONT_QUALITY_SHADED` y fíjate en lo que pasa detrás de cada cadena. . *Pon la pista en amarillo.* Mueve la línea `colorForeground(140, 200, 255)` de encima de `fontToSprite` a justo encima del `spriteDraw` que dibuja `hintImage`, y cambia los números. Averigua por qué no pasa nada. Luego devuélvela a su sitio, haz la pista blanca y vuélvela amarilla con `spriteSetTint` justo antes de dibujarla. . *Alinea el reloj a la derecha.* El reloj se dibuja con `fontPrint`, que no se puede medir. Ponlo contra el margen derecho de todos modos. Solo hay una forma, y encontrarla es lo importante. . *Quita la comprobación de `scoreShown`*, para que `renderScore()` se ejecute en cada fotograma. Se verá igual, pero ahora dibuja la puntuación sesenta veces por segundo en lugar de cuando cambia -- justo el trabajo que `fontPrint` te ahorra cuando le dejas. Luego quita también el `spriteUnload` de `renderScore`, déjalo en marcha un minuto y vigila la memoria que usa tu juego. . *Imprime un reloj en milisegundos.* Cambia la línea del reloj para que imprima `singeGetTicks()` sin el `// 1000`. Sigue funcionando; ahora es una cadena que `fontPrint` no ha visto nunca, en cada fotograma. En un ordenador lento, ese es el tipo de texto que cuesta. . *Usa tu propia fuente.* Busca un `.ttf` en tu ordenador, cópialo junto a tu script y cárgalo con `DIR .. "thatfont.ttf"`. Algunas fuentes quedan fatal a 18 puntos y bien a 48. Eso es culpa de la fuente, no tuya. [#_break_it_on_purpose_18] === Rómpelo a propósito Añade una línea que diga `fontPrint(24, 60, "SCORE")` justo debajo de `overlaySetResolution(discGetWidth(), discGetHeight())`, para que se ejecute antes de que haya habido ningún `fontLoad`. Singe arranca y se muere al instante: ---- 4:fontPrint: No font selected. ---- Léelo igual que cualquier otro error. `4` es la línea. `fontPrint` es la llamada del motor que se ha quejado -- este tipo de mensaje sale de dentro del motor y no de Lua, así que nombra la función en lugar del archivo. El resto es la queja, y aquí es exacta: le has pedido que imprima y no había ninguna fuente con la que imprimir. Te encontrarás con este de verdad de una forma menos evidente. Descarga la fuente seleccionada mientras el juego está en marcha y el siguiente `fontPrint` dirá exactamente lo mismo, a varios cientos de líneas del `fontUnload` que lo provocó. [#_what_you_learned_19] === Lo que has aprendido * `fontLoad` abre un archivo TrueType a un tamaño en puntos fijo y devuelve un handle. Un tamaño, un handle. * Cargar una fuente la selecciona; `fontSelect` elige entre las fuentes cargadas. * `fontPrint` cuenta en píxeles y `overlayPrint` cuenta en celdas de carácter. * El color no es un argumento. `colorForeground` es un ajuste; ponlo y luego dibuja. * `fontQuality` elige cómo se rasterizan las letras, y `FONT_QUALITY_BLENDED` es la respuesta habitual. * `fontPrint` guarda lo que dibuja, así que imprimir el mismo texto en cada fotograma es barato; lo que cuesta es el texto que cambia en cada fotograma. * `fontToSprite` dibuja en un sprite, que `spriteGetWidth` mide y que las llamadas `sprite*` pueden escalar y girar. Úsalo para eso, no por velocidad. * Medir es la forma de centrar o alinear texto a la derecha, porque nada más te puede decir cuánto va a medir de ancho una cadena. * `colorForeground` llega al texto cuando se crea, nunca a un sprite cuando se dibuja; `spriteSetTint` es la forma de volver a colorear un sprite después. * `fontUnload` libera una fuente; los sprites que creó sobreviven. [#_next_time_19] === La próxima vez Un texto con buen aspecto está muy lejos de un menú que funcione. Una página con un título, una lista por la que mueves un cursor, un deslizador y un botón son cien líneas de dibujo y de cuentas con el cursor si la construyes con las llamadas de esta lección. La lección veinte no la construye con esas llamadas en absoluto.