🖱️

Godot Engine の mouse_filter を理解する

2024/08/24に公開

概説

Godot EngineでUIを作ろうとしたことがある人なら、一度はノードやシーンにまつわる挙動に戸惑ったことがあると思います。

戸惑う原因は人によって様々だとは思いますが、例えば「シーンエディタでノードの順番を入れ替えたら、ボタンが反応しなくなった」は、ポピュラーな部類でしょう。

この記事では、UIの設計の際に使われることが多いプロパティ、mouse_filterについて調べていきます。公式ドキュメントにあたりつつ、実際に動かしながら理解を深めるのが目的です。

検証の様子をGIFでキャプチャしているので、画像だけでも是非チェックしてみてください。

この記事の対象者

この記事の対象者

  • ノードを重ねたらボタンが押せなくなって困ったことがある人
  • mouse_filter、特にMOUSE_FILTER_PASS(Pass)の挙動がよくわからない人
  • だけど、自分で検証するのは面倒くさい人

必要な前提知識

この記事はGodotについて以下の知識があれば理解できるように書いています。

  • ノードやシーンツリーの親子関係、兄弟関係について理解している
  • Control系ノード、Node2D系のノードの違いがなんとなくわかる
  • mouse_filterを初めて聞いた」という人でも読めるはずです


ここでmouse_filterが変更できることを覚えていれば読めます

mouse_filterについて調べてみよう

最初にmouse_filterとは何かを、実例を挙げて説明します。

mouse_filterってなに?

Control系のノードを使ってUIを実装する際、「ノードが前後に重なる状況」はよく発生します。


前後に重なったControl系ノードのイメージ。ColorRectが左のボタンの手前にある

例えば上の画像ではColorRectが左のボタンの手前にあります。そのため左のボタンはクリックなどの入力を検知できません。

左のボタンが入力を検知できるようにするために設定するのが、mouse_filterプロパティです。

上の画像の場合であれば、ColorRectmouse_filterIgnoreに設定すれば、左のボタンはマウス入力を検知できるようになります。

では、mouse_filterはノードの前後に入力を振り分けるためのプロパティなのでしょうか?

公式ドキュメントでmouse_filterを調べてみる

公式ドキュメントで、mouse_filterについて調べてみましょう。

公式ドキュメントのmouse_filterの項目を見てみると、mouse_filterは以下の要素を設定できるプロパティとあります。

  • _gui_input()を通じて、マウスのボタン入力を受け取るかどうか
  • mouse_enteredmouse_exitedのシグナルを受け取るかどうか

先ほどのColorRectButtonの手前にある例もですが、なんとなく「mouse_filterは、UI要素が前後に重なった際の入力を振り分けるためもの」と理解するのが一番直感的で簡単です。しかし実は公式ドキュメントのmouse_filterに関する記述で、「ノードの前後関係」についての言及している箇所は(探した限りでは)見つかりません。

mouse_filterはあくまで「Controlが入力を受け取るかどうかを指定するもの」で、その結果としてUI要素が重なった時に入力を前後のノードに振り分けることができる、と捉えるのがより正確そうです。

さらに詳しく見ていきましょう。

mouse_filterに設定できる値

ここからはmouse_filterに設定できる値とそれぞれの役割を、公式ドキュメントを参照しながら具体的に見ていきましょう。

  • MouseFilter.MOUSE_FILTER_STOP
    デフォルトの値。_gui_inputを通して、マウス移動やマウスボタン入力イベントを受け取ることができます。また、mouse_enteredmouse_exitedシグナルも受け取ります。これらのイベントは自動的に「処理済み」とマークされ、他のControlに伝搬することはありません。また、他のControlによるシグナルの発火も妨げられます。

  • MouseFilter.MOUSE_FILTER_PASS
    マウス入力やmouse_enteredなどのシグナルを受け取るところまではMOUSE_FILTER_STOPと同じです。ただ、この値を設定されたControlが入力イベントをハンドルしない場合、そのイベントを処理する可能性のあるControlが見つかるまでツリーを遡って、祖先のControlにシグナルやイベントを伝播します。

  • MouseFilter.MOUSE_FILTER_IGNORE
    この値が設定されたControlは、マウス移動やマウスボタン入力イベントを_gui_inputを通して受け取ることができなくなります。同様にmouse_enteredmouse_exitedシグナルも受け取りません。また、他のControlがこれらの入力イベントを受け取ったり、シグナルを発火したりするのを妨げることもありません。

Godotをしばらく触ったことがある人であれば、上に挙げた公式ドキュメントの内容はなんとなく理解できると思います。が、敢えて分かりやすくするなら、以下のように言い換えることができると思います。

  • MOUSE_FILTER_STOPは、_gui_inputの入力を独占し、他のノードに渡さない。
  • MOUSE_FILTER_PASSは、_gui_inputの入力を受け付けるし、祖先のノードに入力を伝播させることがある。
  • MOUSE_FILTER_IGNOREは、_gui_inputの入力を無視し、あたかもそこに無いかのように振る舞う。

