I was working on an inventory system, and as such one must enable players to play inventory Tetris in case they're bored. I have made drag and drop solutions before on different platforms, and know that there are potential edge-cases and problems waiting to suck time and life out of me. Looking for the go-to Godot solution for drag and drop, search engine results were all throwing older, manually implemented approaches mostly.
But I did finally stumble on the built-in solution which already exists in Godot, and if there's one thing I've tried to get away from, it's from DIY/NIH rabbitholes. Perfect!
Here's a quick run through so you don't have to:
Control node interface
Control
nodes have 3 virtual private functions you can override and will enable drag and drop functionality:
# Control that can be dragged from
func _get_drag_data(at_position:Vector2)->Variant
# Control that can be dragged to
func _can_drop_data(at_position:Vector2, data:Variant)->bool
func _drop_data(at_position:Vector2, data:Variant)->void
Docs: _get_drag_data, _can_drop_data, _drop_data
The drag and drop mechanism also works in that order:
-
_get_drag_data
- returns the data that can be dragged from the currentControl
. In my case, if a slot has an item, then I'll return the item, otherwisenull
. -
_can_drop_data
- will continuously be called on theControl
under the mouse position, passing the relative mouse position anddata
, and returns whether or not thisdata
can be accepted. This is where I check if the current item fits in the inventory grid at the mouse position, and if the current slot is even compatible with the item type (Weapon slot for weapons, Back slot for backpacks, etc.) -
_drop_data
- the final call, which is the same as_can_drop_data
, except here we are accepting the drop, and should handle removing the item from the previous container, and add it to the currentControl
(or whatever else you plan on doing with thedata
)
To handle the display preview of the drag and drop operation, we use:
func set_drag_preview(control:Control)->void
(docs)
As one can expect, this will use the Control
node you pass as the display icon of the drag and drop, add it to the scene tree, and destroy it once the drag is complete.
There is also
force_drag(data:Variant, preview:Control)->void
(docs) which will initiate a drag and drop programmatically. If you are calling this from the_drop_data
handler, usecall_deferred
to call it in the next frame, since the current drag operation is still finishing. I use this when dragging items to a slot that already has an item, so I initiate a drag on the previous item for a faster swapping UX.
Using it
It's a simple interface, the basic usage is straight forward - Create some data that will be dragged from a Control
, check this data, and potentially accept this data in a Control
that can be dragged to.
One last problem I had was detecting if the user ends the drag, but on a Control
that will not accept it. Here we can return to Node
basics and use the lifetime of the preview Control
! The full lifecycle then looks as follows:
- User clicks and drags, Godot calls:
_get_drag_data
- We call:
set_drag_preview
- User drags over a
Control
, Godot calls:_can_drop_data
- User releases the drag, and
_can_drop_data
was true, Godot calls:_drop_data
-
preview_control.tree_exiting
signal is emitted
To simplify this lifecycle, I'm using a separate object for managing the drag operation:
class_name ItemDrag
signal drag_completed(data:ItemDrag)
var source: Control = null
var destination: Control = null
var item: Item
var preview: Control
func _init(_source: Control, _item: Item, _preview: Control):
self.source = _source
self.item = _item
self.preview = _preview
self.preview.tree_exiting.connect(_on_tree_exiting)
func _on_tree_exiting()->void:
drag_completed.emit(self)
Using this class, an example of the other functions could be:
func remove_item(item:Item)->void:
inventory.remove_item(item)
func _get_drag_data(at_position:Vector2)->Variant:
var item := inventory.item_at(at_position)
if item == null: return null
var drag_data = ItemDrag.new(self, item, _create_item_preview(item))
set_drag_preview(drag_data.preview)
return drag_data
func _can_drop_data(at_position:Vector2, data:Variant)->bool:
if !data is ItemDrag: return false
var drag_data := data as ItemDrag
# Check if the item can fit in the inventory at this position
return !inventory.intersects_at(drag_data.item, at_position)
func _drop_data(at_position:Vector2, data:Variant)->void:
if !data is ItemDrag: return
var drag_data := data as ItemDrag
drag_data.destination = self
if drag_data.source: drag_data.source.remove_item(drag_data.item)
inventory.add_item_at(drag_data.item, at_position)
Some of the functionality could be enforced by the ItemDrag
class, especially giving things like source
and destination
stronger types that are guaranteed to have an interface for handling/adding/removing items. But having this set of functions defined is a good start to handling the full drag and drop cycle of operations.
Global drag and drop
In addition to the above Control
-based (GUI-based) approach, Godot provides some more event hooks into the lifecycle. Viewport
and Control
provide functions on querying the state of any drag and drop operations:
# Viewport
func gui_is_dragging()->bool
func gui_get_drag_data()->Variant
func gui_is_drag_successful()->bool
# Control
func is_drag_successful()->bool
The first 2 should be obvious - the first can be queried to see if a drag and drop is happening at any point, the second to get the same data
we returned in the _get_drag_data
callback in our Control
(or whatever data
is contained from force_drag
or other ways a drag has been initiated).
The last 2 can be used with the func _notification(what:int)->void
handler to receive global notifications when a drag and drop has started or ended, and if it ended successfully:
func _notification(what:int)->void:
if what == Node.NOTIFICATION_DRAG_BEGIN:
# Drag data is available (populated by our _get_drag_data() function for example)
var data = get_viewport().gui_get_drag_data()
# Use the drag data
if what == Node.NOTIFICATION_DRAG_END:
# Drag data is no longer available and has been disposed already
print("Drag ended. Success: ", get_viewport().gui_is_drag_successful())
These may come in handy, but are further from the drag and drop context. Some use-cases could be used by relevant drop receivers to highlight themselves, based on the drag data.
With these tools, any drag and drop scenario can be handled, hopefully this saves you any attempts at writing a custom drag and drop implementation. I wish you the best of luck and success in using this information!
P.
Top comments (2)
I don’t understand anything, but it’s interesting, can you give me the github so I can find out more?
If there's a need, I can cook up a sample project for this post. Will try to do so during the coming week.