free instanced scene under different if

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

Hi everyone,

there are several if-conditions in a function, one instancing a scene, do some tweening, and then queue_freeing it again.

extends Timer
…
func xyz():

	var a_sprite = preload("res://general-assets/animations-sprites/NiceSprite.tscn").instance()

	if condition_a:

		var tween = Tween.new()

		add_child(a_sprite)
		tween...
		...
		tween.start()
	
		yield(get_tree().create_timer(2),"timeout")
		a_sprite.queue_free()

So after 2 seconds NiceSprite „disappears“ again.

It won’t, however, disappear, if I try to queue_free() under a different if (which follows at a later time).

extends Timer
…
func xyz():

	var a_sprite = preload("res://general-assets/animations-sprites/NiceSprite.tscn").instance()

	if condition_a:

		var tween = Tween.new()

		add_child(a_sprite)
		tween...
		...
		tween.start()
	

	if condition_b:

		a_sprite.queue_free()

So this does not free NiceSprite. How can I free it unter condition_b? Any ideas?

The only way queue_free() isn’t called on the sprite is if condition_b is false at the time function xyz is called.

Writing out this question I assume you already know that. We could actually help you if you posted your actual code because there is likely a bug that you are overlooking.

timothybrentwood | 2021-10-30 22:41

I did indeed not know that, thanks for the hint!

The function is being triggered regularly after a timer-cycle and it compares the numbers in two labels.

func xyz():
        
        	var numbers_in_label: int
        	numbers_in_label = int($LabelTheTimer.text)
        
        	var old_numbers_to_compare: int
        	old_numbers_to_compare = int($OldNumbersToCompare.text)
        	
        	var pos = 0
        	while(old_numbers_to_compare or numbers_in_label):
    
        		if old_numbers_to_compare % 10 != numbers_in_label % 10:
            		if ("Digit %s changed to %s" % [pos, numbers_in_label % 10]) == ("Digit 0 changed to 0"):
        				print ("Digit 0 changed to 0!")
    
        			if ("Digit %s changed to %s" % [pos, numbers_in_label % 10]) == ("Digit 0 changed to 1"):
    					print ("Digit 0 changed to 1!")
    
    				if ("Digit %s changed to %s" % [pos, numbers_in_counter_label % 10]) == ("Digit 0 changed to 2"):
    					print ("Digit 0 changed to 2!")
    
    ...

So that is the problem then, only one of the if-conditions is true when the function is called, all of the others are false.
So not a bug I suppose, just plain logic I didn’t realize until your comment - of course it can’t work that way!
I assume there’s no way to consider all the ifs in relation to each other when the function is called?

Edit:
I’d like to add that I’m actually feeling really dumb right now… ; )

pferft | 2021-10-30 23:21

My apologies then. It seems like a simple rubber duck debugging issue lol.

only one of the if-conditions is true when the function is called, all of the others are false.

With the way the code is structured I would assume this is the case. Seems like you may have some copy and paste errors as in some of the conditionals you’re doing numbers_in_counter_label % 10 and others you’re doing numbers_in_label % 10.

I assume there’s no way to consider all the ifs in relation to each other when the function is called?

Hmm… not sure what you’re getting at there… consider the following code and see if that answers your question:

func number_description(num):
    var divisible_by_2 = num % 2 == 0
    var divisible_by_3 = num % 3 == 0

    if divisible_by_3 and divisible_by_2:
        print(num, " is divisible by 6!")
    elif divisible_by_3:
        print(num, " is divisible by 3!")
    elif divisible_by_2:
        print(num, " is even!")
    else: // same as: not (divisible_by_3 or divisible_by_2)
        print(num, " is odd!") // won't print for odd numbers divisible by 3

