singe/docs/es/lessons/19-text.adoc

543 lines
21 KiB
Text

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