Possible instancing issue when making multiple instantiated nodes

Godot Version

v4.3.stable.official [77dcf97d8]

Question

I am making a 7 key Vertically-Scrolling Rhythm Game in Godot, and I am having struggles with certain aspects of the game. I added Long Notes, where you have to hold a key and release it at specific times.

I have a scene called LongNoteBeginning.tscn, and a scene called LongNoteEnding.tscn. When LongNoteBeginning enters the scene for the first time, it instantiates LongNoteEnding at a certain position, and when LongNoteEnding gets released, it gets deleted with queue_free().

My issue is that when I release a long note, the wrong LongNoteEnding gets deleted when there are multiple long notes on the screen. For example, look at this situation:

A: Long note beginning
B: Long note ending
C: Long note

XXXXBXX
XXXXCXX
XXXXCXX
XXXXAXX
XBXXXXX
XCXXXXX
XAXXXXX

In this case, when the first long note is released, either both LongNoteEndings are deleted, the second LongNoteEnding is deleted, or the first LongNoteEnding is deleted. I want only the first LongNoteEnding to be deleted.

Here are some scripts:

LongNoteBeginning.gd:

extends AnimatedSprite2D

# This list shows the X positions of all the lanes.
var laneData = [21.024, 51.024, 80.024, 115.024, 150.024, 180.024, 210.024]

@export var scroll_speed = 3
@export var lane = 1
@export var keybind = "Note1" # Can be from Note1 to Note7.
@export var noteScale = 5 # The scale of the long note.

@onready var hiteffect = load("res://Objects/Hiteffect/Hiteffect.tscn") # The hiteffect scene.
@onready var longNoteEnding = load("res://Objects/Notes/LongNoteEnding.tscn") # The long note end scene.

var currentStatus = "none" # This holds the current judgement of the note.
var freed = false # This variable prevents the code from running multiple times by checking if this variable is false before executing code. When the note gets deleted, this gets set to true.
var id = 0 # This variable determines whether or not it is the first in its queue.
var respectiveList = [] # This is the note queue for the note's lane (gets set in refreshQueue).
var scene_instance = null # Variable to hold the unique instance of long note ending

func deleteNote() -> void:
	if freed:
		return # Make sure that the note is not already freed before continuing.
	
	freed = true # Sets freed to true to ensure no other script will be running.
	
	if respectiveList.size() > 0:
		respectiveList.remove_at(0) # Removes the note from the queue.
	else:
		print("deleteNote: Attempted to delete note ID: " + str(id) + " in lane: " + str(lane) + " but the queue is empty!")
	
	# Safely free the instance
	if is_instance_valid(scene_instance): # Check if the instance is valid.
		scene_instance.queue_free()

	# Safely free this note
	queue_free()

func refreshQueue() -> void: # Updates the note's local queue based on its lane and respective queue in the global variables.
	match lane:
		1: respectiveList = PlayerGlobals.noteQueue1
		2: respectiveList = PlayerGlobals.noteQueue2
		3: respectiveList = PlayerGlobals.noteQueue3
		4: respectiveList = PlayerGlobals.noteQueue4
		5: respectiveList = PlayerGlobals.noteQueue5
		6: respectiveList = PlayerGlobals.noteQueue6
		7: respectiveList = PlayerGlobals.noteQueue7

func _ready() -> void:
	global_position.x = laneData[lane - 1] # Sets the X based on its lane.
	refreshQueue()
	respectiveList.append(id) # Adds itself to the note queue.
	
	scale.y = noteScale # Sets its scale.
	
	# Create a unique instance of the long note ending.
	scene_instance = longNoteEnding.instantiate()
	scene_instance.global_position.y = global_position.y - ($Area2D/CollisionShape2D.global_position.y - global_position.y) - 10 # Some random ahhh formula I came up with while sleeping.
	scene_instance.scroll_speed = scroll_speed
	scene_instance.lane = lane
	scene_instance.keybind = keybind
	get_tree().get_current_scene().add_child(scene_instance)
	
	# Set the appropriate animation based on the lane.
	match lane:
		1, 3, 5, 7: play("White")
		2, 6: play("Blue")
		4: play("Yellow")

func _process(_delta: float) -> void:
	refreshQueue() # Ensures that the note queue is always updated.
	global_position.y += 3.5 * PlayerGlobals.speed
	
	if currentStatus == "miss" and not freed: # Adds a miss if the long note is not released.
		PlayerGlobals.miss()
		deleteNote()

