【Godot】Godotのマウス入力入門:駒をクリックで選び、ドラッグで動かし、UIの上では掴まない

作成: 2026-09-22最終更新: 2026-10-10

盤の駒をクリックで選んでドラッグで動かし、UIの上では掴まないようにする、マウス入力の受け取り方を解説します。

盤の上の駒をクリックで選び、ドラッグで運んで置く操作は、カードを場に出すゲームや、建物を配置するゲームでも同じ形です。画面の上にボタンなどのUIがあるなら、UIの上では駒を掴まないようにして、2つの操作を分けます。

駒に Area2D を付けて input_event でドラッグを作ると、速く振ったときに駒が置き去りになります。受け取り方を変えると、今度はUIのボタンを押すたびに、その裏の駒まで選ばれます。

この記事では、マウスの出来事が誰に配られるのかを整理して、UIの上では掴み始めず、どれだけ速く振っても付いてくるドラッグの作り方を解説します。

盤の駒は絵から離れたカーソルにも付いてきて、上端のUIパネルの上では新しく掴まれないイメージ

この記事でわかること

  • クリックが誰に配られて、どこで止まるのか
  • 駒に直接書いたドラッグが置き去りになる3つの理由
  • 掴んだ駒を覚えて、毎フレーム追わせる書き方
  • mouse_filter の3つの値が、それぞれ何をするのか

動作確認環境: Godot 4.7(Windows)

Sponsored

マウスの出来事が配られる順

マウスの左ボタンを押すと、その1回ぶんの出来事が1つ作られて、決まった順に配られます。途中で誰かが「処理済み」の印を付けると、その先へは配られません。

クリックは_input、UI、_unhandled_input、ワールドの物の順に配られ、印が付くと先へ行かない
  1. _input()。どのノードのものも、いちばん最初に呼ばれます
  2. UI。カーソルの下にある Control が受け取ります
  3. _unhandled_input()。ここまで残ったものだけが来ます
  4. ワールドの物。CollisionObject2D(Area2D や物理ボディの共通の親クラス)の input_event が最後です。カーソルの下で重なった物には、印が付くまで全部に届きます

途中で誰かが「これは自分が処理した」と印を付けると、そこから先へは配られません。Control の中からなら accept_event()、それ以外からなら get_viewport().set_input_as_handled() が、その印を付ける呼び出しです。

Button は、押した時点で自分で印を付けます。だから _unhandled_input() に書いた処理は、ボタンの絵の上では走りません。この性質が、そのまま「UIの上では駒を掴まない」になります。

_inputに書くとボタンの裏の駒まで選ばれ、_unhandled_inputに書くとボタンだけが反応する

逆に、_input() に掴む処理を書くと、UIより前なのでボタンの上でも駒が選ばれます。_process() の中で Input.is_mouse_button_pressed() を見るのも同じで、こちらは印をいっさい見ません。ボタンの物理的な状態を直接読むだけだからです。

掴み始める場所は _unhandled_input() の1か所に決めます。

Sponsored

駒の input_event で作るドラッグ

いちばん短い書き方は、駒そのものに置く形です。Area2D には _input_event() があって、自分のコリジョン形状の上で起きたマウスの出来事が届きます。シグナルの input_event と同じ出来事を、関数で受け取る形です。

# piece.gd(このあと使わなくなります)
extends Area2D

var dragging := false

func _input_event(_viewport: Node, event: InputEvent, _shape_idx: int) -> void:
	if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
		dragging = event.pressed
	elif event is InputEventMouseMotion and dragging:
		global_position = get_global_mouse_position()

これで駒は動きます。InputEventMouseButton はボタンを押した・離した出来事で、InputEventMouseMotion はマウスが動いた出来事です。マウスで拾える対象にするかどうかの input_pickable は、最初から true です。プロジェクト設定の Enable Object Picking も最初からオンなので、CollisionShape2D を入れるだけで届きます。

ゆっくり動かしているうちは問題が出ません。3つの穴は、手が速くなった瞬間に開きます。

ゆっくりなら駒が付いてくるが、速く振るとカーソルが形の外へ出て、駒は元の場所に残る

1つ目は、速く振ると置き去りになることです。_input_event() に届く InputEventMouseMotion は、カーソルがその形状の上にある間だけ来ます。1フレームで形を飛び越すと、その先の移動は届きません。

2つ目は、形の外でボタンを離したときです。離したという出来事も形の上でしか届かないので、dragging が true のまま残ります。

このあと、ボタンを押していない状態でカーソルが駒へ戻ると、また吸い付きます。押していないのに動く駒ができあがります。

