Help me improve my code for a drag and drop system

Godot Version

4.5

Question

Gadot newbie here. I have made a working system whereby I can select a button from one side of the screen and instance it when it is dropped into a selected area. The instance can then be dragged around itself, and if no longer desired, can be dragged out of the selected area and will delete. The goal here is to have a system where I can have multiple objects on one side of the screen that the player can add to decorate the other side, picking as many of one object as they want. I’m happy with my progress so far, but I suspect that there are better things I can be doing.

Here is a demonstration of it in action:

dress-up-game-example

Here is my main scene tree:

And here is one of the item scene trees which gets instantiated when the buttons are dropped into the scene:

Here is my code.

Attached to the root node inside my item scenes, is a simple drag and drop script:

extends Area2D

var dragging = false
var os = Vector2(0,0)

func _process(delta: float) -> void:
	if dragging:
		position = get_global_mouse_position() - os
		


func _on_button_button_down() -> void:
	dragging = true
	os = get_global_mouse_position() - global_position


func _on_button_button_up() -> void:
	dragging = false

And the Main scene script attached to TectureRect node “background”:

extends TextureRect

#Info
const CLOTHES_BLUE = preload("uid://dlel27egt84p3")
const CLOTHES_GREEN = preload("uid://chksw0pvnklj7")
@onready var button_blue: Area2D = %button_blue
@onready var button_green: Area2D = $"../button_green"

#Variables
var dragging = false
var os = Vector2(0,0)
var in_zone = false
var blue = false
var green = false

#Functions
func _process(delta: float) -> void:
	if dragging:
		if blue:
			button_blue.position = get_global_mouse_position() - os
		if green:
			button_green.position = get_global_mouse_position() - os
	else:
		button_blue.position = Vector2(200, 315)
		button_green.position = Vector2(350, 315)

func inst(x, pos):
	var instance = x.instantiate()
	instance.position = pos
	instance.add_to_group("clothes")
	instance.z_index = 1
	add_child(instance)

##Blue Button
func _on_blue_button_button_down() -> void:
	dragging = true
	blue = true
	os = get_global_mouse_position() - button_blue.position

func _on_blue_button_button_up() -> void:
	if in_zone:
		inst(CLOTHES_BLUE, get_global_mouse_position() - os)
	dragging = false
	blue = false
	
func _on_button_blue_area_entered(area: Area2D) -> void:
	if area.name == "Zone":
		in_zone = true

func _on_button_blue_area_exited(area: Area2D) -> void:
	if area.name == "Zone":
		in_zone = false

##Green Button
func _on_green_button_button_down() -> void:
	dragging = true
	green = true
	os = get_global_mouse_position() - button_green.position

func _on_green_button_button_up() -> void:
	if in_zone:
		inst(CLOTHES_GREEN, get_global_mouse_position() - os)
	dragging = false
	green = false

func _on_button_green_area_entered(area: Area2D) -> void:
	if area.name == "Zone":
		in_zone = true

func _on_button_green_area_exited(area: Area2D) -> void:
	if area.name == "Zone":
		in_zone = false

##Zone
func _on_zone_area_exited(area: Area2D) -> void:
	if area.is_in_group("clothes"):
		area.queue_free()

As you can see, I utilize buttons and groups along with a script to drag my objects. The instantiated items get added to the group “clothes” when they are added, and are checked by the collision2D when they leave the “zone”.

Now that I have explained everything, here’s what I want to improve.

I would like to add the buttons to a new node to make a menu interface that can be turned on and off. However, whenever I drag the buttons to be children of a new node (I have tried a 2D node and another TextureRect), I get an error telling me that I cannot set the position of both the blue and green buttons.

Invalid assignment of property or key 'position' with value of type 'Vector2' on a base object of type 'null instance'.

This is something I would like to fix.

Secondly, this will become very unruly very quickly, as I would like there to be 20-50 draggable assets. I’m not sure if there’s a way to use Classes to help streamline this process, but with the script listening out for button down and up signals, and wanting to add the instantiated children to the main scene tree, I have not been able to figure out a way to package the scenes effectively.

