iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0

Day 20 講的是抓漏,這篇換個方向:沒有真的壞掉的功能要修,主要是在處理遊戲的打磨。
目前出牌的流程很直接:按下卡片、血條立刻跳到新數值,畫面上除了日誌多一行之外,幾乎沒有其他回饋。

所以今天想處理操作回饋的問題:
讓出牌除了是結算攻防數字,而且讓玩家看得出來「打到了」。

設計過程也因此碰到幾個之前沒有遇過的問題:例如動畫和邏輯的先後順序、飄字的位置、Tween 同時修改同一個屬性、粒子系統和渲染模式,以及卡片縮放後的位置計算。


一、動畫不能影響戰鬥邏輯

原始的想法是先算好結果,再執行動畫:
戰鬥結果要在動畫開始之前就算完,動畫只負責把已經發生的結果呈現出來。

如果反過來,等動畫播完才扣血,會帶來兩個問題。

  • 第一,smoke test 如果沒有等待動畫,就可能測不到最後的結果。
  • 第二,玩家每出一張牌都要等動畫結束,操作會變得比較拖。

所以這條規則直接寫進程式碼註解裡。

實際測試後,加入動畫並沒有改變戰鬥結算的時間。玩家出牌之後,魔王的 HP、震動等狀態已經完成更新,動畫只是接著把這些變化表現出來。

這也讓後面的動畫比較單純:不用讓動畫自己負責遊戲規則,只需要處理「這件事情已經發生了,要怎麼讓玩家看見」。


二、傷害飄字改了三版

血條補間本身不複雜。比較需要處理的是血條上面的數字。如果只有血條慢慢減少,數字卻直接跳到新值,兩者在動畫進行期間就會對不起來,看起來反而像是畫面出錯。

所以這裡使用 tween_method,讓血條和數字一起更新。
真正花比較多時間的是受擊時的飄字。
前後總共改了三版。

第一版:從血條正上方往上飄

第一版很直覺。受到傷害之後,-10、-8 這類數字從血條附近往上移。

但是有兩個問題。

  • 第一,數字會和「黑曜石魔王」那一行文字重疊。
  • 第二,往上移之後,很容易接近畫面上緣,甚至被裁掉。

第二版:把文字做得更明顯

所以第二版沒有改方向,而是調整原本的設計:

  • 增加深色描邊
  • 放大字級
  • 拉長顯示時間
  • 縮短上升距離

這些調整確實改善了可讀性。但整體還是擠在原本的區域裡。
繼續調參數,能改善一些細節,卻沒有解決空間本身的問題。

第三版:改從血條右側走出去

第三版改換了方向。

重新看了一下戰鬥畫面,發現血條本身大約只有 420px 寬,而整個視窗是一千多 px。
血條左右其實有一大片沒有使用的空間。

前兩版一直在處理血條「上面」的空間,所以一直和文字搶位置。
這次改成讓受擊數字從血條右側飛出去。

這個位置沒有其他重要文字,也比較不容易被畫面邊界裁掉,而且仍然靠近血條,玩家很容易把數字和剛才的攻擊連在一起。

軌跡也從直線改成拋物線。
前段往上彈,後段再往下落,落下的同時慢慢淡出。
連續兩次受到攻擊時,兩個飄字也不再左右排列,而是上下錯開:

戰鬥畫面截圖,魔王血條右上方同時有兩個受擊數字「-10」與「-8」,上下錯開沒有疊在一起

前兩版都是在同一個方向裡調整。
第三版則是重新看了一次整個畫面的空間,考慮使用右側那一大片空白。
如果同一個方向一直調都沒有改善,有時候可以先停下來重新看一次整個畫面,而不是繼續增加參數。


拋物線又遇到 Tween 互相覆蓋

拋物線的第一版,Claude Code 把 x 和 y 拆成兩條 Tween:
一條負責補間 x,另一條負責補間 y。
實際跑起來,上升高度只有設定值的大約八成。
後來才發現問題在 global_position。

它是一個 Vector2,當 Tween 分別修改 x、y 時,實際上會變成:

  1. 讀出整個 Vector2
  2. 修改其中一個分量
  3. 再把整個 Vector2 寫回去

如果兩條 Tween 在同一幀都這樣做,後寫入的結果就可能把前一條剛改好的值蓋掉。
最後改成只使用一條 tween_method,由自己計算拋物線的位置,再一次寫入完整的 Vector2。

這樣就沒有兩條 Tween 同時修改同一個屬性的問題。
這個寫法後來也用在血條震動和卡片飛行上。


三、卡片打出動畫:攻擊卡飛去打魔王

原本的想法很簡單:所有卡片打出去之後,都往上飄,然後消失。
後來在和 AI 討論動畫時,我提出一個比較具體的想像:

攻擊卡牌是武器,會飛去打魔王。

於是卡片動畫開始依照類型分流。

攻擊卡:

  • 斬擊
  • 盾牌衝鋒
  • 重擊
  • 破甲打擊

會飛向魔王頭像。