func _input(_event):
	if Input.is_action_just_pressed(keybind) and respectiveList.size() > 0 and respectiveList[0] == id and currentStatus != "none": # If the keybind is pressed, the note queue is not empty, the note is first in the queue, and the note is in the players range.
		if freed: # Stop running the code if the note has already been freed.
			return 0
		
		match currentStatus:
			"bad":
				PlayerGlobals.bad()
				PlayerGlobals.longNotes[lane - 1] = true # Idk if this is accurate to the original game. Keeping it anyways just in case.
			"good":
				PlayerGlobals.good()
				PlayerGlobals.longNotes[lane - 1] = true
			"cool":
				PlayerGlobals.cool()
				PlayerGlobals.longNotes[lane - 1] = true
		
		var hiteffectInstance = hiteffect.instantiate() # Make the hiteffect.
		hiteffectInstance.lane = lane
		get_tree().get_current_scene().add_child(hiteffectInstance)

		if PlayerGlobals.combo > PlayerGlobals.maxcombo:
			PlayerGlobals.maxcombo = PlayerGlobals.combo # This sets maxcombo if the combo is a new highest.
		
		deleteNote() # Finally, our work here is done. Delete the note.

LongNoteEnding.gd:

extends AnimatedSprite2D

# This list shows the X positions of all the lanes.
var laneData = [21.024, 51.024, 80.024, 115.024, 150.024, 180.024, 210.024]

@export var scroll_speed = 3
@export var lane = 1
@export var keybind = "Note1" # Can be from Note1 to Note7.

# The hiteffect scene.
@onready var hiteffect = load("res://Objects/Hiteffect/Hiteffect.tscn")

var currentStatus = "none" # This holds the current judgement of the note.
var freed = false # This variable prevents the code from running multiple times by checking if this variable is false before executing code. When the note gets deleted, this gets set to true.
var id = 0 # This variable determines whether or not it is the first in its queue.
var respectiveList = [] # This is the note queue for the note's lane (gets set in refreshQueue).

func deleteNote() -> void:
	if freed:
		return # Make sure that the note is not already freed before continuing.
	
	freed = true # Sets freed to true to ensure no other script will be running.
	queue_free()
	
	if PlayerGlobals.longNotes[lane - 1]:
			PlayerGlobals.longNotes[lane - 1] = false # Reset the long note status.
	
	if respectiveList.size() > 0:
		respectiveList.remove_at(0) # Removes the note from the queue.
	else:
		print("deleteNote: Attempted to delete note ID: " + str(id) + " in lane: " + str(lane) + " but the queue is empty!")

func refreshQueue() -> void: # Updates the note's local queue based on its lane and respective queue in the global variables.
	match lane:
		1: respectiveList = PlayerGlobals.noteQueue1
		2: respectiveList = PlayerGlobals.noteQueue2
		3: respectiveList = PlayerGlobals.noteQueue3
		4: respectiveList = PlayerGlobals.noteQueue4
		5: respectiveList = PlayerGlobals.noteQueue5
		6: respectiveList = PlayerGlobals.noteQueue6
		7: respectiveList = PlayerGlobals.noteQueue7

func _ready() -> void:
	global_position.x = laneData[lane - 1] # Sets the X based on its lane.
	refreshQueue()
	respectiveList.append(id) # Adds itself to the note queue.

func _process(_delta: float) -> void:
	refreshQueue() # Ensures that the note queue is always updated.
	global_position.y += 3.5 * PlayerGlobals.speed
	
	if currentStatus == "miss" and not freed: # Adds a miss if the long note is not released.
		PlayerGlobals.miss()
		deleteNote()

func _input(_event):
	if Input.is_action_just_released(keybind) and respectiveList.size() > 0 and respectiveList[0] == id: # If the keybind is pressed, the note queue is not empty, and the note is first in the queue.
		if freed: 
			return 0 # Stop running the code if the note has already been freed.
		
		match currentStatus:
			"bad":
				PlayerGlobals.bad()
			"good":
				PlayerGlobals.good()
			"cool":
				PlayerGlobals.cool()
			"none": # "none" is if you release it too late.
				PlayerGlobals.miss()
		
		var hiteffectInstance = hiteffect.instantiate() # Make the hiteffect.
		hiteffectInstance.lane = lane
		get_tree().get_current_scene().add_child(hiteffectInstance)

		if PlayerGlobals.combo > PlayerGlobals.maxcombo:
			PlayerGlobals.maxcombo = PlayerGlobals.combo # This sets maxcombo if the combo is a new highest.

		deleteNote() # Finally, our work here is done. Delete the note.

Any help is appreciated. Thanks! :slight_smile:

P.S. if there is anything in my code that is a bad practice or just not recommended, please let me know!

It doesn’t seem you are consuming the input event by calling Viewport.set_input_as_handled(). If you don’t, the input event will keep being propagated to other nodes. In your case, it means that deleteNote() will be called on all nodes that checks for the same keybind in their _input implementation.

