精靈與文字特效

在 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)      ' 一個偏冷調的陰影,而不是灰色的

不透明度為 0 的陰影,就等於沒有陰影。這正是關掉它的方法。

外框(Outline)#

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

外框跟隨的是精靈的輪廓,而不是它所在的矩形範圍:一張透明的 PNG 得到的是圍繞著圖案本身的外框,而不是圍繞著整個檔案的外框。粗細以螢幕單位計算,1 到 3 是實用的範圍。超過這個範圍,外框所依據的八個方向就會開始顯現成八個瓣狀突起。

SpriteOutline(hero, 2, 255, 255, 255)   ' 放在任何背景上都清楚可辨的外框
SpriteOutline(hero, 0, 0, 0, 0)         ' 關閉

發光(Glow)#

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

一圈有顏色的光暈,向外擴散超出輪廓 size 個單位。它位於精靈後方、陰影前方,這正是讓兩者能同時使用的原因。

SpriteGlow(pickup, 255, 210, 0, 10)     ' 這個值得讓玩家特地走過去看看

模糊(Blur)#

SpriteBlur(spriteID, radius)

模糊是特別的一個。另外三種效果都畫在精靈周圍;模糊則是把精靈本身的圖片換成一份模糊過的副本。這代表兩件值得知道的事:

  • 它不會消耗任何逐幀成本(模糊過的圖片只會計算一次),而且
  • 模糊會在圖片邊緣被裁掉。一張圖案觸及自己 PNG 邊緣的圖片,會失去那部分原本會落在檔案範圍之外的光暈。如果你打算模糊一張圖,請在檔案裡留一點透明像素的邊界。

SpriteColor 在一個已模糊的精靈上依然能正常運作。

全部關閉,再全部打開#

SpriteEffects(spriteID, 0)   ' 四種效果全部關閉
SpriteEffects(spriteID, 1)   ' 四種效果全部恢復,與原本完全一樣

SpriteEffects 不會抹除你的設定。它是一個開關,不是一次重置,所以一個角色可以閃爍一下,而你不必事後重新宣告它的陰影設定。

文字上的同一套四種效果#

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

TextOutline(score, 3, 0, 0, 0)          ' 在任何背景上都清楚可辨
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 到 255
size  = SpriteGlowSize(hero)         ' 0 代表沒有發光

完整清單,依家族區分:SpriteShadowOffsetXSpriteShadowOffsetYSpriteShadowBlurSpriteShadowAlphaSpriteShadowColorRedSpriteShadowColorGreenSpriteShadowColorBlueSpriteGlowRedSpriteGlowGreenSpriteGlowBlueSpriteGlowSizeSpriteOutlineThicknessSpriteOutlineRedSpriteOutlineGreenSpriteOutlineBlueSpriteBlurRadiusSpriteEffectsEnabled,外加同樣的十七個指令加上 Text 前綴。

有兩個規則值得知道:

  • 一個從未被設定過任何效果的精靈,會回報自己是開啟狀態。 那正是預設狀態;如果回答 0,會讓一個程式誤以為它的效果在還沒設定任何內容之前就已經被關閉了。
  • 詢問一個不存在的精靈會得到 0,而不會讓程式停止。 這個方言到處都是這樣處理問題的:SpriteHit 在沒有碰到任何東西時也回傳 0,而程式會直接使用這個 0,不會另外檢查。

它的成本是什麼#

陰影、外框和發光都是被畫出來的,所以它們會消耗繪製呼叫:陰影一次(柔和時要三次),發光四次,外框八次。一個畫面上有兩百個帶外框的精靈,就是一個要畫出一千八百個四邊形的畫面,而且效果會顯現出來。這些效果是為了玩家真正需要留意的少數東西而設計的:角色、拾取物、選取中的單位。

模糊不會消耗任何逐幀成本。

一個實用的原則:效果只用在會動、且重要的東西上,背景上一個都不要用。

匯出後的遊戲會保留什麼#

目標平台陰影、外框、發光模糊文字特效
編輯器
HTML5 匯出
原生與 wasm(Bevy)沒有沒有

陰影、外框和發光是用一般的精靈組成的,所以精靈能到達的地方,它們都能到達。模糊和文字特效需要一座 Bevy 目標平台目前還沒有的橋樑:匯出的轉換報告會明確列出它們,而不是悄悄丟棄,所以一次組建會告訴你它沒能帶走什麼。

這些效果沒有涵蓋到的地方#

這裡沒有一個 SpriteEffect(id, "glow", ...) 這種以字串指定效果名稱的指令。每種效果都是自己獨立的指令,這樣它的參數才能被命名、被寫進文件,並在你輸入時被自動補全。除了這四種之外,還有另外兩個指令可以接手: SpriteBlendModes(id, src, dst) 用於加法或乘法混合(例如火焰、雷射),以及 SpriteShader(id, shaderID) 可以把一個由 LoadSpriteShader 編譯出來的著色器套到一個精靈上。這兩個指令曾經在目錄裡登記了很長一段時間卻沒有實作,呼叫它們會拋出錯誤;現在它們已經可以正常運作了。