注意したいのが、MOUSE_FILTER_PASSです。唯一、ツリーの親子関係がその挙動に関わってくることが言及されています。個人的には、ここがmouse_filterの理解でつまずきやすく、GodotのUI設計の際に戸惑いがちなポイントでした。

MOUSE_FILTER_PASSPASSする相手

MOUSE_FILTER_STOPMOUSE_FILTER_IGNOREは、背後のノードが入力を検知するかどうかを指定するために使う場合が多いです。例えば、MOUSE_FILTER_IGNOREを使えば、背後に隠れているControlノードにマウス入力をパスすることができます。

しかし、同じ感覚でMOUSE_FILTER_PASSを使うことはできません。MOUSE_FILTER_PASSは確かに入力を受け取りつつPASSもします。しかしPASSする相手は背後に隠れているノードではなく、祖先なのです。

次のセクションからは、実際にUIを配置して動かしつつmouse_filterを理解していきます。


✅ 1. ボタン同士が兄弟で、前後に重なっている場合

まずは一番簡単な状況からテストしてみましょう。

兄弟関係にあるButton (TOP)Button (BOTTOM)が配置されています。z軸上はButton (TOP)が手前です。

シーンの構成は以下の通りです。

シーン構成の抜粋
Control
├─ Button (BOTTOM)
└─ Button (TOP)


Buttonのレイアウトはこの通りです。前後のボタンが一部重なっています。

Button (TOP)mouse_filterの設定を変更しつつ、動作を検証していきましょう。

1.1 MOUSE_FILTER_STOP

前後にあるボタンがそれぞれ個別に入力イベントを処理していることがわかります。ボタン同士が前後に重なっている箇所では、前にあるButton (TOP)が入力を受け付けています。

1.2 MOUSE_FILTER_PASS

MOUSE_FILTER_STOP同様に、それぞれのボタンは個別の入力イベントに反応しています。

また、前後のボタンが重なっている部分の入力イベントは、Button (TOP)が処理しています。1.1と同じ挙動ですね。

MOUSE_FILTER_PASSは、後ろの要素にも入力を渡す」わけではないのが、見てわかります。

1.3 MOUSE_FILTER_IGNORE

前にあるボタンが「ないもの」として扱われているように挙動することがわかります。
ボタンが重なっている箇所では、奥にあるボタンが入力を処理していますね。


✅ 2. 重なってるボタンが、親子関係にある場合

次は、重なっているボタンが親子関係にある場合です。シーンの構成は以下の通りです。

シーン構成の抜粋
Control
└─ Button (BOTTOM)
   └─ Button (TOP)

今回はButton (TOP)MOUSE_FILTER_PASSの場合のみをみていきます。

2.1 親子で重なっている場合

親子がz軸上で少し重なっている状態で、実際に動作する様子を見てみましょう。

ドキュメントの通り、子であるButton (TOP)への入力イベントは、親であるButton (BOTTOM)に伝播しました。子をクリックした場合は、どちらのボタンもpressed状態になります。

2.2 親と子が重なっていない場合

念の為、親と子のノードがz軸で重なっていない場合も見てみましょう。


2.1と挙動に違いはありません。Button同士の前後の重なり方に関わらず、MOUSE_FILTER_PASSがツリーを遡って入力を伝播することがわかります。


✅ 3. ボタンが親・子・孫の関係にある場合

続いては「親・子・孫」と、入れ子になっている場合の挙動を確認してみます。シーンの構成としてはこのような感じです。

シーン構成の抜粋
Control
└─ Button (BOTTOM)
   └─ Button (MIDDLE)
      └─ Button (TOP)

今回もButton (TOP)MOUSE_FILTER_PASSに固定します。その代わり、子であるButton (MIDDLE)の設定を色々試してみましょう。

3.1 子要素がMOUSE_FILTER_STOPの場合


子要素がMOUSE_FILTER_STOPの場合の挙動

子要素がMOUSE_FILTER_STOPの場合、孫に対する入力イベントは、子要素で止まります。

子孫のMOUSE_FILTER_PASSは、MOUSE_FILTER_STOPが設定されているノードまで遡って、入力イベントを渡すということですね。

3.2 子要素がMOUSE_FILTER_PASSの場合


子要素がMOUSE_FILTER_PASSの場合の挙動

ドキュメント通り、入力イベントは親のノードまでツリーを遡りました。
孫と子がz軸上で重なっていなくても入力が伝播するのは、2.で確認した時と同じですね。

3.3 子要素がMOUSE_FILTER_IGNOREの場合


子要素がMOUSE_FILTER_IGNOREの場合の挙動

子要素がMOUSE_FILTER_IGNOREの場合、孫に対する入力イベントは子要素を無視し、親にまで伝播しているのがわかります。IGNOREは入力がツリーを遡ることを「妨げない」ことがわかります。


✅ 4. 途中にNode2Dがある場合

ここまででMOUSE_FILTER_PASSがツリーを遡る挙動は、おおよそつかめました。それでは、途中にControl系ではなく、Node2D系のノードがあったらどうなるでしょうか?

