精灵与文字特效

在 2D 精灵和文字上添加阴影、描边、发光和模糊,附带每一种的开销,它对一个正在移动的精灵会造成什么影响,以及它取代了哪种手搓配方。

这是用来做什么的#

在此之前,一个 2D 精灵只能携带一个色调和一张图片,别的什么都没有。要把一个角色从背景里凸显出来,你得手工搭出这个效果:创建第二个精灵,把它染成黑色,做一个偏移,放到后面,并且在每一帧都让它跟第一个精灵保持同步。这样能行。但这也是每个角色额外四行的记账工作,而你哪天忘了其中一行,那个阴影就会拖在它主人身后。

四个命令替你做了这件事,作用于精灵,也作用于文字:

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

不需要再维护别的东西。移动这个精灵,缩放它,翻转它,播放它的动画:这些效果都会跟着走。

四种效果#

阴影(Shadow)#

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

offsetXoffsetY 用的是你自己的屏幕单位,计算方式和 SpritePosition 一样:正的 Y 是向下softness 让阴影扩散;opacity 的范围是 0 到 255。如果不特别指定,颜色就是黑色。

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

透明度为 0 的阴影,就等于没有阴影。这就是关掉它的方式。

描边(Outline)#

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

描边跟随的是精灵的轮廓,而不是它所在的那个矩形:一张透明背景的 PNG,得到的描边会围绕图案本身,而不是围绕整个文件。粗细以屏幕单位计,1 到 3 是常用范围。超过这个范围,描边所依据的那八个方向就会开始显现成八个瓣状突起。

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

发光(Glow)#

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

一圈彩色的光晕,向外扩散到超出轮廓 size 个单位的地方。它位于精灵之后、阴影之前,这正是让你能同时使用这两者的原因。

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

模糊(Blur)#

SpriteBlur(spriteID, radius)

模糊是这四者中的特例。另外三种是画在精灵周围的,模糊则是把精灵自己的图像替换成它的一份模糊拷贝。这意味着两件值得知道的事:

  • 它每帧不需要额外开销(这份模糊图像只计算一次),并且
  • 模糊会在图像边缘被裁切。一幅画到自己 PNG 边缘的图案,会失去那部分本该落在文件之外的光晕。如果你打算给一张图做模糊,就在文件里留几个像素透明的边距。

SpriteColor 在一个已经模糊的精灵上依然有效。

把一切都关掉,再打开#

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

SpriteEffects 不会抹掉你的设置。它是一个开关,不是一次重置,所以一个角色可以先闪一下再恢复,而不需要你事后重新声明它的阴影。

同样的四种效果,用在文字上#

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)

这些函数签名和精灵上的那套完全一致,所以没有新东西要学。行为上有两处不同:

  • 发光会压过阴影的柔和度。 两者由同一套机制绘制,一个没有柔和度的发光是不可见的,而一个没有柔和度的阴影依然是一个阴影。当你两者都要的时候,柔和度会被发光拿走。
  • 这些效果只适用于矢量文字,不适用于位图字体文字。TextFontImage 绘制的文字,本质上是一组精灵的组合;这些命令在它上面会被静默忽略。遇到这种情况,改用精灵特效,作用在那些精灵上。

一个实际例子:在任何背景下都看得清#

这些命令真正解决的问题,是你无法控制的对比度。一个白色的分数在雪地上会看不见;一个黑色的会在夜里看不见。一次描边就能一劳永逸地解决它,适用于你未来会画的每一种背景。

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

把一种效果的值读回来#

你写入的每一个参数都有对应的读取方式,就像 SpriteColorSpriteColorRed 一样。这是这门方言的规则,这几个命令也遵循它。

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

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

完整的一套,按族分别列出:SpriteShadowOffsetXSpriteShadowOffsetYSpriteShadowBlurSpriteShadowAlphaSpriteShadowColorRedSpriteShadowColorGreenSpriteShadowColorBlueSpriteGlowRedSpriteGlowGreenSpriteGlowBlueSpriteGlowSizeSpriteOutlineThicknessSpriteOutlineRedSpriteOutlineGreenSpriteOutlineBlueSpriteBlurRadiusSpriteEffectsEnabled,外加同样十七个、带 Text 前缀的版本。

两条值得了解的规则:

  • 一个从未被赋予过任何效果的精灵,会把自己报告为开启状态。 这就是默认状态;如果回答 0,会让一个程序误以为自己的效果被关闭了,而它其实还没设置过任何效果。
  • 询问一个不存在的精灵会得到 0,而不会让程序停下来。 这正是这门方言在所有地方处理疑问的方式:SpriteHit 在没有碰到任何东西时会返回 0,程序读到这个 0 时不需要先做判断。

它的开销#

阴影、描边和发光都是绘制出来的,所以它们要占用绘制调用次数:阴影一次(柔和时是三次),发光四次,描边八次。一个屏幕上有两百个带描边的精灵,就是一个正在绘制一千八百个四边形的屏幕,而且这会显现出来。它们是为数不多几样、玩家真正应该被引导去看的东西准备的:角色、拾取物、被选中的单位。

模糊每帧不产生额外开销。

一条实用的原则:在会动、且重要的东西上用效果,背景上一个都不用。

导出到游戏成品里会保留什么#

目标平台阴影、描边、发光模糊文字特效
编辑器
HTML5 导出
原生和 wasm(Bevy)没有没有

阴影、描边和发光是用普通精灵搭建出来的,所以精灵能到达的地方它们都能到达。模糊和文字特效需要一座 Bevy 目标平台还没有的桥梁:导出的转换报告会点名它们,而不是悄悄丢弃,所以一次构建会告诉你它没能带上什么。

这些效果没有覆盖到的地方#

没有一个 SpriteEffect(id, "glow", ...) 这样以字符串形式传入效果名称的命令。每一种效果都是自己独立的一个命令,这样它的参数才能被命名、被记录文档,并在你输入时给出补全。除了这四种之外,还有另外两个命令可以接手: SpriteBlendModes(id, src, dst) 用于加色或乘色混合(一团火焰、一道激光),以及 SpriteShader(id, shaderID) 可以把一个由 LoadSpriteShader 编译出来的着色器用到一个精灵上。这两者曾经在目录里被声明了很长时间,却没有对应的实现,调用它们会报错;现在它们已经可以用了。