DEV Community

Cover image for Drag and Drop in Godot 4.x
PDeveloper
PDeveloper

Posted on

Drag and Drop in Godot 4.x

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
Enter fullscreen mode Exit fullscreen mode

Docs: _get_drag_data, _can_drop_data, _drop_data

The drag and drop mechanism also works in that order:

  1. _get_drag_data - returns the data that can be dragged from the current Control. In my case, if a slot has an item, then I'll return the item, otherwise null.
  2. _can_drop_data - will continuously be called on the Control under the mouse position, passing the relative mouse position and data, and returns whether or not this data 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.)
  3. _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 current Control (or whatever else you plan on doing with the data)

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, use call_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:

  1. User clicks and drags, Godot calls: _get_drag_data
  2. We call: set_drag_preview
  3. User drags over a Control, Godot calls: _can_drop_data
  4. User releases the drag, and _can_drop_data was true, Godot calls: _drop_data
  5. 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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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 (0)