[Godot 3.x] ドラッグ&ドロップでインベントリシステムを作ってみる
インベントリって何? -> ゲームでよくあるアイテム画面のことだよ
この記事はGodotでよくあるControlノードのDrag&Dropの実装をやってみた類ですが、ちょっとしたアレンジもあります。
完成コード
こちら
Godot3.x のドラッグ&ドロップ
実装方法は2つありました。
- マウスイベント検出系の
signal
-
Control
ノードの virtual method (今回はこちらを採用)
Signal を使う場合(今回は使わない)
基本的に、まずマウスイベントの検出から始まります。
衝突検出系のノード、つまりCollisionObject
系の(RigidBodyとかAreaとか、2D/3D両方)もしくはControl
(UI系)を継承しているノードにそれっぽいsignal
があります
input_event
gui_input
mouse_entered/exited
これらを使う場合は、ドラッグ&ドロップの処理はほぼ自前の実装になるでしょう。
出来ないことは無さそうですが、面倒そうなんで今回は採用しませんでした。
UIだけじゃなく、ゲームのロジックにドラッグ&ドロップ操作を組み込みたい場合(キャラクターとかを動かす)場合は必然的にこっちでしょう。
Control
系の virtual method
Control
系のノードの場合はドラッグ&ドロップでの利用が想定されているようで、予め用意されている virtual method をオーバーライドすることで比較的かんたんに実装できます。
(いわゆる、継承して拡張すること前提で定義されたメソッドということですね)
↓インターフェース
# Control
# Drag する Control Node に実装する
virtual get_drag_data(position: Vector2) -> Variant
# Drop される側の Control Node に実装する
virtual can_drop_data(position: Vector2, data: Variant) -> bool
virtual drop_data(position: Vector2, data: Variant) -> void
(↓Zennを探したらこっちの方法に関する記事を書いてる人がいたので、参考リンク貼らせていただきます🙏)
- ドラッグを開始する側に
get_drag_data()
関数 - ドロップされる側に
can_drop_data()
,drop_data()
関数
を実装するということですね。
Signalを使う方法に比べるとドラッグ&ドロップの検知やデータの受け渡しを実装する必要がないのでだいぶ楽です。
ドラッグの見た目をアレンジ (set_drag_previewを使わない)
Control
系の virtual method を使う方法をググると、大体はset_drag_preview()
関数をつかっている記事や動画がでてきます。
# Control
set_drag_preview(control: Control) -> void
Control
ノードを渡すとドラッグしているマウスカーソルに追従して、それでドラッグしてる視覚効果を得られるわけですね。
個人的には、もう少し見た目をアレンジしたいんですよねぇ。
マウスを離したときにドロップ先がなかったら元の場所に戻るTweenアニメーションみたいなのを追加したい(シャ○バのカードみたいな)
そもそも Drag Preview にセットしたノード って SceneTree のどこにあるんじゃいという。
ドキュメントによれば
void set_drag_preview(control: Control)
Shows the given control at the mouse pointer. A good time to call this method is in get_drag_data(). The control must not be in the scene tree. You should not free the control, and you should not keep a reference to the control beyond the duration of the drag. It will be deleted automatically after the drag has ended.
与えられたControlをマウスポインターに表示します。このメソッドを呼び出すタイミングとしては、get_drag_data()の中が良いでしょう。Controlはシーンツリーにあってはいけません。Controlを解放してはいけません。また、ドラッグしている間を超えてControlへの参照を保持してはいけません。ドラッグが終了すると自動的に削除されます。
つまり、set_drag_preview()
したあとのControlはプログラマーがいじっちゃダメってことです。ドラッグを終了すると自動で削除されるので、元の位置に戻るアニメーションなんかをついかするなら他の方法を使う必要があります。
(手っ取り早くドラッグ&ドロップの見た目を実現するだけなら使ったほうが断然楽ですが...)
なので、今回はドラッグ&ドロップ処理にはControlノードのvirtual methodを使い、視覚的な効果は自前でやるということにしました。
アイコンを作る
まずドラッグできるアイテムアイコンを作ります。
まずはドラッグする側のControlノードを作っていきます。
TextureRect
ノードでアイコンを作りました。
元の場所に戻るアニメーションのための Tween ノードも追加してます。
↓スクリプト
# ItemIcon.gd
extends Control
var picked := false
var slot: Control = null
func return_to_slot():
picked = false
# ドラッグ終了したらまたマウスイベントを拾うようにする
self.mouse_filter = MOUSE_FILTER_PASS
if slot:
# slot の Rect の中央に戻るようにオフセットを設定
var offset = (slot.rect_size - self.rect_size) / 2
$Tween.interpolate_property(self, "rect_position", rect_position, slot.rect_global_position + offset, 0.5, Tween.TRANS_EXPO, Tween.EASE_OUT)
$Tween.start()
# ドラッグ中 かつ マウスが離されたら元の slot に戻る
func _input(event):
if event is InputEventMouseButton:
if not event.is_pressed() and picked:
return_to_slot()
# マウス位置への移動を自前で実装
func _process(dt):
if picked:
self.rect_position = get_global_mouse_position() - (self.texture.get_size() / 2)
# ドラッグを検出
func get_drag_data(_pos):
var data = {node = self}
print(data)
picked = true
# ドラッグ開始したら、マウスイベントを拾わないようにする (そのままだと、ドロップ先のControlノードがイベントを拾えない)
self.mouse_filter = MOUSE_FILTER_IGNORE
return data
特に、ドラッグ開始/終了時のmouse_filter
プロパティを変更する処理が超重要です!
ドロップするときにMOUSE_FILTER_IGNORE
じゃないとドロップされる側のControlノードがイベントを拾ってくれませんでした。
(set_drag_preview()
関数を使った場合のドラッグプレビューだとマウスイベントを拾わないように自動でしてくれるので問題ないですが、自前でControlノードを操作する場合はドロップ時にドラッグプレビューがマウスイベントを吸わないようにする必要があります。)
アイテムスロットを作る
次は、アイテムをドロップできるスロットを作ります
# ItemSlot.gd
extends Control
var item: Control = null
# slot が item をセットされていなかったらドロップできる
func can_drop_data(position, data):
return item == null
func drop_data(position, data):
print(data)
print("dropped")
var _item: Control = data.node
# update item slot
if _item.slot != null:
_item.slot.item = null # item が以前にセットされていた slot の item property を null にする
self.item = _item
self.item.slot = self
self.item.return_to_slot()
主に can_drop_data()
とdrop_data()
のオーバーライドですね。
data
には Dictionaly型を渡していて、data.node
で ItemIcon のノードを取得できるので slot
プロパティを更新した後にreturn_to_slot()
を呼び出して新しいslotの位置に移動させてあげます。
いわゆる、SceneTreeシステムの親子関係的なものはセットしてません。
ItemSlotの子要素にItemIconがあるのが自然な気もしますが、親子関係をセットするとローカル座標の位置が変わったりよくわかんなかったので。
もっといい方法がある気もするけど[1]、とりあえず動くのでヨシ!👉
インベントリ画面
最終的に出来た UI がこちら
Panel
とか GridContainer
とか VBox/HBoxContainer
を駆使してできるいつものやつですね。
RPGとかの装備画面に使えそう。
まとめ
- UIのドラッグ&ドロップであれば、
Control
ノードの virtual method をオーバーライドするのが楽 -
Control.set_drag_preview()
を使えば見た目も簡単に作れる- 今回は見た目やアニメーションを凝りたかったので使わなかったけど
感想
get_drag_data()
, can_drop_data()
, drop_data()
だけ使って、set_drag_preview()
を使わないのはありなんでしょうか...
探した限り、あんまりそういうやり方の人がいないのでちょっと異端感を感じる
あと、欲を言えばドラッグ&ドロップ周りはもうちょっと Godot の signal システムに統合されててもいい気もする
(自前で実装するより簡単だけど)
-
※余談。実は上のコード、ちょっと出来が良くない気がする。
オブジェクト指向でお互いに参照をプロパティで持つような感じとか(Slot <=> Item)、nullを多用するコードとか... ↩︎
Discussion
Godot 4.0以降のプロジェクトの場合、_can_drop_data(), _drop_data(), _get_drag_data() に関数名が変わってました(頭に" _ "がついている)。ご参考まで!