That’s all I can think of for the time being on how this project is going. If you’re able to help me do this in a more efficient way, please can you explain a little bit of the how as well as the answer so I can learn a bit more about Gadot and GD Script in the process. If you need any more info, please let me know.

Hi. It is not immediately clear what buttons you drag and where to get the error.
That is a nice system you have built, but if you are looking for help you may want to narrow the question down to one specific issue, and provide the exact information relevant.

Chances are you will run into an answer yourself, while describing what parts are there, what they are doing and how it doesn’t work.

2 Likes

I agree with @rpahut

About this issue:

It says that the object reference you are trying to assign a position is actually null.
So it means you think your variable has a reference to a Node, but actually it doesn’t.
Let’s say if it happens here:

It means button_blue variable does not hold a reference to the Node that is your blue button. It’s just null (empty)

It’s relatively easy to debug this. You can add breakpoints to your code, and check what’s happening. Also, when you see the error, the project stops running, and you can debug what’s happening. You can even see the variable assignments there.

1 Like

@lastbender Thank you for that, your explaination made me realise that I never reconnected the nodes after moving them. This is now fixed.

@rpahut Thanks for the advice. I’ll try to be clearer.

I was wondering if there was a more succinct way to achieve the movement and spawning of my items. Currently I have 4 functions for each inside my main script, consisting of button_down, button_up, area_entered and area_exited. I figure this will make a very long list in my script if I was to then repeat the process for every item I put in the game. My question is, can I streamline this process? Or is repeating the process for each item one by one this way the best course of action?

Instead of using the area_entered and area_exited signals, you chould just check if the area overlaps with Zone when the button is released.

For further serializing things, you could combine the functions to one that receives the button’s area as argument. (Conceptually something like:)

var zone := %Zone
var currently_dragged_button: Area2D

func _process(delta: float) -> void:
	if currently_dragged_button:
			currently_dragged_button.position = get_global_mouse_position() - os
	else:
	# This could probably be done when a button is released, via a
	# dictionary that contains the reset position of every button.
		button_blue.position = Vector2(200, 315)
		button_green.position = Vector2(350, 315)

func _on_any_button_down(button: Area2D) -> void:
	currently_dragged_button = button
	os = get_global_mouse_position() - button.position

func _on_any_button_up(button: Area2D) -> void:
	if currently_dragged_button == button: # safety check
		if button.overlaps_area(zone):
			inst(button.clothes_scene, get_global_mouse_position() - os)
		currently_dragged_button = null

Then each button would have to listen to their own button_up/button_down signals and call the main script’s functions in response (either via signal or directly), passing in the button’s area as argument. They would also need an (exported) var clothes_scene that contains the scene you want to instantiate for them. (You could also do this via a dictionary in the main script if you want.)

1 Like

:index_pointing_up: That in many ways. But most importantly, instead of writing one script governing all the pieces in the specific combination, you ought to think in terms of what pieces themselves do and automate them.

Your items already have the script that “does what item does”. Another piece of the system with distinct behavior is this button that can be dragged, and when dropped within certain area spawns a certain scene. So make a separate script that realizes this “DraggableSceneSpawner” behavior. Make it have “exported properties” too, one to hold the target drop Zone, and another to hold the path of the scene to be spawned. Now you can attach this script to however many buttons you need, and configure each button with individual parameters right in the editor.

2 Likes

@hyvernox @rpahut Thank you both! That is super helpful. I understand what you are suggesting, but will need to have a bit of a play to see if I understand how to impliment it. I want to give it a go though before asking anymore questions :slight_smile: So I’ll see what I can do and be back if I get stuck.

1 Like

Just an update: I’ve made the improvements! I’ve changed the entered and exited signals to check if it is overlapping like hyvernox suggested, then separated the script out so the buttons have their own script, with exported properties for the scene to be spawned and the resting position (since the zone is the same for all items) like ruhut suggested. Thank you again for your help, I’m much happier with it all now.

2 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.