iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0

前幾天開發《異世界救援》時,我在畫面上遇到過幾次很像的問題:
卡片明明顯示正常,卻點不到。

後來加血條,又遇到一次。
再加上魔王頭像,同樣的問題又出現一次。

最後我在專案裡採用了一個很簡單的做法:

只要是疊在互動元件上、自己不需要接收滑鼠事件的裝飾節點,就明確設定 mouse_filter = IGNORE。

不再去記每一種 Godot 節點的預設值,也不再猜這次是不是剛好安全。

原本以為三次都是同一個問題。查下去之後發現:
三次看起來一樣,實際遇到的節點類型和原因並不完全相同。


一、第一次是卡框,第二次是血條

《異世界救援》的畫面越做越完整,也代表畫面上的節點越來越多。

第一次遇到問題,是 Card.tscn 加上卡片邊框之後。

畫面看起來完全正常,但卡片有時候按不下去。
後來戰鬥場景加入魔王與玩家的 HP 進度條,手牌又偶爾點不到。
再後來加入魔王頭像,同樣的問題又出現一次。

三次最後都和畫面上「有東西疊在互動元件上面」有關。

所以當時最容易留下的印象就是:

「上面有裝飾,下面的按鈕就被擋住了。」

而解法也很簡單:

mouse_filter = IGNORE

第一次修好之後,我其實就把這件事情記住了。
直到第三次又遇到同樣的問題,我才開始想:
如果我已經知道解法,為什麼還是一直遇到?


二、AI 給的第一個解釋,聽起來合理

第一次遇到問題時,我有問 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 時,還是可能影響手牌的點擊。


四、三次問題其實不完全一樣

查完預設值後,再回頭看之前三次問題,就比較有意思了。

1. 卡框:NinePatchRect

卡框使用的是 NinePatchRect。

查到的預設值其實就是:

IGNORE

也就是說,它本身預設就不應該攔截滑鼠事件。
所以第一次遇到的卡框問題,不能簡單解釋成:

「因為 NinePatchRect 預設會攔截。」

這個說法和實際查到的結果對不起來。
代表當時可能還有其他原因,例如複製其他節點時,把某些設定一起帶了過來。

這是第一次讓我發現:
看到相同症狀,不代表原因一定相同。


2. 血條:TextureProgressBar

血條使用的是 TextureProgressBar。

它的預設值是:

PASS

一開始看到 PASS,很容易理解成:

「它沒有處理滑鼠事件,應該就會傳下去吧?」

但實際上不是這樣。

PASS 是往父節點傳。

手牌按鈕和血條如果只是同一個場景裡的不同分支,它們不是父子關係。

所以畫面上雖然重疊,事件傳遞卻不會從血條直接「穿過去」找到另一個分支的按鈕。

因此這種情況下,還是會造成點擊問題。


3. 魔王頭像:TextureRect

魔王頭像使用的是 TextureRect。
它的預設值跟血條一樣,也是:

PASS

所以又遇到類似的情況。
畫面上看起來只是「多放了一張圖片」,結果卻讓原本可以點的手牌沒有反應。

這次我才真正把問題串起來:
三次都是「畫面上有東西重疊」,但節點類型和預設值並不一樣。

後來加入的 StatLabel 也讓我再次遇到這件事。
因為它需要顯示內嵌圖示,所以使用的是 RichTextLabel。
而它的預設值是:

STOP

如果沒有特別處理,玩家點在卡片中間的圖示或數字上,也可能遇到點擊被攔住的問題。


五、所以我最後決定不要再賭預設值

查完這些預設值之後,我最後沒有去背:

Label → IGNORE
TextureRect → PASS
TextureProgressBar → PASS
RichTextLabel → STOP

因為下次再遇到新的節點,我還是可能忘記。

最後我直接在專案裡訂了一個簡單規則:

只要是疊在互動元件上、自己不需要接收滑鼠事件的裝飾節點,一律明確設定 mouse_filter = IGNORE。

例如:

Label
RichTextLabel
NinePatchRect
TextureRect
TextureProgressBar

只要它的工作只是顯示東西,就把這件事情寫明白。

這樣做的好處是,下次新增節點時,我不用先猜:

「這個節點預設到底是什麼?」

直接看它的用途決定設定就好了。


六、這件事情也讓我重新看了一下 Card

同樣的想法,其實也出現在 Card 元件的設計裡。

現在 Card 只負責兩件事情:
透過 setup(data) 接收卡牌資料。
玩家點擊卡片時,透過 card_selected 訊號把結果送出去。

它不需要知道自己現在是在獎勵場景,還是戰鬥場景。
也不需要自己去讀 Global。

這一點在獎勵場景特別重要。
因為玩家開寶箱時,護甲通常是 0。

如果 Card 自己去讀 Global 算數字,那麼寶箱畫面上的防禦卡,就可能因為目前玩家護甲是 0,而顯示出不符合卡牌本身的內容。

所以現在的做法是:
外面把資料給 Card,Card 就負責顯示。
它不用知道自己為什麼會出現在這裡。


七、這次真正留下來的是什麼?

我以前記住的比較像是:「上次這樣改就好了。」
但應該是要想:「上次為什麼這樣改?」

因為如果只記得解法,看到類似問題時,很容易直接複製。
但不同節點的預設行為可能不同,節點之間的關係也可能不同。
所以同樣是「按不到」,背後的原因不一定相同。

之後的規則很簡單:
裝飾節點如果不需要接收滑鼠事件,就明確設定 IGNORE。

至於其他看起來很像的問題,我會先確認條件是不是一樣,再決定要不要套用以前的解法。
這比單純記住「上次加了哪一行」更有用。


明天 Day 14 又會遇到另一種防禦性設計。
這次是 duplicate(true)。
它是 Claude Code 主動寫進程式碼裡的,我一開始其實不知道為什麼需要這樣做。
直到我追問「為什麼?」之後,才發現它在兩個完全不同的地方,各自解決了不同的問題。


上一篇
Day 12:【程式碼驗證】程式能跑,真的就沒問題嗎?
下一篇
Day 14:【程式碼理解】同樣是 duplicate(true),保護的東西完全不同?
系列文
從 Vibe Coding 到系統架構:Gemini x Claude Code 雙 AI 協同開發 Godot 2D Roguelike 卡牌遊戲實戰 共 15 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言