Discussion of best practices regarding queue_free()

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

Hi everyone. I am hoping someone can help me figure this out, since it is really ruining Godot for me at the moment.

In my project, I am getting a reference to another node via:

var otherNode = get_tree().get_nodes_in_group("TargetNode")[0]

This works fine and as expected.

However, if I then queuefree() this otherNode, its data is seemingly not actually removed from the game/world. More specifically, if I queuefree() it and then reinstance it, all of the variables contain things such as [Deleted Object] along with the new values, and the ready() function does not get called unless I call requestready() on the instancing script.

This is frustrating because I am unable to properly initialize the new instance of the node, since it is seemingly remembering old variables, and containing [Deleted Object]s in arrays etc.

It also appears that no combination of free(), queuefree(), calldeferred(“queuefree”), or calldeferred(“free”) solves this issue, as the reference to the other node still persists. Even if I throw in otherNode = null, and or otherNode = preload(“NullScene.tscn”).instance().

The accompanying error message is "Attempt to call function 'add_child' in base 'previously freed instance' on a null instance."

Any discussion or insight would be hugely appreciated. I am not sure what the best practice is regarding getting references to other nodes and how that interacts with the different methods of freeing them from the tree. At this point I am leaning towards something like avoiding references all together and just using signals. Thanks!

Edit: removed underscores because they were causing italics

:bust_in_silhouette: Reply From: Inces

Wait, do you want to kill the node and bring it back to the scene ? If so You can’t queue it free. Just use remove_child(). Such a node will exist away of the scene tree with all of its variables, as long as its hard references are not broken. It can be then childed again, preceded with request_ready.

About the good practices, I would say - when You are making complicated game, make objects dead instead of deleting them. Like design state : CORPSE, in which your node will reside, with process set to false, raycasts turned off and dead body sprite. All the corpses can be queued free simoultanoulsy on occasion, for example when player enters new room. This is correct way to control removal of objects.

Finally, there is an antidote for arrays filled with deleted objects.
method

is_instance_valid(object) 

returns true when a node is alive and false when it is just empty reference to something already deleted.
Still it is wiser to manually remove objects from arrays of references whenever You queue any node free

Hi! Thanks a lot for your answer. Those are good insights for sure. I think what you said about the hard references being unbroken is the root of my problem. Ideally I am looking for a way to completely remove all references to the freed node.

Maybe I should have been more specific though on what’s plaguing me. The node I am freeing and then reinstancing is a tilemap node, the current level of the game. I am then using the tilemap’s ready function to parse a config file which determines things about the level such as enemies to spawn etc.

I was experimenting with what would happen if I just freed and reinstanced the tilemap to see if I would be able to create a progressively harder version of it via the config file. The ideal loop would be level is loaded, player beats the level, level is freed and reinstanced, incrementing the completed level tracker, and then its ready function sets it up according to the config file.

And finally when I call remove_child(tilemap) on the tilemap node, the whole game crashes, haha. Maybe the engine doesn’t like when you remove something and then reinstance it immediately.

As I’m typing this out though I am seeing that this is definitely not the right way to go about it, so I think I’ll try to take a little break to shake off the tunnel vision.

BigTuna675 | 2022-06-01 21:25

I see :slight_smile:
Yes, freeing a node just to reset it may seem like easiest way to avoid tedious process of recreating it. But it actually is the hardest way, as it requires to tediously ensure, that all other nodes stop depending on it.
I assume You created some tile layout in editor, that changes during project run, and want to assemble it to original on reset. I would advise to keep layout data in some resources or dictionaries and load them as needed. You can also create toolscript, that will encrypt layout You made in editor into a file or resource.

Also, queuing free wouldn’t be this problematic if it wasn’t for hardcoded references. If You dig into programming practice of “observer pattern”, You will learn how to avoid hard dependencies and make more elastic code, that doesn’t care for changes in scene tree hierarchy.

Inces | 2022-06-02 15:31

Ahh yes this is exactly what I was looking for! It’s true that I’ve used quite a few of:
var nodeRef = get_tree().get_nodes_in_group("Target")[0]
which is what I guess created a lot of hard dependencies/references. Glad to hear that using signals (observer pattern) would be a better way to do that. I’ll still need to get some kind of a reference to the emitter/receiver to connect the signal but that should still de-spaghettify the code a bit.

I’ll mark your answer as accepted now. I know the simplest way to fix this is going to be to create a fakeReset() function that will reset the level without actually freeing and reinstancing it. I was just curious as to why I was getting such weird behaviour, because it was not very intuitive at first. Or at least I didn’t know that you need to manually deal with nulling out references to freed nodes to prevent this weird zombie behaviour.

Moving forward I guess I’ll try to avoid getting hard references to things that are not within the immediate scene if I can avoid it, or at least will intentionally null them once they’ve served their purpose. Cheers!

BigTuna675 | 2022-06-02 15:59

To finish I will give You ultimate tip, that was a huge bliss for me :
You can define all signals in Autoload,
listeners will call Autoload.connect()
emitters will call Autoload.emit_signal()

This way everything is connected without hard references :slight_smile:

Inces | 2022-06-02 17:49

Ohhh that is very interesting. Yes that certainly changes how I might go about dealing with these sorts of things, thanks a bunch! :smiley:

BigTuna675 | 2022-06-02 18:08