飛行使用 EASE_IN,讓卡片越接近魔王時速度越快,看起來比較像被吸過去。
淡出則只放在最後一小段時間,不讓卡片從一開始就慢慢變透明。

防禦和增益類型的卡片沒有特定攻擊目標,就維持原本往上飄並淡出的方式。
卡片離開手牌列這件事則保持原本的節奏。

被打出的卡片開始飛之後,其他手牌立即補位,不等待飛行動畫結束。
這樣玩家手上的牌不會因為一張卡正在飛,就整排停住。

傷害與震動也沒有放進飛行動畫裡。
它們在呼叫端就已經完成結算,卡片飛行只是最後的視覺表現。

後來又遇到一個細節:卡片飛行時能不能慢慢縮小?
於是再加入從原尺寸縮小到 0.15 倍的動畫,縮放中心設定在卡片中央。
這裡直接沿用了 RewardScene 選卡飛行時已經處理過的做法,沒有重新摸索一次。

不過這裡也因此帶出另一個更深的問題:當位置和縮放同時變化時,原本用來計算目標位置的公式還成立嗎?
這個問題後來解決的方式請參考附錄。


四、打中的粒子特效:先查證再動手

接著開始處理「卡片砸中魔王」的效果。
我問,有沒有辦法做出卡片打到魔王之後,四散飛濺的粒子。

這裡先確認 Godot 對 2D 粒子的建議。
Godot 官方文件對 2D 粒子的建議是,除非有明確理由,否則優先使用 GPUParticles2D。

但這個專案有一個特殊條件:project.godot 已經固定使用 Compatibility 渲染模式。而這個專案又需要 Web 匯出。

查證後發現,Compatibility 渲染器不支援 compute shader,因此 GPUParticles2D 在這個專案的條件下不能正常使用。

所以這裡就不能只看到官方文件寫「推薦 GPUParticles2D」,就直接照做。
這個專案最後選的是 CPUParticles2D。

另外也查到一則 Godot 4.3 的紀錄:當時 CPUParticles2D 在 Web 匯出、沒有指定貼圖時,預設的粒子顯示方式也有問題。

這個專案使用的是 Godot 4.7.1,因此不能直接把舊版本的問題當成現在一定存在。
但在還沒有實際跑過 Web 匯出之前,也不適合假設它一定沒問題。

所以最後沒有依賴粒子的預設畫法,而是在執行時自己產生一張純色貼圖,再交給粒子使用。
這裡的選擇不是「官方建議錯了」。
比較準確地說,是官方文件提供的是一般情況下的建議,而這個專案剛好有一個額外條件,讓一般建議不適用。
所以實際開發時,還是得把自己的專案設定一起放進來查。


五、魔王死亡特效:兩版迭代

打贏魔王之後,原本只有文字和按鈕發生變化。
魔王立繪沒有設計變化反應。
結果畫面上會同時出現「倒下」之類的文字,但魔王看起來還站在原地。

這和 Day 20 遇到的問題有點像:資料狀態已經改變,但畫面沒有把這個變化表現出來。
所以這次也加入了死亡演出。

第一版:爆散粒子 + 立繪淡出

第一版是在顯示死亡文字之後,固定等待 0.5 秒。
這個時間比卡片飛行動畫稍微長一點,讓致命一擊的粒子和死亡效果有一小段重疊。

接著播放一批爆散粒子,再讓魔王立繪淡出。
實際看過之後,發現這個效果消失得太快。
而且想要的感覺並不是「東西往外爆開」,而比較像魔力慢慢在魔王身上消散。

第二版:讓魔力在身上閃爍

第二版乾脆換掉原本的粒子效果。
不再一次噴出一批粒子,而是隨時間持續產生,一顆一顆隨機出現在魔王頭像附近。

粒子的初速接近零,也不使用重力。
它們不是飛出去,而是在原地短暫閃一下。

真正讓效果看起來像「閃爍」的,是粒子透明度的變化:

透明 → 變亮 → 再透明。

如果只是固定顏色突然出現,再突然消失,看起來會比較像一顆東西被硬塞進畫面。
加入亮度變化之後,才比較接近「魔力正在消散」的感覺。

四格連續截圖:第一格是致命一擊打中的金色粒子,第二、三格換成紫色魔力粒子在身上閃爍,第四格立繪整體淡出,最後幾乎完全消失於黑暗中

這四格其實包含三層效果。

  • 第一格是致命一擊當下的金色粒子,和前面介紹的打中特效是同一批。
  • 第二、三格是死亡特效本身,換成紫色魔力粒子在身上閃爍。
  • 第四格則是魔王立繪整體淡出。

這三種效果沒有全部同時發生,而是按照時間順序疊起來。

另外,死亡粒子直接掛在魔王頭像節點底下。
這樣做的好處是,頭像往上飄、淡出的時候,底下的粒子會跟著父節點一起移動和變化。
不用另外寫一套程式,不斷追蹤頭像目前的位置和透明度。
這種情況交給 Godot 的節點階層處理,就已經足夠。


六、這批打磨共同的限制:headless 能測什麼?

這一輪打磨有一個很明確的限制。

