Efeitos de sprite e de texto

Sombra, contorno, brilho e desfoque em sprites 2D e em texto, com o que cada um custa, o que faz com um sprite em movimento, e a receita feita à mão que ele substitui.

Para que serve isso#

Até agora um sprite 2D podia carregar um tingimento e uma imagem, e nada mais. Para destacar um personagem do fundo, era preciso construir o efeito à mão: criar um segundo sprite, tingi-lo de preto, deslocá-lo, colocá-lo atrás, e mantê-lo sincronizado com o primeiro em cada quadro. Funciona. Também são quatro linhas extras de contabilidade por personagem, e no dia em que você esquece uma delas a sombra fica atrás de quem a projeta.

Quatro comandos fazem isso por você, em sprites e em 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 mais para manter. Mova o sprite, escale-o, inverta-o, toque sua animação: os efeitos acompanham.

Os quatro efeitos#

Sombra#

SpriteShadow(spriteID, offsetX, offsetY, softness, opacity)
SpriteShadowColor(spriteID, red, green, blue)

offsetX e offsetY estão nas suas próprias unidades de tela, contadas do mesmo jeito que SpritePosition: Y positivo é para baixo. softness espalha a sombra; opacity vai de 0 a 255. A cor é preta a menos que você diga o contrário.

SpriteShadow(hero, 3, 5, 4, 110)
SpriteShadowColor(hero, 20, 0, 40)      ' a cold shadow, not a grey one

Uma sombra com opacidade 0 não é sombra nenhuma. É assim que você a desliga.

Contorno#

SpriteOutline(spriteID, thickness, red, green, blue)

O contorno segue a silhueta do sprite, não o retângulo em que ele vive: um PNG transparente ganha um contorno ao redor do desenho, não ao redor do arquivo. A espessura está em unidades de tela, e de 1 a 3 é a faixa útil. Acima disso, as oito direções a partir das quais o contorno é construído começam a aparecer como oito lóbulos.

SpriteOutline(hero, 2, 255, 255, 255)   ' the readable-on-anything outline
SpriteOutline(hero, 0, 0, 0, 0)         ' off

Brilho#

SpriteGlow(spriteID, red, green, blue, size)

Um halo colorido, espalhando size unidades para além da silhueta. Ele fica atrás do sprite e na frente da sombra, o que é o que permite usar os dois juntos.

SpriteGlow(pickup, 255, 210, 0, 10)     ' this one is worth walking to

Desfoque#

SpriteBlur(spriteID, radius)

O desfoque é o diferente do grupo. Os outros três são desenhados ao redor do sprite; o desfoque substitui a própria imagem do sprite por uma cópia desfocada dela. Isso significa duas coisas que vale saber:

  • ele não custa nada por quadro (a imagem desfocada é calculada uma vez), e
  • o desfoque é cortado na borda da imagem. Um desenho que toca a borda do seu PNG perde a parte do seu halo que cairia para fora. Deixe algumas margens transparentes de pixels no arquivo se você planeja desfocá-lo.

SpriteColor continua funcionando num sprite desfocado.

Desligando tudo, e ligando de volta#

SpriteEffects(spriteID, 0)   ' all four off
SpriteEffects(spriteID, 1)   ' all four back, exactly as they were

SpriteEffects não apaga suas configurações. É um interruptor, não um reset, então um personagem pode piscar sem que você precise redeclarar a sombra dele depois.

Os mesmos quatro no texto#

score = CreateText("SCORE 0")
TextPositionSet(score, 10, 10)
TextSizeSet(score, 6)

TextOutline(score, 3, 0, 0, 0)          ' readable on any background
TextShadow(score, 2, 2, 4, 160)
TextGlow(score, 255, 80, 0, 6)
TextBlur(score, 0)
TextEffects(score, 1)