func alternative_number_description(num):
    var divisible_by_2 = num % 2 == 0
    var divisible_by_3 = num % 3 == 0

    if divisible_by_3 and divisible_by_2:
        print(num, " is divisible by 6!")
    elif divisible_by_3:
        print(num, " is divisible by 3!")
    elif divisible_by_2:
        print(num, " is even!")
    
    if not divisible_by_2:
        print(num, " is odd!") // WILL print for odd numbers divisible by 3 in addition to "(num) is divisible by 3!" from the above if statement

timothybrentwood | 2021-10-31 00:22

No need to apologize for anything, thanks for the help!

Sorry for that copy/paste "typo"´(guess I was too tired last night…)

I’m sure that if not is the way to to. I added it to my example:

func xyz():
    
    var numbers_in_label: int
    numbers_in_label = int($LabelTheTimer.text)
    
    var old_numbers_to_compare: int
    old_numbers_to_compare = int($OldNumbersToCompare.text)
    
    var pos = 0
    while(old_numbers_to_compare or numbers_in_label):
    
    if old_numbers_to_compare % 10 != numbers_in_label % 10:

        if ("Digit %s changed to %s" % [pos, numbers_in_label % 10]) == ("Digit 0 changed to 0"):
            print ("Digit 0 changed to 0!")
    
        if ("Digit %s changed to %s" % [pos, numbers_in_label % 10]) == ("Digit 0 changed to 1"):
            print ("Digit 0 changed to 1!")
    
        if ("Digit %s changed to %s" % [pos, numbers_in_label % 10]) == ("Digit 0 changed to 2"):
            print ("Digit 0 changed to 2!")
            if not ("Digit %s changed to %s" % [pos, numbers_in_label % 10]) == ("Digit 0 changed to 1"):
            print ("Digit 0 is not 1 anymore!")
    
        ...

So this works, “Digit 0 is not 1 anymore!” is being printed.

Going back to my initial problem though, freeing my earlier instanced scene still does not work.

    if ("Digit %s changed to %s" % [pos, numbers_in_label % 10]) == ("Digit 0 changed to 1"):
        add_child(a_sprite)

    if ("Digit %s changed to %s" % [pos, numbers_in_label % 10]) == ("Digit 0 changed to 2"):
        if not ("Digit %s changed to %s" % [pos, numbers_in_label % 10]) == ("Digit 0 changed to 1"):
        a_sprite.queue_free()

Now I assume that an earlier instanced new child it’s not as easy to grab and free again under another (later) if-condition, perhaps due to where that new child is being put into the scene tree, maybe among many others, but I do not know enough about these structures to be sure… is there a way to “mark” that particular child so I can “find” it later to free it?

pferft | 2021-10-31 11:32

Hmmm okay I think I understand what you’re going for. If you have 100 -> 101 in one step you want a_sprite to appear. On the next timer timeout if 101 -> 10x (with x being anything other than 1), you want a_sprite to disappear. Or something along those lines.

It’s possible that you’re complicating things in your head and thus translating them to code is more complicated than it needs to be. Could you describe exactly what you want to happen then I can help you further? Is it the same sprite image every time or do you have a different image for every digit that changes? Are you iterating through all the digits of a number and checking for differences digit-wise? Is it always an N digit number changing to an N digit number or can it be like a 4 digit number going to a 3 digit number? Those are just some questions I have off the top of my head.

Note that in the instance of the nested if that you have:

if ("Digit %s changed to %s" % [pos, numbers_in_label % 10]) == ("Digit 0 changed to 2"):
        if not ("Digit %s changed to %s" % [pos, numbers_in_label % 10]) == ("Digit 0 changed to 1"):

The inner if (if not in this case) is always superfluous and can be removed entirely. In the most basic case if your number changes from 5 -> 2 in order to make the outer if condition true then the inner if condition is ALWAYS true. Likewise if the outer if condition is false the inner if condition will never evaluate.

timothybrentwood | 2021-10-31 13:03

Yes, your first paragraph is exactly what I’m trying to achieve.