這幾天 Claude Code 常用 headless 模式讓 Godot 在沒有開啟遊戲視窗的情況下直接執行。簡單來說,就是讓 Godot 在背景跑程式,不需要真的把遊戲畫面顯示出來。

因此,時序、座標計算、數值和部分動畫公式,可以用 headless 驗證。

但「看起來順不順」沒有辦法靠 headless 判斷。
例如:

  • 飄字的速度是不是太快
  • 粒子的數量會不會太多
  • 震動是不是太明顯
  • 卡片飛行的速度是否自然
  • 顏色和背景是否容易混在一起

這些最後都還是要真的把遊戲畫面跑出來看。

這和前幾天的測試方式不太一樣。

  • Day 08 建立的 --headless --import,可以確認整個 Godot 專案能不能正常載入。
  • Day 09 做的 --check-only,可以檢查程式碼的語法。
  • Day 10 則加入 Smoke test,讓遊戲真的跑一次主要流程。

但這些測試都不能替代「人看畫面」。

所以這一輪我能自動化確認的是:
程式的時間軸、座標和邏輯有沒有照預期執行。

至於動畫本身是不是看起來自然,還是需要實際看到畫面之後才能判斷。
這也是這批打磨裡,自動化測試能做到的範圍。


最後

以上把原本已經可以運作的東西,再往前推一點,加上回饋的設計。

  • 出牌之後,血條會動。
  • 傷害數字有自己的位置和軌跡。
  • 攻擊卡會飛向魔王。
  • 命中時有粒子。
  • 魔王死亡時,立繪和魔力效果也會跟著變化。

而在這些動畫背後,也陸續遇到一些程式本身的問題:Tween 同時修改 Vector2、粒子系統和渲染模式的關係,以及 pivot_offset 和 scale 同時改變造成的目標位置偏移。

這一輪也讓我更熟悉 headless 測試的範圍。它可以幫我確認很多程式上的事情,但最後畫面看起來怎麼樣,還是得真的看到畫面才知道。

Day 22,繼續抓 Bug。

這次的問題比較特別:一句「輸入減輸出」的推理看起來完全合理,最後卻發現護甲根本沒有擋下那一點傷害。


💬 你如果也有用 AI 幫忙做遊戲打磨,可以試著問它:「這個動畫如果同時改變 position 和 scale,目標位置的計算需要注意什麼?」看看它會不會一起檢查 pivot_offset。

附錄:pivot_offset 疊 scale 動畫的通用坑

前面提到,卡片飛向魔王時會同步縮小到 0.15 倍。
實際加入之後,我回報了一個新的問題:
卡片明明是往魔王頭像中心飛,最後卻停在右上方。

螢幕錄影截圖,實線箭頭指向飛行動畫終點那張半透明小卡片,落在魔王頭像右上方;虛線箭頭指向紅框標出的魔王實際視覺中心,兩者明顯對不上

原本的目標位置計算,假設:

卡片中心 = global_position + 半個尺寸

在 scale = 1 的時候,這個算法沒有問題。

但這次卡片同時在縮小。
所以動畫結束時,scale 已經不是 1,而是 0.15。
Claude Code 另外做了一個臨時場景,直接用 Godot 算出實際的視覺中心,再和預期位置比較。
最後確認到:

視覺中心 = global_position + pivot_offset × scale

原本的 global_position 目標,是按照 scale = 1 算出來的。
但動畫結束時,實際 scale = 0.15。
因此兩個值會產生偏移。
偏移量就是:

pivot_offset × (1 - 0.15)

以這張卡片的尺寸來看,大約是 90 × 115px,和實際截圖看到的偏移量級接近。

最後把目標位置改成:

var target: Vector2 = (
    boss_head.global_position + boss_head.size * 0.5
    - card_node.size * 0.5 * CARD_PLAY_FLY_END_SCALE
)

修好之後,我又主動檢查同一套寫法是不是在其他地方也存在。
結果找到兩個:

  • RewardScene:選卡飛向牌組數字
  • 祭壇卡片:飛向日誌

三個地方使用了同一類型的「位置和縮放同時補間」邏輯。
所以三處一起修改。
修改後再用 headless 逐格量測,原本大約 85~110px 的誤差,最後縮小到個位數。

這個問題其實不是今天才出現。
第一次使用同一套寫法時,偏移量還沒有明顯到會被注意。
直到這次卡片縮小到 0.15 倍,偏移被放大到肉眼很容易看見,才真的被抓出來。

所以這次最後留下來的,不只是那個公式。而是之後只要看到「飛向固定位置,同時又改變 scale」的動畫,就要一起檢查 pivot_offset 和縮放比例,而不能直接假設卡片中心永遠等於 global_position + 半個尺寸。



上一篇
Day 20:【真人測試】同一顆按鈕,兩次「看得到卻按不到」的原因完全不同
下一篇
Day 22:【抓蟲實錄】溢殺被誤報成「護甲擋下」
系列文
從 Vibe Coding 到系統架構:Gemini x Claude Code 雙 AI 協同開發 Godot 2D Roguelike 卡牌遊戲實戰 共 26 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言