3つ目は、掴んだ位置が無視されることです。global_position(ワールドの中での位置。インスペクタの Position は親から見た位置)にカーソルの位置をそのまま入れているので、駒の端をつまんでも、最初の移動で中心がカーソルへ飛びます。

最初の2つは、原因が同じです。駒が「自分の上にカーソルがあるか」を見ているからです。3つ目だけは別で、掴んだ位置を覚えていないことが原因です。

盤で受け取って駒を選ぶ(intersect_point)

そこで、受け取る場所を駒から盤へ移します。押したことを盤の _unhandled_input() で受けて、その位置に駒があるかを自分で調べます(実践の _pick_top_piece())。

調べるには、物理の世界に「この点に何があるか」を直接たずねます。たずね先が get_world_2d().direct_space_state で、条件を PhysicsPointQueryParameters2D に入れて intersect_point() に渡すと、その点に重なっている当たり判定が配列で返ります。条件の collide_with_areas の既定は false で、Area2D は数に入りません。ここをオンにしないと、駒が1つも見つからないまま何も起きません。

重なった駒の上を押すとintersect_pointは2件返す。z_indexの大きいPiece3を選ぶ

返ってくるのは配列で、その順番は描画順ではありません。駒ごとに受け取る書き方だと、重なった2つに同じ出来事が届いて両方掴まれます。盤で受けて1つ選ぶのは、このためでもあります。先頭をそのまま使うと、見た目で下にある駒が動きます。候補の中から、z_index(描く順番を決める数字。大きいほど手前)がいちばん大きいものを選びます。駒どうしが同じ親にいて、z_index をそれぞれ別の数字にしてあるときは、これで見た目の上が取れます。

そして、駒を掴めたら印を付けます。

func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
		held = _pick_top_piece(get_global_mouse_position())
		if held != null:
			grab_offset = held.global_position - get_global_mouse_position()
			get_viewport().set_input_as_handled()   # 駒の input_event へは渡さない

grab_offset が、さきほどの3つ目を消します。掴んだ瞬間の「駒の位置とカーソルの位置の差」を覚えておくだけです。端をつまめば端を持ったまま動きます。

差を覚えないと押した瞬間に中心がカーソルへ飛び、grab_offsetを覚えるとつまんだ点のまま動く

掴んだ駒の追従と解放

残りの2つは、追従と解放を _process() へ移すと消えます。

func _process(_delta: float) -> void:
	if held == null:
		return
	held.global_position = get_global_mouse_position() + grab_offset
	if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
		held = null                                    # 離した。どこで離したかは見ない
		_update_label()

カーソルが駒の上にあるかどうかを、どこも見ていません。見ているのは「どの駒を掴んでいるか」と「左ボタンがまだ下がっているか」の2つだけです。

駒に書くと円の外で追従が途切れ、盤に書くとカーソルがどこにあっても毎フレーム駒を追う

Input.is_mouse_button_pressed() は、イベントの印と関係なくボタンの状態を返します。この記事の最初で「掴み始めには使わない」と書いたのと同じ性質が、離す判定ではそのまま長所になります。パネルの上で離しても、ボタンの絵の上で離しても、押されていないことは分かります。

mouse_filter と UI の上のクリック

mouse_filter は Control のプロパティで、値は3つです。駒は Control ではないので、このプロパティを持っていません。

Stopは受け取って止め、Passは親へ渡して誰も止めなければ先へ、Ignoreは下をくぐって後ろへ届く
  • Stop: 自分が受け取り、印を付けて止める。Panel と ColorRect の既定
  • Pass: 自分が受け取り、accept_event() で印を付けなければ親の Control へ渡す。誰も印を付けなければ _unhandled_input() まで届く
  • Ignore: 受け取らない。印も付けないので、後ろのものへ届く

Stop は、ボタンの絵の上だけでなく、そのパネルの余白でも印を付けます。 半透明のパネルでも、Modulate(ノードの色に掛ける色)のアルファ(不透明度)を 0 にした見えないパネルでも同じです。だから、HUDを Panel で囲っておけば、その矩形の中で押しても新しく駒を掴みません。Control が自分の上の入力を受け取る _gui_input() を書いて、accept_event() を呼ぶ必要はありません。

止まるのは掴み始めだけです。すでに掴んでいる駒は、_process() が毎フレーム位置を入れているので、カーソルがパネルの上へ入っても付いてきます。