There’s a counter changing numbers (eventually increasing from only one digit at the beginning to an unknown number of digits later on with a maximum of N), and each number calls its own instanced sprite(-scene). (In case of, say, two 3s simultaneously, they share the same instance but I place them on different coordinates by a tween… I prepared individual variables for each case - each digit-position has its unique name… certainly not very elegant coding, I suppose.)

The update happens by comparing two labels, “old” and “current”, while the old one will be replaced by the current one right after the step is done, ready for the next round.
I was able to make all this work.

What I need is to make the “former” sprite disappear the moment it’s being replaced by a new one, so for example when a counter of 156 changes to 186, the 5-sprite disappears while the 8-sprite appears. That disappearing of the 5-sprite is what I struggle with.
As the sprite has been added as a child, I thought I could free it to disappear, but I can’t seem to address it anymore. In my example, that’s probably due to what you explained as the inner if condition never evaluating.

I hope all this makes some sense. Your help is very much appreciated!

pferft | 2021-10-31 19:42

I would recommend using a singular sprite instance for each digit and changing its texture property when you need to. This allows you to keep track of the sprites in an easy to access array. Making the sprite its own scene with its own script allows you to instance it and call functions on it. This would allow you to make the sprite know how to handle the tweening logic, what do do when it is first added to the scene, what to do when its digit changes and when to make itself visible/invisible. You could make each of the formerly mentioned processes its own function.

Below is a quick and dirty method of how I would approach this problem. Note that digit_comparison() needs a call with old_string = "" in order to properly setup the sprites_for_digits array.

# [1's place, 10's place, 100's place, ..., pow(10,index)'s place]
var sprites_for_digits = []
const sprite_scene = preload("res://Sprite.tscn") # whatever yours is

func _ready() -> void:
	var old_string = ""
	var new_string = "123"
	digit_comparison(new_string, old_string)
	
    old_string = "123"
	new_string = "4124"
	digit_comparison(new_string, old_string)
	
func digit_comparison(new_string:String, old_string:String="") -> void:
	var old_array = make_string_array(old_string)
	var new_array = make_string_array(new_string)
	
	var new_digits = []
	var number_of_new_digits = new_string.length() - old_string.length()
	
	# remove new digits from new_array
	for i in number_of_new_digits:
		# add in reverse order to make it easier later
		new_digits.push_front(new_array.pop_front())
	
	# new_array and old_array are now the same length with corresponding digits
	# in corresponding positions e.g. [1,2,3] and [1,2,4]
    # deal with existing digits
	var number_of_old_digits = old_array.size()
	for index in number_of_old_digits:
		# digits don't match
		if new_array[index] != old_array[index]:
			# number_of_old_digits - (index+1) since we're iterating from left to right
			prints(str(pow(10, number_of_old_digits - (index+1))) + "s digit changed from", old_array[index], "to", new_array[index])
			# do something with sprites_for_digits[number_of_old_digits - (index+1)] here
			# such as sprites_for_digits[number_of_old_digits - (index+1)].digit_changed_from_to(old_array[index], new_array[index])
			# the sprite scene should know how to handle this IMO
	
	# deal with newly added digits
	for index in number_of_new_digits:
		# new_digits array is in REVERSE ORDER to easily add to the back of sprites_for_digits array
		prints(str(pow(10, number_of_old_digits + index)) + "s digit changed from", 0, "to", new_digits[index])
		# add a new sprite to sprites_for_digits
		var new_sprite = sprite_scene.instance()
		sprites_for_digits.push_back(new_sprite)
		self.add_child(new_sprite)
		# do something with sprites_for_digits.back() here
		# such as sprites_for_digits.back().add_new_digit(new_digits[index])

	
func make_string_array(s:String) -> Array:
	var array = []
	for character in s:
		array.push_back(character)
	return array

timothybrentwood | 2021-11-01 00:22