際ほどと同じく親・子・孫と、入れ子になっています。ただし、子要素はNode2Dです。この場合の挙動を確認してみます。シーンの構成としてはこのような感じ。

シーン構成の抜粋
Control
└─ Button (BOTTOM)
   └─ Node2D
      └─ Button (TOP)


見えないですが、親と孫の間にはNode2Dがあります

孫のButton (TOP)MOUSE_FILTER_PASSにします。

Node2Dを突き抜けて、入力イベントが親まで伝播されることがわかります。3.3で子要素をMOUSE_FILTER_IGNOREにしたと同じ挙動です。意外に感じる方もいるかもしれません。


✅ 5. 直接の親子関係じゃない場合

今度はノード同士が直接の親子関係にない場合の挙動を見ていきます。

シーンの構成は以下の通りです。

シーン構成の抜粋
Control
├─ Button (BOTTOM)
└─ ColorRect
   └─ Button (TOP)

Button (TOP)ColorRectはどちらもMOUSE_FILTER_PASSとしています。

Button (TOP)の入力イベントはツリーを遡りますが、親のColorRectと兄弟関係にあるButton (BOTTOM)にまでは伝播しないことがわかります。また、ColorRectに対する入力も、Button (BOTTOM)に伝播しません。


✅ 6. マウス以外の入力の場合

最後に、2.1と同じくノードが親子の状況に戻ってみます。ただし、入力するのはキーボードからです。

シーンの構成は以下の通りです。

シーン構成の抜粋
Control
└─ Button (BOTTOM)
   └─ Button (TOP)

6.1 子要素がMOUSE_FILTER_PASSの場合

まずは2.1と同じように、Button (TOP)MOUSE_FILTER_PASSの場合をみていきます。

マウス入力と違って、キー入力はツリーを遡らないことがわかります。
MOUSE_FILTER_PASSを使った場合、マウス入力とキーやゲームパッド入力では挙動に違いがあることに気をつける必要がありますね。

6.2 子要素がMOUSE_FILTER_IGNOREの場合

今後は、Button (TOP)MOUSE_FILTER_IGNOREの場合です。

Button (TOP)はマウス入力を一切無視するものの、キーボードを使ってフォーカスを当てたり、ボタンを押したりすることができます。なお、キーボードでフォーカスを移した状態でマウスクリックしても反応しません。

MOUSE_FILTER_PASSの時と同様に、マウス入力とキーやゲームパッド入力では挙動が違うことがわかります。

おわり

ひとまず、mouse_filterの動作検証は以上です。

mouse_filterは、シーンツリー全体を意識しながら使う必要があることがわかりました。ノードの前後の重なりだけを意識すると、特にMOUSE_FILTER_PASSの挙動に戸惑うかもしれません。

またmouse_filterやツリーの設計次第ではマウスと他の入力デバイスとで挙動が変わってしまうところも、初見では分かりづらく注意が必要なポイントですね。

まとめ

mouse_filterを使って開発する際には以下の点に気をつけましょう。

  • シーンツリーの構造を把握して使う
  • もしくはmouse_filterに合わせてシーンツリーを設計する
  • マウスと、それ以外のデバイスの挙動の違いを把握すること

まだまだ触れてない要素もありますが、今回はここまでにしたいと思います。


おまけ

ご自分の環境でもmouse_filterなどの動作をチェックされたい方のために、今回使用したコードを共有します。検証がしやすいように、デバッグコンソールへのテキスト出力もします。

以下のような機能を持つButton継承クラスのコードです。基本的には、この継承クラスをシーンに配置するだけで動くと思います。

  • ボタンを押すと、ノード名が出力される
  • ボタンを押すと、チェックマークがハイライトされる(フォントはNoto Emoji推奨)
  • ボタンの上にマウスカーソルが乗ると、ノード名が出力される
  • ボタンの外にマウスカーソルが外れると、ノード名が出力される

コードは以下の通りです。

ボタンのスクリプト
extends Button
class_name ExtButton


func _ready():
    # ノード名とチェックの絵文字を、ボタンに表示する
    self.text = self.name + " ☑️"
    self.pressed.connect(
        func():
            # ボタンが押されたら、ノード名をコンソールに出力する
            print("%sが押されました" % self.name)
    )
    
    self.button_down.connect(
        func():
            # ボタンが押されたら、チェックの絵文字をハイライト
            self.text = self.text.replace(" ☑️", " ✅")
    )

    self.button_up.connect(
        func():
            # ボタンが離されたら、チェックの絵文字を書き換える
            self.text = self.text.replace(" ✅", " ☑️")

    )

    self.mouse_entered.connect(
        # マウスがボタンの上に乗ったら、コンソールに出力
        func ():
            print("mouse entered to %s" % self.name)
    )

    self.mouse_exited.connect(
        # マウスがボタンの外に出たら、コンソールに出力
        func ():
            print("mouse exited from %s" % self.name)
    )

Discussion