As assinaturas batem exatamente com as dos sprites, então não há nada novo para aprender. Duas diferenças de comportamento:

  • Um brilho vence a suavidade de uma sombra. Os dois são desenhados pelo mesmo mecanismo, e um brilho sem suavidade é invisível, enquanto uma sombra sem suavidade ainda é uma sombra. Quando você pede os dois, o brilho fica com a suavidade.
  • Eles se aplicam a texto vetorial, não a texto de fonte bitmap. Um texto desenhado com TextFontImage é uma composição de sprites; os comandos são ignorados nele em silêncio. Use os efeitos de sprite nos sprites, nesse caso.

Um exemplo prático: legível sobre qualquer coisa#

O problema que esses comandos de fato resolvem é o contraste que você não controla. Uma pontuação branca é invisível na neve; uma preta é invisível à noite. Um contorno resolve isso de uma vez, para todo fundo que você algum dia vai desenhar.

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

Lendo um efeito de volta#

Todo parâmetro que você escreve tem um leitor, do jeito que SpriteColor tem SpriteColorRed. Essa é a regra do dialeto, e esses comandos a seguem.

IF SpriteEffectsEnabled(hero) = 1
  SpriteEffects(hero, 0)
ENDIF

alpha = SpriteShadowAlpha(hero)      ' 0 to 255
size  = SpriteGlowSize(hero)         ' 0 means no glow

O conjunto completo, por família: SpriteShadowOffsetX, SpriteShadowOffsetY, SpriteShadowBlur, SpriteShadowAlpha, SpriteShadowColorRed, SpriteShadowColorGreen, SpriteShadowColorBlue, SpriteGlowRed, SpriteGlowGreen, SpriteGlowBlue, SpriteGlowSize, SpriteOutlineThickness, SpriteOutlineRed, SpriteOutlineGreen, SpriteOutlineBlue, SpriteBlurRadius, SpriteEffectsEnabled, mais os mesmos dezessete com um prefixo Text.

Duas regras que vale a pena conhecer:

  • Um sprite que nunca recebeu um efeito relata a si mesmo como LIGADO. Esse é o estado padrão; responder 0 diria a um programa que seus efeitos estão cortados antes mesmo de ele ter definido algum.
  • Perguntar sobre um sprite que não existe responde 0, e não interrompe o programa. É assim que o dialeto trata perguntas em todo lugar: SpriteHit responde 0 quando nada é tocado, e programas leem esse 0 sem testá-lo.

O que isso custa#

Sombra, contorno e brilho são desenhados, então custam chamadas de desenho: uma para a sombra (três quando ela é suave), quatro para um brilho, oito para um contorno. Uma tela com duzentos sprites contornados é uma tela desenhando mil e oitocentos quads, e isso vai aparecer. Eles são pensados para o punhado de coisas que o jogador de fato deve olhar: o personagem, o item, a unidade selecionada.

O desfoque não custa nada por quadro.

Uma regra prática: efeitos no que se move e no que importa, nenhum no fundo.

O que atravessa para um jogo exportado#

AlvoSombra, contorno, brilhoDesfoqueEfeitos de texto
O editorsimsimsim
Exportação HTML5simsimsim
Nativo e wasm (Bevy)simnãonão

Sombra, contorno e brilho são construídos a partir de sprites comuns, então viajam para onde quer que os sprites vão. O desfoque e os efeitos de texto precisam de uma ponte que os alvos Bevy ainda não têm: o relatório de conversão da exportação os nomeia em vez de descartá-los em silêncio, então um build te diz o que não conseguiu carregar.

Onde os efeitos não estão#

Não existe um SpriteEffect(id, "glow", ...) que recebe o nome do efeito como string. Cada efeito é seu próprio comando para que seus parâmetros possam ser nomeados, documentados e completados enquanto você digita. Para qualquer coisa além desses quatro, dois outros comandos assumem: SpriteBlendModes(id, src, dst) para mistura aditiva ou multiplicada (um fogo, um laser), e SpriteShader(id, shaderID) para colocar um shader compilado por LoadSpriteShader num sprite. Os dois estavam declarados no catálogo há muito tempo sem uma implementação, e chamá-los levantava um erro; agora funcionam.