How to Hook Into Object Destruction During SceneTree's change_scene() or quit()

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

I’m working on hunting down a few memory leaks in my project, and one thing that surprises me is that it doesn’t seem like queue_free() nor free() are called on objects when they’re effectively removed from the game by doing a get_tree().change_scene("path") call.

If I override queue_free() or free() to just print the fact that they’re called this won’t happen upon scene changing nor SceneTree.quit(). Yet _exit_tree() does get called for such objects. While I’m currently overriding that for my destructor hooks it’s not ideal – it means any such objects can never be removed from and then re-added to the tree.

Does anyone know how objects are destroyed when Godot changes scenes or quits the application, and which function calls I can override during this process?

Of lesser importance, why aren’t either free() or queue_free() called in a way that can be overridden during quit() or change_scene()?

Edit: It’s worth noting that it does not look like free() can be overridden the same way as other functions can. Be warned if trying to do so for debugging purposes.

Edit 2: Also, unlike _ready(), queue_free() does not chain down the tree of objects. It’s only called on the object it’s called on, though presumably that object’s children are free()d.

If you want to implement destructor-style logic, do it on NOTIFICATION_PREDELETE.

That notification will be received by anything which can exist in memory, be it an Object (which must be manually free()d), Node-based (which can either be freed up directly or will automatically be freed if their parent gets freed, but will not be automatically freed if they’re not part of the scene tree in some capacity), or Reference-based objects (which untyped classes default to) right before they’re automatically freed when no longer referenced.

:bust_in_silhouette: Reply From: DDoop

change_scene() has the effect of changing the scene tree, but Nodes still exist in memory until freed. The other side (malloc to your dealloc) of this is the behavior observable when one instantiates a node and then doesn’t add it to the scene tree: the node exists somewhere in memory but isn’t represented in the game. Therefore, when you change_scene() without cleaning up the nodes, they hang around.

Because quit() and change_scene() aren’t interested in deallocating memory, just in managing the scene tree, it would be unusual to invoke free() or queue_free() when calling them. At least that is my understanding based on the docs. If I’m wrong I’d certainly appreciate someone else’s corrections.
One suggestion from this question is to old_scene.call_deferred("free") after switching your scene.

That makes a lot of intuitive sense but it doesn’t seem to reflect testing.

If you create a simple project with two scenes which only change_scene to each other when a button is pressed then check out the various debugging monitors, you’ll find that the memory (static and dynamic), the object count, resource count, node count, and orphan node count all stay constant no matter how many times you flip back and forth.

Not only that but if you add a call_deferred("free") (or actually any function) to the scene that was active before calling change_scene, it never actually gets called.

Given that get_tree().root.remove_child(self) followed by self.call_deferred("function") will call the given function but get_tree().change_scene("wherever") followed by self.call_deferred("function") will not call that function I’m inclined to think Godot is doing some kind of non-obvious memory management.

Hammer Bro. | 2020-07-30 21:28

I believe the fact that the memory counts don’t accumulate is due to the fact that you are re-using the nodes, at least in the test case you describe. In other words, there are a pair of scenes in memory that exist alongside each other, and the scene tree switches between them, never creating more than 2.
I am very interested in hearing any further responses to this question since it seems important to me and I’m confused! I think the docs could explain this better, as it stands I can only find a small section regarding memory management in the getting started with GDScript page.

DDoop | 2020-07-30 21:35

I stand corrected – it looks more like one just cannot override free() at all (I’ve yet to come up with an example which does so successfully), but that call_deferred("anything-but-free') does actually get called after changing scenes, whether by doing it via SceneTree.change_scene("path") or by manually removing the current node from the root, adding a new one, and setting SceneTree.current_scene = [new node].

I’ll test a few other edge cases that have me curious but I suspect you may be right after all. Thanks.

Hammer Bro. | 2020-07-30 22:10

Turns out you were half right.

By using NOTIFICATION_PREDELETE to verify when things are actually deleted, one can conclude that changing scenes will not cause the current scene to be deleted (though you’re free to delete it however you see fit), but closing the application will cause the current scene to be freed up.

Hammer Bro. | 2020-07-31 00:12

Thanks, reminds me I should spend more time reading about notifications…

DDoop | 2020-07-31 00:31

Change scenes manually — Godot Engine (stable) documentation in English

For posterity, this is a useful page. change_scene does delete things :slight_smile:

DDoop | 2020-12-14 16:18