How to implement drag-and-drop of a Node2D from Control Node (UI) to 2D space and vice versa?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By swenner

What I want to achieve:

  1. Drag and drop in 2d space moves the Node2D
  2. Drag from the control node to 2d space should create a new Node2D
  3. Drag from the control to a control node should do nothing
  4. Drag from 2d space to the control node should delete the Node2D

I managed to implement 1, 2 and 3 with the signal based approach below, but how do I do 4? I tried to use “can_drop_data” / “drop_data” but could not get it to work either.


Project structure

  • world (Node2d)
    – 2DThingy (Area2d with sprite and collision)
    – Menu (ColorRect)

world.gd

extends Node2D
var held_object = null

func _ready():
    for node in get_tree().get_nodes_in_group("pickable"):
        node.connect("clicked", self, "_on_pickable_clicked")

func _on_pickable_clicked(object):
    if !held_object:
        held_object = object
        held_object.pickup()
        
func _unhandled_input(event):
    if event is InputEventMouseButton and event.button_index == BUTTON_LEFT:
        if held_object and !event.pressed:
            held_object.drop()
            held_object = null

func _on_Menu_sprite_pickup(sprite):
    if !held_object:
        print("create sprite")
        held_object = sprite
        add_child(sprite)
        sprite.pickup()

func _on_Menu_sprite_drop_scene():
    if held_object:
        print("drop sprite in scene")
        held_object.drop()
        held_object.connect("clicked", self, "_on_pickable_clicked")
        held_object = null

func _on_Menu_sprite_drop_menu():
    if held_object:
        print("drop sprite in menu")
        remove_child(held_object)
        held_object = null

2DThingy.gd

extends Area2D 
signal clicked
var is_held = false

func _ready():
    pass
            
func _physics_process(delta):
    if is_held:
        global_transform.origin = get_global_mouse_position()

func _input_event(viewport, event, shape_idx):
    if event is InputEventMouseButton:
        if event.button_index == BUTTON_LEFT and event.pressed:
            emit_signal("clicked", self)

func pickup():
    if is_held:
        return
    is_held = true
    set_process_input(false)

func drop():
    assert(is_held)
    is_held = false
    set_process_input(true)

Menu.gd

extends ColorRect

const Thingy2D = preload("res://2DThingy.tscn")

signal sprite_pickup(sprite)
signal sprite_drop_scene()
signal sprite_drop_menu()

func _ready():
    connect("gui_input", self, "_on_gui_input")

func _on_gui_input(event):
    if event is InputEventMouseButton and event.button_index == BUTTON_LEFT:
        if event.pressed:
            var thingy = Thingy2D.instance()
            thingy.global_position = get_global_mouse_position()
            emit_signal("sprite_pickup", thingy)
        elif !event.pressed:
            var mouse_pos = get_global_mouse_position()
            # workaround for https://github.com/godotengine/godot/issues/20881
            if get_rect().has_point(mouse_pos):
                emit_signal("sprite_drop_menu")
            else:
                emit_signal("sprite_drop_scene")
:bust_in_silhouette: Reply From: jeroenheijmans

I found this Question without any answer, and wanted something very similar. I’m a beginner with Godot but still came up with something that works for me, not sure if it would hold up or is proper.

It is unfortunately not a straight up answer to OP’s problem, but looking at other threads on this forum it seems acceptable to post related answers and solutions as well. And perhaps one can extrapolate from my answer to find the answer to OP’s question too?

Clone & Run

You can grab the working code from here: GitHub - jeroenheijmans/sample-godot-drag-drop-from-control-to-node2d: Shows how Godot Drag & Drop can work between Control and Node2D nodes.

Demo

Left are the Control items to drag around.
Middle has a Node2D section.
Right side has a Control based drop zone.

GitHub raw animated gif

Gist

The essence of the repository linked above is:

  • Encapsulate the Node2D stuff in a SubViewport node that’s again a child of a SubViewportContainernode;
  • Implement the _drop_data(...) function on the SubViewportcontainer;
  • Make it “drop” the actual stuff as a child of the Node2D stuff

Script on toolbox_item items:

extends Control

func _get_drag_data(_position):
	var icon = TextureRect.new()
	var preview = Control.new()
	icon.texture = %TextureRect.texture
	icon.position = icon.texture.get_size() * -0.5
	icon.modulate = modulation
	preview.add_child(icon)
	set_drag_preview(preview)
	return { item_id = "godot_icon" }

Script on the SubViewportContainer that contains a SubViewport which contains the Node2D with its physics bodies:

extends SubViewportContainer

func _can_drop_data(at_position, data):
	return data.item_id == "godot_icon" # Your logic here

func _drop_data(at_position, data):
	var component = RigidBody2D.new()
	component.position = at_position
	var sprite = Sprite2D.new()
	sprite.texture = load("res://icon.svg")
	component.add_child(sprite)
	var shape = CollisionShape2D.new()
	var rect = RectangleShape2D.new()
	rect.size = Vector2(64, 64)
	shape.shape = rect
	component.add_child(shape)
	
	%Node2D.add_child(component) # Or any child Node2D you want

Drop logic on the Control of your choosing could be along these lines, for my Panel Control script it was:

extends Panel

func _can_drop_data(at_position, data):
	return data.item_id == "godot_icon"

func _drop_data(at_position, data):
	var component = TextureRect.new()
	component.texture = load("res://icon.svg")
	component.position = at_position - (component.texture.get_size() * 0.5)
	add_child(component)

Finally

I want to repeat my disclaimer: I’m a beginner with Godot. However, I still hope this may help someone.