See also: Using InputEvent — Godot Engine (stable) documentation in English

BTW, I find a little strange that you ignore the event on _input and instead you opt to use the Input singleton. At that point, you could move your _input code to _process or move your _input to another method and call that method on _process.

I tried this, and it sort of worked. I used the following code:

func _input(event: InputEvent) -> void:
	get_viewport().set_input_as_handled()

in both LongNoteBeginning.gd and LongNoteEnding.gd. And while the ending shows up, in some notes, the beginning doesn’t show up. Not only this, but I have another script that needs to detect the key presses. I also merged _input into _process (except theget_viewport().set_input_as_handled()).

Here is my new code:

LongNoteBeginning.gd:

extends AnimatedSprite2D

# This list shows the X positions of all the lanes.
var laneData = [21.024, 51.024, 80.024, 115.024, 150.024, 180.024, 210.024]

@export var scroll_speed = 3
@export var lane = 1
@export var keybind = "Note1" # Can be from Note1 to Note7.
@export var noteScale = 5 # The scale of the long note.

@onready var hiteffect = load("res://Objects/Hiteffect/Hiteffect.tscn") # The hiteffect scene.
@onready var longNoteEnding = load("res://Objects/Notes/LongNoteEnding.tscn") # The long note end scene.

var currentStatus = "none" # This holds the current judgement of the note.
var freed = false # This variable prevents the code from running multiple times by checking if this variable is false before executing code. When the note gets deleted, this gets set to true.
var id = 0 # This variable determines whether or not it is the first in its queue.
var respectiveList = [] # This is the note queue for the note's lane (gets set in refreshQueue).
var scene_instance = null # Variable to hold the unique instance of long note ending

func deleteNote() -> void:
	if freed:
		return # Make sure that the note is not already freed before continuing.
	
	freed = true # Sehts freed to true to ensure no other script will be running.
	
	if respectiveList.size() > 0:
		respectiveList.remove_at(0) # Removes the note from the queue.
	else:
		print("deleteNote: Attempted to delete note ID: " + str(id) + " in lane: " + str(lane) + " but the queue is empty!")
	
	# Safely free the instance
	if is_instance_valid(scene_instance): # Check if the instance is valid.
		scene_instance.queue_free()

	# Safely free this note
	queue_free()

func refreshQueue() -> void: # Updates the note's local queue based on its lane and respective queue in the global variables.
	match lane:
		1: respectiveList = PlayerGlobals.noteQueue1
		2: respectiveList = PlayerGlobals.noteQueue2
		3: respectiveList = PlayerGlobals.noteQueue3
		4: respectiveList = PlayerGlobals.noteQueue4
		5: respectiveList = PlayerGlobals.noteQueue5
		6: respectiveList = PlayerGlobals.noteQueue6
		7: respectiveList = PlayerGlobals.noteQueue7

func _ready() -> void:
	global_position.x = laneData[lane - 1] # Sets the X based on its lane.
	refreshQueue()
	respectiveList.append(id) # Adds itself to the note queue.
	
	scale.y = noteScale # Sets its scale.
	
	# Create a unique instance of the long note ending.
	scene_instance = longNoteEnding.instantiate()
	scene_instance.global_position.y = global_position.y - ($Area2D/CollisionShape2D.global_position.y - global_position.y) - 10 # Some random ahhh formula I came up with while sleeping.
	scene_instance.scroll_speed = scroll_speed
	scene_instance.lane = lane
	scene_instance.keybind = keybind
	get_tree().get_current_scene().add_child(scene_instance)
	
	# Set the appropriate animation based on the lane.
	match lane:
		1, 3, 5, 7: play("White")
		2, 6: play("Blue")
		4: play("Yellow")

func _process(_delta: float) -> void:
	refreshQueue() # Ensures that the note queue is always updated.
	global_position.y += 3.5 * PlayerGlobals.speed
	
	if currentStatus == "miss" and not freed: # Adds a miss if the long note is not released.
		PlayerGlobals.miss()
		deleteNote()
	
	if Input.is_action_just_pressed(keybind) and respectiveList.size() > 0 and respectiveList[0] == id and currentStatus != "none": # If the keybind is pressed, the note queue is not empty, the note is first in the queue, and the note is in the players range.
		if !freed: # Stop running the code if the note has already been freed.
			match currentStatus:
				"bad":
					PlayerGlobals.bad()
					PlayerGlobals.longNotes[lane - 1] = true # Idk if this is accurate to the original game. Keeping it anyways just in case.
				"good":
					PlayerGlobals.good()
					PlayerGlobals.longNotes[lane - 1] = true
				"cool":
					PlayerGlobals.cool()
					PlayerGlobals.longNotes[lane - 1] = true
			
			var hiteffectInstance = hiteffect.instantiate() # Make the hiteffect.
			hiteffectInstance.lane = lane
			get_tree().get_current_scene().add_child(hiteffectInstance)

			if PlayerGlobals.combo > PlayerGlobals.maxcombo:
				PlayerGlobals.maxcombo = PlayerGlobals.combo # This sets maxcombo if the combo is a new highest.
			
			deleteNote() # Finally, our work here is done. Delete the note.

