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 TextFontImage es 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: SpriteHit responde 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#

DestinoShadow, outline, glowBlurEfectos de texto
El editor
Exportación HTML5
Nativo y wasm (Bevy)nono

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.