逆に、装飾のつもりで置いた ColorRect が画面を覆っていると、ワールドのクリックが全部そこで止まります(ポストプロセス入門)。文字やアイコンでクリックが止まるときも同じ話です(インベントリ入門)。Label だけは既定が Ignore なので、そのままで後ろへ通ります。

Sponsored

実践:重なった駒から、いちばん上を掴む

ボードゲームの駒、カードを場に出す操作、建物の配置モード——どれも「選んで、運んで、置く」の形です。ここでは駒を3つ置いて、そのうち2つを最初から重ねておきます。

① 置くもの

新しいシーンを1つ作ります。絵はプロジェクトに最初から入っている icon.svg だけです。

ルートに Node2D を置いて Board、board.gd を付けます。

  • Board の子に Node2D を2つ、Marks と Pieces
  • Pieces の子に Area2D を置いて Piece1。位置 (300, 420)、Z Index は 0
  • Piece1 の子に CollisionShape2D。形は CircleShape2D、半径は 40
  • Piece1 の子に Sprite2D。icon.svg を入れて Scale を (0.6, 0.6)
  • Piece1 を子ごと2回複製して Piece2((560, 420)・Z Index 1)と Piece3((600, 450)・Z Index 2)にする
  • Marks の子に Sprite2D を3つ置いて Mark1〜Mark3。駒と同じ位置に置く
  • 3つとも icon.svg・Scale (0.6, 0.6)・Z Index -1・Modulate のアルファを 0.25
  • Board の子に CanvasLayer(ほかの絵とは別の層に描くノード)を置いて Hud。その子に Panel、位置 (0, 0)・Size (1152, 96)
  • Panel の子に Button を置いて ResetButton。位置 (16, 24)、テキストは「リセット」
  • Panel の子に Label を置いて StateLabel。位置 (200, 30)
  • Pieces・ResetButton・StateLabel は、固有名でアクセスする項目(英語UIでは「Access as Unique Name」)をオンにする。コードからは %Pieces のように % を付けて取る

Piece2 と Piece3 の中心は 50 しか離れていないので、半径 40 の円どうしが重なります。(580, 435) のあたりを押すと、intersect_point() は2件返します。

絵のほうは一辺が 76.8 になります。四隅は中心から 54 ほど離れていて円の外なので、つまむのは辺の真ん中あたりです。

Mark はアルファを下げた薄い絵で、初期位置に置いたままにします。駒が動いたとき、どこから来たのかが文字なしで分かります。駒の Collision Layer は既定の 1 のままでかまいません。

② 盤のスクリプト

# board.gd
extends Node2D

var held: Area2D = null
var grab_offset := Vector2.ZERO

var _start := {}

@onready var pieces: Node2D = %Pieces
@onready var state_label: Label = %StateLabel

func _ready() -> void:
	for piece in pieces.get_children():
		_start[piece] = piece.global_position      # 戻す先を覚えておく
	%ResetButton.pressed.connect(_on_reset_pressed)
	_update_label()

func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
		held = _pick_top_piece(get_global_mouse_position())
		if held != null:
			grab_offset = held.global_position - get_global_mouse_position()
			get_viewport().set_input_as_handled()
		_update_label()

func _process(_delta: float) -> void:
	if held == null:
		return
	held.global_position = get_global_mouse_position() + grab_offset
	if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
		held = null
		_update_label()

func _pick_top_piece(point: Vector2) -> Area2D:
	var params := PhysicsPointQueryParameters2D.new()
	params.position = point
	params.collide_with_areas = true                # 既定は false で、Area2D を拾わない
	params.collide_with_bodies = false
	var hits := get_world_2d().direct_space_state.intersect_point(params, 8)   # 8件まで返す
	var top: Area2D = null
	for hit in hits:
		var piece := hit["collider"] as Area2D
		if piece != null and (top == null or piece.z_index > top.z_index):
			top = piece
	return top

func _on_reset_pressed() -> void:
	held = null
	for piece in pieces.get_children():
		piece.global_position = _start[piece]
	_update_label()

func _update_label() -> void:
	state_label.text = "掴んでいる: %s" % ("なし" if held == null else held.name)

駒にはスクリプトを付けません。位置を書き換えられるだけの側で、選ばれたことも知りません。

_start は、駒のノードそのものをキーにした辞書です。名前や番号で覚えると、駒を増やしたときに合わせる手間が出ます。

③ 触って確かめる

再生して、駒をドラッグします。上のラベルが、いま掴んでいる駒の名前を出します。見るのは3つです。

