スクロールしたいけど、スクロールコンテナに表示されているオブジェクトもドラッグ&ドロップにしたいときの実現方法です。
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種類あり、それぞれはマウス操作に対して以下の動きになります。
- Stop: 自分はキャッチ、後ろには通過させない
- Pass: 自分はキャッチ、後ろにも通過させる
- Ignore: 自分は無視、後ろには通過させる
この属性をStopとPassの切替で、ScrollContainerにマウスの動きを伝達するか否かを動的に決めてあげます。
具体的には、一連の動きは以下です。
- デフォルトStopにしてスクロールできないようにします。
- マウスが押された瞬間、押された位置にオブジェクトが存在しているをチェックします。
- オブジェクト存在していた場合、フィルターをStopのまま、そうするとドラッグのアクションが始まります。
- オブジェクト存在していない場合、フィルターをPassにし、ScrollContainerにタッチイベントを渡してスクロールできるようになります。
- マウスを放す瞬間、マウスフィルターまた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で次の設定をオンにすると、マウスでタッチイベントを起こせることになります。
コメントを残す