Godot Engine の mouse_filter を理解する
概説
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
プロパティです。
上の画像の場合であれば、ColorRect
のmouse_filter
をIgnore
に設定すれば、左のボタンはマウス入力を検知できるようになります。
では、mouse_filter
はノードの前後に入力を振り分けるためのプロパティなのでしょうか?
mouse_filter
を調べてみる
公式ドキュメントで公式ドキュメントで、mouse_filter
について調べてみましょう。
公式ドキュメントのmouse_filter
の項目を見てみると、mouse_filter
は以下の要素を設定できるプロパティとあります。
-
_gui_input()
を通じて、マウスのボタン入力を受け取るかどうか -
mouse_entered
やmouse_exited
のシグナルを受け取るかどうか
先ほどのColorRect
がButton
の手前にある例もですが、なんとなく「mouse_filter
は、UI要素が前後に重なった際の入力を振り分けるためもの」と理解するのが一番直感的で簡単です。しかし実は公式ドキュメントのmouse_filter
に関する記述で、「ノードの前後関係」についての言及している箇所は(探した限りでは)見つかりません。
mouse_filter
はあくまで「Control
が入力を受け取るかどうかを指定するもの」で、その結果としてUI要素が重なった時に入力を前後のノードに振り分けることができる、と捉えるのがより正確そうです。
さらに詳しく見ていきましょう。
mouse_filter
に設定できる値
ここからはmouse_filter
に設定できる値とそれぞれの役割を、公式ドキュメントを参照しながら具体的に見ていきましょう。
-
MouseFilter.MOUSE_FILTER_STOP
デフォルトの値。_gui_input
を通して、マウス移動やマウスボタン入力イベントを受け取ることができます。また、mouse_entered
とmouse_exited
シグナルも受け取ります。これらのイベントは自動的に「処理済み」とマークされ、他のControl
に伝搬することはありません。また、他のControl
によるシグナルの発火も妨げられます。 -
MouseFilter.MOUSE_FILTER_PASS
マウス入力やmouse_entered
などのシグナルを受け取るところまではMOUSE_FILTER_STOP
と同じです。ただ、この値を設定されたControl
が入力イベントをハンドルしない場合、そのイベントを処理する可能性のあるControl
が見つかるまでツリーを遡って、祖先のControl
にシグナルやイベントを伝播します。 -
MouseFilter.MOUSE_FILTER_IGNORE
この値が設定されたControl
は、マウス移動やマウスボタン入力イベントを_gui_input
を通して受け取ることができなくなります。同様にmouse_entered
とmouse_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_PASS
がPASS
する相手
MOUSE_FILTER_STOP
やMOUSE_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
の設定を変更しつつ、動作を検証していきましょう。
MOUSE_FILTER_STOP
1.1 前後にあるボタンがそれぞれ個別に入力イベントを処理していることがわかります。ボタン同士が前後に重なっている箇所では、前にあるButton (TOP)
が入力を受け付けています。
MOUSE_FILTER_PASS
1.2 MOUSE_FILTER_STOP
同様に、それぞれのボタンは個別の入力イベントに反応しています。
また、前後のボタンが重なっている部分の入力イベントは、Button (TOP)
が処理しています。1.1と同じ挙動ですね。
「MOUSE_FILTER_PASS
は、後ろの要素にも入力を渡す」わけではないのが、見てわかります。
MOUSE_FILTER_IGNORE
1.3 前にあるボタンが「ないもの」として扱われているように挙動することがわかります。
ボタンが重なっている箇所では、奥にあるボタンが入力を処理していますね。
✅ 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)
の設定を色々試してみましょう。
MOUSE_FILTER_STOP
の場合
3.1 子要素が
子要素がMOUSE_FILTER_STOP
の場合の挙動
子要素がMOUSE_FILTER_STOP
の場合、孫に対する入力イベントは、子要素で止まります。
子孫のMOUSE_FILTER_PASS
は、MOUSE_FILTER_STOP
が設定されているノードまで遡って、入力イベントを渡すということですね。
MOUSE_FILTER_PASS
の場合
3.2 子要素が
子要素がMOUSE_FILTER_PASS
の場合の挙動
ドキュメント通り、入力イベントは親のノードまでツリーを遡りました。
孫と子がz軸上で重なっていなくても入力が伝播するのは、2.で確認した時と同じですね。
MOUSE_FILTER_IGNORE
の場合
3.3 子要素が
子要素がMOUSE_FILTER_IGNORE
の場合の挙動
子要素がMOUSE_FILTER_IGNORE
の場合、孫に対する入力イベントは子要素を無視し、親にまで伝播しているのがわかります。IGNORE
は入力がツリーを遡ることを「妨げない」ことがわかります。
Node2D
がある場合
✅ 4. 途中にここまでで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)
MOUSE_FILTER_PASS
の場合
6.1 子要素がまずは2.1と同じように、Button (TOP)
がMOUSE_FILTER_PASS
の場合をみていきます。
マウス入力と違って、キー入力はツリーを遡らないことがわかります。
MOUSE_FILTER_PASS
を使った場合、マウス入力とキーやゲームパッド入力では挙動に違いがあることに気をつける必要がありますね。
MOUSE_FILTER_IGNORE
の場合
6.2 子要素が今後は、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