"queue_free" deleting all instances of an object

Godot Version

4.3.stable.mono

Question

` Hi,

I’m making a game that involves several minigames, and one of them is a very basic game where when you click on a target, the target gets destroyed. The amount of targets in the game is a random number between 1 and 4 targets, and all of them are loaded in when the game is first loaded, so they are not in the scene tree from the very beginning.

I have everything else set up and it works fine; the reticle, the random amount and positions of the targets, the collisions for the targets, the functionality to destroy the target with queue_free when it is clicked on and the counter to show how many targets have been destroyed

The one thing I can’t get to work is that when I click on one target, no matter which one it is, all of them get destroyed at the same time.

I’ve tried putting the targets inside of a seperate array, a group for the targets, even using ‘self’ on a specific instance gets me the same result

Theres a few moving parts to this minigame, so I’ll just post the code for the 3 main components; the mouse cursor (named “Reticle”), the target itself, and the gamemode.

Mouse Cursor/Reticle:

extends Node2D


@onready var reticle_collision: ShapeCast2D = $ColorRect/ShapeCast2D
signal reticle_hit()
signal target_hit




func _process(delta: float) -> void:

    position = get_viewport().get_mouse_position()

    if reticle_collision.is_colliding():
		    reticle_hit.emit()
		    print("Target Hit! " + str(reticle_collision.get_collider(0)))


func _input(event: InputEvent) -> void:

    if event.is_action_pressed("Use", false):
	
	    $ColorRect/ShapeCast2D.enabled = true
	    $ColorRect.modulate = Color.RED
	    print("Gun Firing")
	
    if event.is_action_released("Use"):
	
	    $ColorRect/ShapeCast2D.enabled = false
	    $ColorRect.modulate = Color.WHITE
	    print("Gun Stopped")

Targets:

extends Node2D

@onready var Target = $TargetColour


signal target_hit(hit_object)
var myself


func _ready() -> void:
    myself = self

func _on_reticle_reticle_hit() -> void:

    Target.modulate = Color.GREEN_YELLOW
    $TargetColour/Area2D.monitorable = false
    target_hit.emit(myself)

Game Mode/“ShootMain”:

extends Node2D

@onready var ScoreDisplay = $scoretest
@onready var ShootGame = $"."

var target_spawn = preload("res://Scenes/Minigames/Shoot/target.tscn")
var target_instance = target_spawn.instantiate()
var score = 0
var starting_targets
var targets_remaining

func _ready() -> void:
    starting_targets = randi_range(1, 4)
    print("STARTING TARGETS: " + str(starting_targets))
    ScoreDisplay.text = str(score)

    for n in starting_targets:
	    target_instance = target_spawn.instantiate()
	    var count = n
	    target_instance.name = "Target_" + str(count)

	    var instance_position_x = randi_range(100, 1000)
	    var instance_position_y = randi_range(100, 550)

	
	    add_child(target_instance)
	    target_instance.add_to_group("Targets")
	    $Reticle.reticle_hit.connect(target_instance._on_reticle_reticle_hit)
	    target_instance.target_hit.connect(ShootGame._on_target_instance_target_hit)
	    target_instance.position = Vector2(instance_position_x, instance_position_y)

    targets_remaining = get_tree().get_node_count_in_group("Targets")
    print("Current Targets: " + str(get_tree().get_nodes_in_group("Targets")))

func _on_target_instance_target_hit(hit_object) -> void:

    hit_object.queue_free()
    score += 1
    ScoreDisplay.text = str(score)
    print("Target Destroyed!")
    print("Targets Remaining: " + str(targets_remaining))

Heres the scene tree for reference:

image_2024-12-04_205845683

Could you put queue_free() just under target_hit.emit() in the target script?

Nope, returns the same result.

The problem is that the line reticle_hit.emit() doesn’t choose a single target. It’s connected to all targets, so all of them are hit.

I suggest removing the reticle_hit signal and calling a function on the target directly.

Cursor:

if reticle_collision.is_colliding():
    reticle_collision.get_collider(0).hit()

Target:

func hit() -> void:
    Target.modulate = Color.GREEN_YELLOW
    ...

How do I get the hit() function to not trigger an error message on the Cursor, because it doesn’t exist in that scene? It’s coming up with “Invalid call. Nonexistent function ‘hit’ in base ‘Area2D’”

EDIT: I definitely think you’re right about the issue though, I’ve been testing it out by just having it print the collider the cursor is colliding with, and it is indeed a different collider on each target. I also made a test Space Bar input that performs roughly the same code thats in the “_on_target_target_hit” function from the initial post, and in that case it deletes each target one by one as intended, instead of all together.

One of the posts I saw somewhat relating to this issue mentioned something to do with having to instance the Area2D’s as well, but I didn’t know how to do that without breaking everything else. Now I just need to figure out how to call the hit() function on an individual target without resorting to using signals.

If the Area2D is a child of the target node, the easiest solution is probably

reticle_collision.get_collider(0).get_parent().hit()

You could alse use an Area2D as a root node of the target scene.

New error: "Invalid Call. Nonexistent function ‘hit’ in base ‘Node2D (target.gd)’

even though its literally right there.

(literally 2 seconds later) wait wtf the code works but only when i switch it to my 2nd monitor. I got rid of the _ in “func _hit()” and Now it only works on my 2nd monitor lol.

The error that I get on my main monitor is:

“Attempt to call function ‘get_parent’ in base ‘null instance’ on a null instance”

But I check the still opened, crashed window for the game and the target I hit is indeed the only one that gets deleted, even on my main monitor.

Ok, I have seemed to fix this bug through a pretty weird method. This is just how I believe this works, so someone please correct me if I’m wrong.

In the Cursor scene, there is this if statement in the _process function that constantly checks whether the Cursor is colliding with something (mainly the targets). The Cursor’s ShapeCast2D is disabled by default and only enabled when the LMB is clicked, allowing it to detect collisions only when the Mouse is being clicked, and I had made the changes that grulps has stated above:

var timer = 1

func _process(delta: float) -> void:

    position = get_viewport().get_mouse_position() 

    if reticle_collision.is_colliding():

		    print(str(reticle_collision.get_collider(0).get_parent()) + " " + str(timer))
		    timer += 1 #These first 2 lines are to detect how many times the reticle has collided with the target

		    reticle_collision.get_collider(0).get_parent().hit() #Calls the hit() function on the Target scene

Note that “get_parent()” is only necessary here when the Area2D node you are using for collision isn’t the root node of the scene, which is the case here for me. Just make sure that the Area2D isn’t a child of something else and its only parent is the root node that has the Target’s script on it.

This code alone is successful in making sure the Cursor only deletes the target it clicked on and not all of them at once, but the problem arose, mainly from looking it up, only after the first target got queue_free()'d. The program immediately crashes stating that the function “get_parent()” can’t be called on a null instance. Basically, it can’t be called anymore because the target I am clicking on no longer exists due to it being deleted from the scene tree with queue_free(), so the program crashes. What’s odd though, as stated in the previous comment, that this code doesn’t crash at all and works perfectly only when the program is run on my 2nd monitor, which has a lower refresh rate than my main monitor (144hz on main vs. 60hz on 2nd).

The solution online I found to this was to change the code so that rather than running in a _process function, it runs in a _physics_process function instead, but this actually made sure the code didn’t work on my 2nd monitor either. This was helpful though as it further confirmed that the issue was happening due to my monitors differing refresh rates.

The solution I came up with just now was to add a little piece of code after the initial “reticle_collision.get_collider(0).get_parent().hit()” that disables the collision of the Cursor immediately. This way, the cursor can’t try and collide with a Target instance that no longer exists since its ability to collide at all has been turned off. The code for this has simply been copied from the _input function you can see in the initial post of this thread.

It looks something like this:

Cursor:

@onready var reticle_collision: ShapeCast2D = $ColorRect/ShapeCast2D
var timer = 1

func _process(delta: float) -> void:

    position = get_viewport().get_mouse_position() 

    if reticle_collision.is_colliding():
	
		print(str(reticle_collision.get_collider(0).get_parent()) + " " + str(timer))
	    timer += 1
		reticle_collision.get_collider(0).get_parent().hit()
		$ColorRect/ShapeCast2D.enabled = false
		$ColorRect.modulate = Color.WHITE
	    print("Target Hit! " + str(reticle_collision.get_collider(0)))

    if reticle_collision.is_colliding() == false:
	    timer = 1

Target:

signal target_hit(hit_object)

func hit() -> void:

    Target.modulate = Color.GREEN_YELLOW
    $Area2D.monitorable = false
    target_hit.emit(self)

Then finally, the ShootMain scene that handles the Game mode:

func _on_target_instance_target_hit(hit_object) -> void:

    hit_object.queue_free()
    score += 1
    ScoreDisplay.text = str(score)
    print("Target Destroyed!")
    hit_object.remove_from_group("Targets")
    targets_remaining = get_tree().get_node_count_in_group("Targets")
    print("Targets Remaining: " + str(targets_remaining))

The rest of the code is basically identical to what you see in the inital post of this thread, although some references have been swapped around due to changing the scene structure in the Target scene (the Area2D used to be a child of a ColorRect node, so all parts that refer to a $TargetColour/Area2D has simply been changed to just $Area2D)

I’m not particularly happy with this solution because it limits the functionality of the Cursor to only being able to click once rather than hold, but for the purposes of this game it works perfectly fine. If i was required to do that though, I would probably just resort to a health based system for the targets anyway. I initially tried tieing everything in the _process function to the framerate by multiplying it by delta, but that resulted in another error.

If anyone else has a better solution to this issue that doesn’t involve limiting the reticle’s functionality then please let me know, but for now this has fixed my issue.

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