Efectos de sprite y de texto
Sombra, contorno, glow y blur sobre sprites 2D y sobre texto, con lo que cuesta cada uno, lo que le hace a un sprite en movimiento, y la receta manual que reemplaza.
Para qué sirve esto#
Hasta ahora un sprite 2D podía llevar un tinte y una imagen, y nada más. Para resaltar un personaje sobre su fondo había que construir el efecto a mano: crear un segundo sprite, teñirlo de negro, desplazarlo, ponerlo detrás, y mantenerlo sincronizado con el primero en cada fotograma. Funciona. También son cuatro líneas extra de contabilidad por personaje, y el día que olvidas una de ellas la sombra se queda rezagada respecto a su dueño.
Cuatro comandos hacen eso por ti, en sprites y en texto:
hero = LoadSprite("hero.png")
SpritePosition(hero, 160, 120)
SpriteShadow(hero, 4, 4, 6, 128) ' offset x, offset y, softness, opacity
SpriteOutline(hero, 2, 255, 255, 255) ' thickness, colour
SpriteGlow(hero, 0, 240, 255, 8) ' colour, size
DO
Sync()
LOOP
Nada más que mantener. Mueve el sprite, escálalo, voltéalo, reproduce su animación: los efectos lo siguen.
Los cuatro efectos#
Sombra#
SpriteShadow(spriteID, offsetX, offsetY, softness, opacity)
SpriteShadowColor(spriteID, red, green, blue)
offsetX y offsetY están en tus propias unidades de pantalla, contadas igual
que SpritePosition: la Y positiva es hacia abajo. softness difumina la
sombra; opacity va de 0 a 255. El color es negro a menos que digas lo
contrario.
SpriteShadow(hero, 3, 5, 4, 110)
SpriteShadowColor(hero, 20, 0, 40) ' una sombra fría, no una gris
Una sombra de opacidad 0 no es ninguna sombra. Así es como la desactivas.
Contorno#
SpriteOutline(spriteID, thickness, red, green, blue)
El contorno sigue la silueta del sprite, no el rectángulo en el que vive: un PNG transparente recibe un contorno alrededor del dibujo, no alrededor del archivo. El grosor está en unidades de pantalla, y de 1 a 3 es el rango útil. Por encima de eso, las ocho direcciones con las que se construye el contorno empiezan a verse como ocho lóbulos.
SpriteOutline(hero, 2, 255, 255, 255) ' el contorno legible sobre cualquier fondo
SpriteOutline(hero, 0, 0, 0, 0) ' apagado
Glow#
SpriteGlow(spriteID, red, green, blue, size)
Un halo de color, que se extiende size unidades más allá de la silueta. Se
sitúa detrás del sprite y delante de la sombra, que es lo que permite usar
ambos.
SpriteGlow(pickup, 255, 210, 0, 10) ' este vale la pena ir a buscarlo
Blur#
SpriteBlur(spriteID, radius)
Blur es el raro de los cuatro. Los otros tres se dibujan alrededor del sprite; blur reemplaza la imagen propia del sprite por una copia difuminada de ella. Eso implica dos cosas que vale la pena saber:
- no cuesta nada por fotograma (la imagen difuminada se calcula una sola vez), y
- el blur se recorta en el borde de la imagen. Un dibujo que toca el borde de su PNG pierde la parte de su halo que caería fuera. Deja unos pocos píxeles transparentes de margen en el archivo si piensas difuminarlo.
SpriteColor sigue funcionando sobre un sprite difuminado.
Apagarlo todo, y volver a encenderlo#
SpriteEffects(spriteID, 0) ' los cuatro apagados
SpriteEffects(spriteID, 1) ' los cuatro de vuelta, exactamente como estaban
SpriteEffects no borra tus ajustes. Es un interruptor, no un reinicio, así
que un personaje puede parpadear sin que tengas que volver a declarar su
sombra después.
Los mismos cuatro sobre texto#
score = CreateText("SCORE 0")
TextPositionSet(score, 10, 10)
TextSizeSet(score, 6)
TextOutline(score, 3, 0, 0, 0) ' legible sobre cualquier fondo
TextShadow(score, 2, 2, 4, 160)
TextGlow(score, 255, 80, 0, 6)
TextBlur(score, 0)
TextEffects(score, 1)
Las firmas coinciden exactamente con las de los sprites, así que no hay nada nuevo que aprender. Dos diferencias de comportamiento:
- Un glow gana sobre el softness de una sombra. Ambos se dibujan con el mismo mecanismo, y un glow sin softness es invisible mientras que una sombra sin softness sigue siendo una sombra. Cuando pides ambos, el glow se queda con el softness.
- Esto se aplica a texto vectorial, no a texto de fuente bitmap. Un texto
dibujado con
TextFontImagees una composición de sprites; los comandos se ignoran en silencio sobre él. Usa los efectos de sprite sobre los sprites en ese caso.
Un ejemplo trabajado: legible sobre cualquier cosa#
El problema que estos comandos realmente resuelven es el contraste que no controlas. Una puntuación blanca es invisible sobre la nieve; una negra es invisible de noche. Un contorno lo resuelve de una vez, para cualquier fondo que vayas a dibujar jamás.
CreateText(1, "SCORE 0")
TextPositionSet(1, 10, 10)
TextSizeSet(1, 8)
TextColorSet(1, 255, 255, 255, 255)
TextOutline(1, 3, 0, 0, 0)
hero = LoadSprite("hero.png")
SpriteName(hero, "hero")
SpriteShadow(hero, 4, 6, 5, 120)
SpriteOutline(hero, 2, 255, 255, 255)
points = 0
DO
points = points + 1
TextString(1, "SCORE " + Str(points))
Sync()
LOOP
Leer un efecto de vuelta#
Cada parámetro que escribes tiene un lector, igual que SpriteColor tiene
SpriteColorRed. Esa es la regla del dialecto, y estos comandos la siguen.
IF SpriteEffectsEnabled(hero) = 1
SpriteEffects(hero, 0)
ENDIF
alpha = SpriteShadowAlpha(hero) ' 0 a 255
size = SpriteGlowSize(hero) ' 0 significa sin glow
El conjunto completo, por familia: SpriteShadowOffsetX, SpriteShadowOffsetY,
SpriteShadowBlur, SpriteShadowAlpha, SpriteShadowColorRed,
SpriteShadowColorGreen, SpriteShadowColorBlue, SpriteGlowRed,
SpriteGlowGreen, SpriteGlowBlue, SpriteGlowSize,
SpriteOutlineThickness, SpriteOutlineRed, SpriteOutlineGreen,
SpriteOutlineBlue, SpriteBlurRadius, SpriteEffectsEnabled, más los
mismos diecisiete con el prefijo Text.
Dos reglas que vale la pena conocer:
- Un sprite al que nunca se le dio un efecto se reporta a sí mismo como encendido. Ese es el estado por defecto; responder 0 le diría a un programa que sus efectos están cortados antes de que haya fijado ninguno.
- Preguntar por un sprite que no existe responde 0, no detiene el
programa. Así es como el dialecto trata las preguntas en todas partes:
SpriteHitresponde 0 cuando nada se toca, y los programas leen ese 0 sin comprobarlo.
Lo que cuesta#
Shadow, outline y glow se dibujan, así que cuestan draw calls: uno para la sombra (tres cuando es soft), cuatro para un glow, ocho para un outline. Una pantalla con doscientos sprites con outline es una pantalla dibujando mil ochocientos quads, y se va a notar. Están pensados para el puñado de cosas que el jugador debe mirar: el personaje, el pickup, la unidad seleccionada.
Blur no cuesta nada por fotograma.
Una regla práctica: efectos en lo que se mueve e importa, ninguno en el fondo.
Lo que llega a un juego exportado#
| Destino | Shadow, outline, glow | Blur | Efectos de texto |
|---|---|---|---|
| El editor | sí | sí | sí |
| Exportación HTML5 | sí | sí | sí |
| Nativo y wasm (Bevy) | sí | no | no |
Shadow, outline y glow están construidos con sprites normales, así que viajan a todas partes donde viajan los sprites. Blur y los efectos de texto necesitan un puente que los destinos Bevy todavía no tienen: el informe de conversión de la exportación los nombra en lugar de descartarlos en silencio, así que una build te dice qué no pudo llevar.
Dónde no están los efectos#
No hay un SpriteEffect(id, "glow", ...) que tome el nombre del efecto como
una cadena. Cada efecto es su propio comando para que sus parámetros se puedan
nombrar, documentar y autocompletar mientras escribes. Para cualquier cosa más
allá de estos cuatro, otros dos comandos toman el relevo:
SpriteBlendModes(id, src, dst) para mezcla aditiva o multiplicada (un fuego,
un láser), y SpriteShader(id, shaderID) para poner un shader compilado por
LoadSpriteShader sobre un sprite. Ambos estuvieron declarados en el catálogo
durante mucho tiempo sin una implementación, y llamarlos lanzaba un error;
ahora funcionan.