Yor suggestion “I would recommend using a singular sprite instance for each digit” is exactly the right way. And now I found a way to free an instanced scene at a later point - my mistake was another really small one:

func xyz():

    var a_sprite = preload("res://general-assets/animations-sprites/NiceSprite.tscn").instance()

    if condition_a:

        add_child(a_sprite)

Now this won’t work:

    if condition_b:

        a_sprite.queue_free()

But this works:

    if condition_b:

        get_node("NiceSprite").queue_free()

Not the variable but the instanced scene itself has to be “named” to free!
As the scene is being added to the tree before, its node can be found and freed later.

Now I wonder why that shouldn’t work by “naming” the variable as it points directly to the scene, but there surely is some good reason for that.

Thank you again for your help! Somehow you’re always leading to the right path : )

pferft | 2021-11-01 15:56

What you’re doing does work but is error prone and potentially leaves yourself open to memory leaks (scenes that are created and never deleted). It’s also not preferable to manage your nodes like that in most cases.

Now I wonder why that shouldn’t work by “naming” the variable as it points directly to the scene, but there surely is some good reason for that.

The variable a_sprite refers an instance of the NiceSprite.tscn scene during the life cycle (or scope) of that function call. In subsequent function calls a_sprite will refer to new instances of the NiceSprite.tscn scene. You will be able to see this occurring if you look at the remote scene tree in the editor while your game is running:
Overview of debugging tools — Godot Engine (stable) documentation in English

Consider the following code:

var member_variable = 5

func _ready():
    for i in 5:
        my_function()

func my_function():
    var a = member_variable + 5
    print(a)
    a  = a + 5

The above code will print 10, 5 times since my_function() is called 5 times in a for loop. This is because the variable a gets deleted every time my_function() is done executing (immediately after a = a + 5 executes). As soon as a leaves its “scope” it is cleaned up (deleted) by Godot’s garbage collector. Thus the a = a + 5 line is superfluous since the variable a gets deleted from memory right after that. Consider this code:

var member_variable = 5

func _ready():
    for i in 5:
        my_function()

func my_function():
    var a = member_variable + 5
    print(a)
    member_variable = member_variable + 5

The above code will print 10, 15, 20, 25, 30. This is because member_variable is a member variable (a variable that is declared inside of a class but outside of a function in that class - declared without an indent) of the class (“object” or “Node” are other names) and thus member_variable’s scope is the class as a whole (basically anywhere in the script member_variable will be whatever you set it to last). The only time member_variable will be deleted is if the object that it is declared in is deleted.

Back to potential issues you can run into using your method. If you name the NiceSprite.tscn scene instance by doing a_sprite.name = "CoolSprite" then add it to the scene tree self.add_child(a_sprite) only the FIRST sprite created is guaranteed to have the name/NodePath CoolSprite.

If CoolSprite exists in the scene tree at the same time that you create a new instance, name it and add it to the scene tree, the new instance will NOT have the name CoolSprite it will instead have the name @CoolSprite@2 meaning that your get_node("CoolSprite") call will never find the newly instanced sprite as its NodePath is @CoolSprite@2.

This is the reason why it’s preferable to store references to the nodes you create at runtime (through code/while the game is running/not in the editor) in member variables, or add them to groups (via add_to_group()). If you only ever create one node in code you can refer to it safely as you are doing but you should ask yourself if you’re only creating one, why not just create it in the editor?

timothybrentwood | 2021-11-01 20:42

The remote scene tree has become my friend only not too long ago and surely offered some surprises. Ever since then I carefully made sure that all my added children get freed again.
There are cases when I tried putting a variable as a member variable, which didn’t work, then put it into a function where it worked and vice versa, but I certainly never understood why - until now. It was some trial and error but from now on I will be able to actually consider the options thanks to your elaborate and really easy to understand explanation, so thank you very much once more for taking your time and all your valuable efforts! Learning can be fun : )

pferft | 2021-11-02 19:53