[Godot4] ScrollContainerでのドラッグ&ドラップの実現方法

投稿日:

更新日:

カテゴリ:

スクロールしたいけど、スクロールコンテナに表示されているオブジェクトもドラッグ&ドロップにしたいときの実現方法です。

RPGとかのインベントリからキャラの装備を脱着とかが考えやすいユースケースではないでしょうか。

これを実現するために、色々試した結果を共有します。

今のところ、これが一番良さげです。

 

まずノードの構成は以下です。

ScrollContainerの中、Controlを追加し、縦のサイズがScrollContainerより大きくすることでスクロールできるようにしています。

Controlのは以下にはオブジェクトのサンプルとして、TextureRectを2つ適当においてます。

そして完成イメージはこうです。

では、イメージが合えば、早速実装方法の解説です。

ドラッグ&ドロップの実現

Godot 4からの機能かもしれませんが、まずドラッグ&ドラッグの実現方法はとても簡単です。

ドラッグ対象には以下のスクリプト(draggable_object.gd)をアタッチします。

extends TextureRect


func _get_drag_data(at_position: Vector2) -> Variant:
	var prev = TextureRect.new()
	prev.texture = load("res://assets/grid/room_01_b.png") # わかりやすいように、任意な画像設定

	var c = Control.new()
	prev.position = -0.5 * prev.texture.get_size() # ドラッグしているとき、カーソルがオブジェクトの中心に来るように位置調整
	c.add_child(prev)

	set_drag_preview(c)
	
	return self

そして、ドロップ対象には以下のスクリプト(droppable_control.gd)をアタッチします。

extends Control


func _can_drop_data(at_position, data) -> bool:
	data.modulate = Color(1, 1, 1, 0.5) # ドラッグ開始、わかりやすいように半透明にする
	return true # 実際はドロップできるところだけtrueを返し、許容しないところはfalseを返す
	
func _drop_data(at_position, data) -> void:
	# 元の親から削除、同じ親の中の移動では、これとadd_childあたりが不要かも
	data.get_parent().remove_child(data)
	data.modulate = Color(1, 1, 1, 1) # 色を元に戻す

	# 前述プレビュー位置調整に合わせてドロップ位置も調整
	data.position = at_position - data.texture.get_size() * 0.5
	add_child(data)

はい、これだけです。個人的にはかなり驚きですが、フレームごとの計算なんていらないなんですね。

プレビュー位置や見た目調整でコードが増えますが、実態はそんなにないですよね。

スクロールオン・オフ制御

上記だけでオブジェクトをScrollContainer中に配置すると、ドラッグしたらScrollContainerも一緒に動いたり、狙った位置に移動が難しい問題があります。

それを解決するために、マウスのフィルター機能を利用してスクロールのオン・オフ制御をします。

 

まずマウスのフィルター機能ですが、以下のようにControl継承しているノードにある属性です。

フィルターは3種類あり、それぞれはマウス操作に対して以下の動きになります。

  1. Stop: 自分はキャッチ、後ろには通過させない
  2. Pass: 自分はキャッチ、後ろにも通過させる
  3. Ignore: 自分は無視、後ろには通過させる

この属性をStopとPassの切替で、ScrollContainerにマウスの動きを伝達するか否かを動的に決めてあげます。

具体的には、一連の動きは以下です。

  1. デフォルトStopにしてスクロールできないようにします。
  2. マウスが押された瞬間、押された位置にオブジェクトが存在しているをチェックします。
  3. オブジェクト存在していた場合、フィルターをStopのまま、そうするとドラッグのアクションが始まります。
  4. オブジェクト存在していない場合、フィルターをPassにし、ScrollContainerにタッチイベントを渡してスクロールできるようになります。
  5. マウスを放す瞬間、マウスフィルターまたStopに戻します。

 

実装では、先ほどのドロップ対象にアタッチしたスクリプト(droppable_control.gd)に以下の処理を追加します。

extends Control


func _ready():
	mouse_filter = Control.MOUSE_FILTER_STOP # Inspectorで設定してもいいが、アタッチすると自動的適用されると便利のため、ここで設定

func _input(event):
	if !(event is InputEventScreenTouch): # タッチイベントのみに反応
		return
	if not event.pressed: # タッチイベントには押すと放すの2種類あり、放す場合はマウスフィルターを元に戻す
		mouse_filter = Control.MOUSE_FILTER_STOP
		return
	
	# タッチイベントで押下された場合、タッチされた位置にオブジェクト存在しているかチェック 
	for child in get_children():
		if child.get_global_rect().has_point(event.position):
			print_debug("inside object")
			return

	# 何も押されていないときだけ、タッチイベントをScrollContainerに渡す
	print_debug("mouse pass")
	mouse_filter = Control.MOUSE_FILTER_PASS

### 以下既存コード

これで冒頭動画の動きができるようになります。

 

最後に一つ注意点ですが、この例ではスマホとかの端末を意識しているため、イベントをInputEventScreenTouchでチェックしてますが、実際マウスのイベントはこれではありません。

パソコンの場合、GodotのProject Settingsで次の設定をオンにすると、マウスでタッチイベントを起こせることになります。


投稿日

カテゴリー:

投稿者:

タグ:

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です