func _input(event: InputEvent) -> void:
	get_viewport().set_input_as_handled()

LongNoteEnding.gd:

extends AnimatedSprite2D

# This list shows the X positions of all the lanes.
var laneData = [21.024, 51.024, 80.024, 115.024, 150.024, 180.024, 210.024]

@export var scroll_speed = 3
@export var lane = 1
@export var keybind = "Note1" # Can be from Note1 to Note7.

# The hiteffect scene.
@onready var hiteffect = load("res://Objects/Hiteffect/Hiteffect.tscn")

var currentStatus = "none" # This holds the current judgement of the note.
var freed = false # This variable prevents the code from running multiple times by checking if this variable is false before executing code. When the note gets deleted, this gets set to true.
var id = 0 # This variable determines whether or not it is the first in its queue.
var respectiveList = [] # This is the note queue for the note's lane (gets set in refreshQueue).dj flliojlksadfsdfsdfdsfsdf

func deleteNote() -> void:
	if freed:
		return # Make sure that the note is not already freed before continuing.
	
	freed = true # Sets freed to true to ensure no other script will be running.
	queue_free()
	
	if PlayerGlobals.longNotes[lane - 1]:
		PlayerGlobals.longNotes[lane - 1] = false # Reset the long note status.
	
	if respectiveList.size() > 0:
		respectiveList.remove_at(0) # Removes the note from the queue.
	else:
		print("deleteNote: Attempted to delete note ID: " + str(id) + " in lane: " + str(lane) + " but the queue is empty!")

func refreshQueue() -> void: # Updates the note's local queue based on its lane and respective queue in the global variables.
	match lane:
		1: respectiveList = PlayerGlobals.noteQueue1
		2: respectiveList = PlayerGlobals.noteQueue2
		3: respectiveList = PlayerGlobals.noteQueue3
		4: respectiveList = PlayerGlobals.noteQueue4
		5: respectiveList = PlayerGlobals.noteQueue5
		6: respectiveList = PlayerGlobals.noteQueue6
		7: respectiveList = PlayerGlobals.noteQueue7

func _ready() -> void:
	global_position.x = laneData[lane - 1] # Sets the X based on its lane.
	refreshQueue()
	respectiveList.append(id) # Adds itself to the note queue.

func _process(_delta: float) -> void:
	refreshQueue() # Ensures that the note queue is always updated.
	global_position.y += 3.5 * PlayerGlobals.speed
	
	if currentStatus == "miss" and not freed: # Adds a miss if the long note is not released.
		PlayerGlobals.miss()
		deleteNote()
	
	if Input.is_action_just_released(keybind) and respectiveList.size() > 0 and respectiveList[0] == id: # If the keybind is pressed, the note queue is not empty, and the note is first in the queue.
		if !freed:
			match currentStatus:
				"bad":
					PlayerGlobals.bad()
				"good":
					PlayerGlobals.good()
				"cool":
					PlayerGlobals.cool()
				"none": # "none" is if you release it too late.
					PlayerGlobals.miss()
			
			var hiteffectInstance = hiteffect.instantiate() # Make the hiteffect.
			hiteffectInstance.lane = lane
			get_tree().get_current_scene().add_child(hiteffectInstance)

			if PlayerGlobals.combo > PlayerGlobals.maxcombo:
				PlayerGlobals.maxcombo = PlayerGlobals.combo # This sets maxcombo if the combo is a new highest.

			deleteNote() # Finally, our work here is done. Delete the note.

func _input(event: InputEvent) -> void:
	get_viewport().set_input_as_handled()

I’m sorry if I misunderstood anything, Godot is still relatively new to me. Thank you! :slight_smile:

You should call get_viewport().set_input_as_handled()
only if you do something with that input. Consider moving get_viewport().set_input_as_handled() to _process after

if Input.is_action_just_released(keybind) and respectiveList.size() > 0 and respectiveList[0] == id:

and remove _input.

If other nodes need to be aware of when an input is handled, you could use signals, specially if that other node is a parent.

It worked perfectly, thanks! There are still some issues unrelated with the input I still have to fix, but for the most part, this fixed most of my issues.