精灵与文字特效
在 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)
offsetX 和 offsetY 用的是你自己的屏幕单位,计算方式和
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
把一种效果的值读回来#
你写入的每一个参数都有对应的读取方式,就像 SpriteColor 有 SpriteColorRed 一样。这是这门方言的规则,这几个命令也遵循它。
IF SpriteEffectsEnabled(hero) = 1
SpriteEffects(hero, 0)
ENDIF
alpha = SpriteShadowAlpha(hero) ' 0 to 255
size = SpriteGlowSize(hero) ' 0 means no glow
完整的一套,按族分别列出:SpriteShadowOffsetX、SpriteShadowOffsetY、
SpriteShadowBlur、SpriteShadowAlpha、SpriteShadowColorRed、
SpriteShadowColorGreen、SpriteShadowColorBlue、SpriteGlowRed、
SpriteGlowGreen、SpriteGlowBlue、SpriteGlowSize、
SpriteOutlineThickness、SpriteOutlineRed、SpriteOutlineGreen、
SpriteOutlineBlue、SpriteBlurRadius、SpriteEffectsEnabled,外加同样十七个、带 Text 前缀的版本。
两条值得了解的规则:
- 一个从未被赋予过任何效果的精灵,会把自己报告为开启状态。 这就是默认状态;如果回答 0,会让一个程序误以为自己的效果被关闭了,而它其实还没设置过任何效果。
- 询问一个不存在的精灵会得到 0,而不会让程序停下来。 这正是这门方言在所有地方处理疑问的方式:
SpriteHit在没有碰到任何东西时会返回 0,程序读到这个 0 时不需要先做判断。
它的开销#
阴影、描边和发光都是绘制出来的,所以它们要占用绘制调用次数:阴影一次(柔和时是三次),发光四次,描边八次。一个屏幕上有两百个带描边的精灵,就是一个正在绘制一千八百个四边形的屏幕,而且这会显现出来。它们是为数不多几样、玩家真正应该被引导去看的东西准备的:角色、拾取物、被选中的单位。
模糊每帧不产生额外开销。
一条实用的原则:在会动、且重要的东西上用效果,背景上一个都不用。
导出到游戏成品里会保留什么#
| 目标平台 | 阴影、描边、发光 | 模糊 | 文字特效 |
|---|---|---|---|
| 编辑器 | 有 | 有 | 有 |
| HTML5 导出 | 有 | 有 | 有 |
| 原生和 wasm(Bevy) | 有 | 没有 | 没有 |
阴影、描边和发光是用普通精灵搭建出来的,所以精灵能到达的地方它们都能到达。模糊和文字特效需要一座 Bevy 目标平台还没有的桥梁:导出的转换报告会点名它们,而不是悄悄丢弃,所以一次构建会告诉你它没能带上什么。
这些效果没有覆盖到的地方#
没有一个 SpriteEffect(id, "glow", ...) 这样以字符串形式传入效果名称的命令。每一种效果都是自己独立的一个命令,这样它的参数才能被命名、被记录文档,并在你输入时给出补全。除了这四种之外,还有另外两个命令可以接手:
SpriteBlendModes(id, src, dst) 用于加色或乘色混合(一团火焰、一道激光),以及 SpriteShader(id, shaderID) 可以把一个由 LoadSpriteShader 编译出来的着色器用到一个精灵上。这两者曾经在目录里被声明了很长时间,却没有对应的实现,调用它们会报错;现在它们已经可以用了。