1つ目は掴み方です。駒の端をつまんでも、中心がカーソルへ飛びません。そのまま速く振っても置き去りにならず、手を止めると、つまんだ点がカーソルの下に来て止まります。ラベルは 掴んでいる: Piece1 のままです。

2つ目は重なりです。(580, 435) のあたりを押すと、Piece3 だけが動き、下の Piece2 はその場に残ります。下が動くなら、intersect_point() の先頭をそのまま使っています。

3つ目はUIです。上のパネルの上でクリックしても、駒は選ばれません。 ボタンの上でも、パネルの余白でも、掴み始めは起きません。 「リセット」を押すと、駒が初期位置へ戻るだけです。

端をつまんでも飛ばず、速く振っても付いてきて、重なりは上だけ動き、パネルの上では掴まない4コマ

次に、駒をパネルの下へ運んでみてください。Hud は CanvasLayer なので、駒は z_index をいくつにしてもパネルの背後へ回り、半透明のパネル越しに暗く透けて見えます。パネルの上で指を離すと、円ぜんぶがパネルの中に入った駒はもう掴めません。「リセット」が、この状態から戻す手段になります。

ポイントは2つです。

  • 掴み始めるのは _unhandled_input() の中だけ: UIが先に印を付けてくれるので、ボタンの上でもパネルの余白でも、自分で判定を書かずに済みます
  • 掴んだあとは、カーソルが駒の上にあるかを見ない: 覚えるのは駒とオフセットの2つだけです。追うのも離すのも _process() で、駒の形は関係ありません

置いた場所をマスに吸わせたくなったら、held を空にする直前に座標を丸めます。追従の式はそのままで足ります。

うまくいかないときは

症状原因直し方
駒をクリックしても何も起きない駒に CollisionShape2D が無いか、Shape が空形を入れる。絵だけでは拾えない
HUDを足したら、盤のどこを押しても駒を掴めないHUDの根元に置いた画面いっぱいの Control が、既定の Stop のまま根元の Control を Ignore にする。上に載せたボタンやパネルは Stop のまま止めてくれる
カメラを動かしたりズームしたりすると、押した場所と違う駒を拾うevent.position(画面上の画素)を intersect_point() に渡しているget_global_mouse_position() を渡す。座標の種類はVector2入門にまとめてあります
見た目で上にある駒ではなく、下の駒が動く駒どうしの z_index が同じ値か、親の Y Sort Enabled(y が大きいものほど手前に描く設定)で描く順を決めていて、z_index と見た目が一致していない駒ごとに違う z_index を付ける。Y Sort の盤なら、z_index が同じ駒の中で y が大きいほうを選ぶ

おまけ:先に知っておくと良いこと

  • クリックとドラッグを区別したいとき: 押しているあいだにカーソルが動いた量を記録し、しきい値(数ピクセル)を一度でも超えたらドラッグとして扱います。離したときの距離だけで見ると、大きく動かして元の位置へ戻した操作もクリックになります
  • 掴んだ駒を一番手前に出したいとき: 掴んだ瞬間に z_index を上げ、離したら元の値へ戻します。戻し忘れると、次に重なったときの上下が変わります
  • InputEventMouseMotion の relative で追わない: relative は直前のマウスの出来事からの差分です。押した地点に毎回その差分だけを足すと、駒は押した場所の近くへ戻り続けます。追従には get_global_mouse_position() を使います
  • Control 同士のドラッグは別系統: _get_drag_data()・_can_drop_data()・_drop_data() という仕組みがあります。これは Control から Control へ物を渡すためのもので、Node2D には来ません。名前がドラッグなので取り違えやすい場所です
Sponsored

まとめ

  • 配る順が決まっている: _input() → UI → _unhandled_input() → ワールドの物。印が付いたら、その先へは配られない
  • UIの上では走らない: Button も Stop の Panel も印を付けるので、_unhandled_input() の処理は走らない
  • Input.is_mouse_button_pressed() は印を見ない: 掴み始めには使わず、離す判定にだけ使う
  • 追従は _process() で: input_event は形の上でしか来ない。駒とオフセットを覚えて、毎フレーム位置に入れる
  • いちばん上は自分で選ぶ: intersect_point() の順番は描画順ではない。z_index が最大のものを選ぶ

マウスの押下を _process() の中で調べていると、UIを足した日にその裏側まで反応します。いま書いているゲームでは、マウスの処理がどの関数に入っていますか。

次に読む学習パスの次の記事
入力処理

Godotのゲームパッド入門:スティックで歩き、被弾で震え、メニューをパッドだけで操作する

この記事を読む
Godot このセクションのノート104