前幾天開發《異世界救援》時,我在畫面上遇到過幾次很像的問題:
卡片明明顯示正常,卻點不到。
後來加血條,又遇到一次。
再加上魔王頭像,同樣的問題又出現一次。
最後我在專案裡採用了一個很簡單的做法:
只要是疊在互動元件上、自己不需要接收滑鼠事件的裝飾節點,就明確設定
mouse_filter = IGNORE。
不再去記每一種 Godot 節點的預設值,也不再猜這次是不是剛好安全。
原本以為三次都是同一個問題。查下去之後發現:
三次看起來一樣,實際遇到的節點類型和原因並不完全相同。
《異世界救援》的畫面越做越完整,也代表畫面上的節點越來越多。
第一次遇到問題,是 Card.tscn 加上卡片邊框之後。
畫面看起來完全正常,但卡片有時候按不下去。
後來戰鬥場景加入魔王與玩家的 HP 進度條,手牌又偶爾點不到。
再後來加入魔王頭像,同樣的問題又出現一次。
三次最後都和畫面上「有東西疊在互動元件上面」有關。
所以當時最容易留下的印象就是:
「上面有裝飾,下面的按鈕就被擋住了。」
而解法也很簡單:
mouse_filter = IGNORE
第一次修好之後,我其實就把這件事情記住了。
直到第三次又遇到同樣的問題,我才開始想:
如果我已經知道解法,為什麼還是一直遇到?
第一次遇到問題時,我有問 AI 為什麼。
當時得到的說法大概是:
Control 節點預設會吃掉滑鼠事件。如果裝飾節點疊在按鈕上面,裝飾節點把點擊攔下來,底下的按鈕就收不到。
這個解釋聽起來很合理。
裝飾如果把滑鼠事件攔走,下面的按鈕當然收不到。
所以我當時就接受了。
但第三次再遇到相同問題時,我開始覺得這個說法太籠統。
到底所有 Control 都會攔截滑鼠事件嗎?
如果答案不是,那我之前記住的其實只是一個太簡單的結論。
所以這次我沒有直接再加一行 IGNORE,而是用 Godot 的 ClassDB 查幾個常用節點的預設值。
extends SceneTree
func _initialize() -> void:
for cls in ["Control", "Button", "Label", "RichTextLabel",
"NinePatchRect", "TextureRect", "TextureProgressBar"]:
print(
cls,
" 預設 mouse_filter = ",
ClassDB.class_get_property_default_value(
cls,
"mouse_filter"
)
)
quit()
結果裡有幾個數字很值得注意:
Button STOP
Label IGNORE
RichTextLabel STOP
NinePatchRect IGNORE
TextureRect PASS
TextureProgressBar PASS
這時我才發現:
「Control 節點預設會吃掉滑鼠事件」這句話其實太籠統。
不同節點的預設值根本不一樣。
mouse_filter 到底在做什麼?Godot 的 mouse_filter 有三種主要設定:
STOP這個節點會接收滑鼠事件。
如果事件在這裡被處理,就不會繼續往外傳。
PASS這個節點可以接收到事件。
如果自己沒有處理,事件可以繼續往父節點傳。
IGNORE這個節點直接忽略滑鼠事件。
所以如果一個節點只是拿來顯示裝飾,本身完全不需要知道玩家有沒有點它,設定成 IGNORE 就很直接。
但這裡有一個容易誤會的地方:PASS 並不代表「滑鼠會穿過這個節點,直接點到下面的按鈕」。
它傳遞的方向是節點樹裡的父節點。
而畫面上兩個東西就算重疊,也不代表它們是父子關係。
這也是為什麼血條和魔王頭像使用 PASS 時,還是可能影響手牌的點擊。
查完預設值後,再回頭看之前三次問題,就比較有意思了。
NinePatchRect卡框使用的是 NinePatchRect。
查到的預設值其實就是:
IGNORE
也就是說,它本身預設就不應該攔截滑鼠事件。
所以第一次遇到的卡框問題,不能簡單解釋成:
「因為
NinePatchRect預設會攔截。」
這個說法和實際查到的結果對不起來。
代表當時可能還有其他原因,例如複製其他節點時,把某些設定一起帶了過來。
這是第一次讓我發現:
看到相同症狀,不代表原因一定相同。
TextureProgressBar血條使用的是 TextureProgressBar。
它的預設值是:
PASS
一開始看到 PASS,很容易理解成:
「它沒有處理滑鼠事件,應該就會傳下去吧?」
但實際上不是這樣。
PASS 是往父節點傳。
手牌按鈕和血條如果只是同一個場景裡的不同分支,它們不是父子關係。
所以畫面上雖然重疊,事件傳遞卻不會從血條直接「穿過去」找到另一個分支的按鈕。
因此這種情況下,還是會造成點擊問題。
TextureRect魔王頭像使用的是 TextureRect。
它的預設值跟血條一樣,也是:
PASS
所以又遇到類似的情況。
畫面上看起來只是「多放了一張圖片」,結果卻讓原本可以點的手牌沒有反應。
這次我才真正把問題串起來:
三次都是「畫面上有東西重疊」,但節點類型和預設值並不一樣。
後來加入的 StatLabel 也讓我再次遇到這件事。
因為它需要顯示內嵌圖示,所以使用的是 RichTextLabel。
而它的預設值是:
STOP
如果沒有特別處理,玩家點在卡片中間的圖示或數字上,也可能遇到點擊被攔住的問題。
查完這些預設值之後,我最後沒有去背:
Label → IGNORE
TextureRect → PASS
TextureProgressBar → PASS
RichTextLabel → STOP
因為下次再遇到新的節點,我還是可能忘記。
最後我直接在專案裡訂了一個簡單規則:
只要是疊在互動元件上、自己不需要接收滑鼠事件的裝飾節點,一律明確設定
mouse_filter = IGNORE。
例如:
Label
RichTextLabel
NinePatchRect
TextureRect
TextureProgressBar
只要它的工作只是顯示東西,就把這件事情寫明白。
這樣做的好處是,下次新增節點時,我不用先猜:
「這個節點預設到底是什麼?」
直接看它的用途決定設定就好了。
同樣的想法,其實也出現在 Card 元件的設計裡。
現在 Card 只負責兩件事情:
透過 setup(data) 接收卡牌資料。
玩家點擊卡片時,透過 card_selected 訊號把結果送出去。
它不需要知道自己現在是在獎勵場景,還是戰鬥場景。
也不需要自己去讀 Global。
這一點在獎勵場景特別重要。
因為玩家開寶箱時,護甲通常是 0。
如果 Card 自己去讀 Global 算數字,那麼寶箱畫面上的防禦卡,就可能因為目前玩家護甲是 0,而顯示出不符合卡牌本身的內容。
所以現在的做法是:
外面把資料給 Card,Card 就負責顯示。
它不用知道自己為什麼會出現在這裡。
我以前記住的比較像是:「上次這樣改就好了。」
但應該是要想:「上次為什麼這樣改?」
因為如果只記得解法,看到類似問題時,很容易直接複製。
但不同節點的預設行為可能不同,節點之間的關係也可能不同。
所以同樣是「按不到」,背後的原因不一定相同。
之後的規則很簡單:
裝飾節點如果不需要接收滑鼠事件,就明確設定 IGNORE。
至於其他看起來很像的問題,我會先確認條件是不是一樣,再決定要不要套用以前的解法。
這比單純記住「上次加了哪一行」更有用。
明天 Day 14 又會遇到另一種防禦性設計。
這次是 duplicate(true)。
它是 Claude Code 主動寫進程式碼裡的,我一開始其實不知道為什麼需要這樣做。
直到我追問「為什麼?」之後,才發現它在兩個完全不同的地方,各自解決了不同的問題。