How to temporarily save a level's state?

Godot Version

4.1.2

Question

I’m trying to save my main level’s state when I enter a temporary mini-level (a shop), but I haven’t been able to get my state to be remembered. Possibly because Godot does memory management and/or parameter passing different to how I expect.

Note that this is temporary saving, not saving the game state as a saved game.

I change to/from the temporary scene using:
scene_tree.change_scene_to_file( new_scene )

I am attempting to save my enemies to a variable in a globally preloaded script:
var enemies : Array[Node]

Most attempts to actually place data into this array fail. I have tried:

First Attempt:

Globals.enemies = Globals.current_scene_node.get_node( "Enemies" ).get_children()

Whenever I needed to look at Globals.enemies it was empty.
I assumed this was because the parent node and all its children were being cleaned up when the scene was being destroyed.

Second Attempt:

	
	nodes = []
	
	for n in Globals.current_scene_node.get_node( node_name ).get_children():
		nodes.append( n )
		n.get_parent().remove_child( n )
		
	print( node_name, " contains ", nodes.size())

The print statement here confirms that the array is populated, but a similar print statement at the call site says the array is empty.
I assumed this was because nodes was passed as a copy or copy-on-write reference or something?

Third Attempt

	
	var nodes = []
	
	for n in Globals.current_scene_node.get_node( node_name ).get_children():
		nodes.append( n )
		n.get_parent().remove_child( n )
		
	print( node_name, " contains ", nodes.size())

	return nodes

Again, print inside the function showed the nodes array was populated, but the value the function returns at the call site is an empty array.
I assumed this was because nodes gets destroyed or something?

Final Attempt

	set( new_value ):
		enemies = []
		for n in new_value:
			enemies.append( n )
			n.get_parent().remove_child( n )
		
		print( "enemies contains ", enemies.size())

This seems to have worked somewhat but I can’t get the values back out using:

	
	var node : Node = Globals.current_scene_node.get_node( node_name )
	for n in nodes:
		print( "Adding ", n )
		node.add_child( n )

I see all the Adding ... lines appear in the console, but node remains childless.

So, I’m guessing I’m missing a fundamental piece of information about function arguments and/or memory management or something.

Any explanations on why the above failed and how to fix things would be greatly appreciated. As would responses on the Right Way™ to do this.

Thanks,
Brian.

You’re storing scenes that get queue_free’ed when you change scenes (i.e. their parent scene gets queue_free’ed). These objects turn into invalid, freed objects that you can’t really ever reference or do anything with (you’ll get a runtime error if you try).

To test this, save a reference to one enemy from temp scene in a singleton and then after that scene is gone, call is_instance_valid(your_enemy_object) and you’ll see it returns false.

Anyhow, to store state between scenes you need to store state in non-scene objects, i.e. extending RefCounted and not Node. For example, Enemy class could have property like:

var state: EnemyState

And your EnemyState could be:

extends RefCounted
class_name EnemyState

var health: float = 50
var dmg: float = 10
#etc including enemy type and such

In your Globals you’d store:

var enemies: Array[EnemyState]

in temp scene before it disappears:

for e in enemies: # here enemies refers to the parent node that contains all enemy scenes
    Globals.enemies.append(e.state)

And finally in your other, permanent level scene, once you’re back to it:

for enemy_state in Globals.enemies:
    var enemy = my_enemy_scene.instantiate()
    enemy.state = enemy_state
    enemies_node.add_child(enemy)

Now if you also have different types of enemies, you can store type of enemy in EnemyState obj also, then check for it in the last bit of code above. If you have this problem, lmk, there are many solutions to it, happy to share the one I’m using.

Hi Alex,

Thanks for the reply.

I thought/hoped that n.get_parent().remove_child( n ) would remove the node from the scene’s tree and therefore stop it from being queue_free’d. Is that a mistaken belief?

I was also hoping to avoid the whole intermediate object (like EnemyState) and just save the real nodes, but if saving the real nodes isn’t possible, then I’ll go down the route you suggested.

Thanks.

Oh, I must have missed that part of misread.

I have no problems with doing this in my code. My Game autoload singleton:

var enemies: Array = []

On my Pause screen, I save enemies like this:

for e in Game.level.enemies.get_children():
	Game.enemies.append(e)
	
for e in Game.enemies:
	Game.level.enemies.remove_child(e)

(yes, I also store reference to level in my Game obj, but even if I set that to null when I hit “quit” button on pause menu, it doesn’t change outcome and enemies persist)

In my Level:

func _ready():
	print("starting level with enemies: " + str(Game.enemies))
	for e in Game.enemies:
		print("is " + str(e) + " valid: " + str(is_instance_valid(e)))	

Output is as expected. First time I start the level, enemies is empty. Then I hit pause, quit back to main menu (changes scene), and start new game (changes scene again, back to Level) and now I get enemies from previous time I ran the level:

Ok, so that looks similar to what I’m doing (except I do both the append and detach in one for loop.)

Two questions about your code:

  1. What does the code look like to add from Game.enemies to Game.level.enemies?
  2. What is Game? Is it a Godot thing I don’t know about? or is it a preloaded scene? or a preloaded script?

Here’s my code for restoring:

func restore_all_children( node_name : String, nodes : Array[Node] ):
	
	var node : Node = Globals.current_scene_node.get_node( node_name )
	for n in nodes:
		print( "Adding ", n, " valid: ", is_instance_valid( n ))
		node.call_deferred( "add_child",  n )

The print shows I am adding the right number of nodes and they are valid.
I’m using call_deferred because I got an error saying I should use it :slight_smile:

And I just solved the last part of the puzzle, an embarrassing bug on my part :slight_smile: I was adding the enemies to the scene before changing it…

You should never be modifying the array while looping through it as it may lead to unintended behavior. You can modify values stored in the array, yes, but not the array structure itself (number of elements or their order).

You just iterate over Game.enemies and add each as child to level.enemies. Here, “level” is your level scene and “enemies” is a node in the scene tree that contains enemies.

It’s just the name I chose for my autoloaded singleton that stores game state. Mine looks like this right now, but none of this applies to your question, just figured I’d share:

extends Node
# stores global game data

var selected_char: String
var player: Player
var level: Level
var save_file: GameSave

func _ready():
	save_file = GameSave.load()
	if not save_file:
		save_file = GameSave.new()
		
func get_elapsed_time() -> float:
	return 0.0 if not level else level.elapsed_time_sec

GameSave is my custom resource that stores save data.

With all this said, be careful storing enemy scenes between levels like this. It can work fine if enemies are just one sprite with no timers, etc (i.e. very simple). But what if you have timers going on? When they timeout, the code that gets called on timers’ timeout will likely expect the enemy object to be a part of the tree, but now it’s not, and this can cause runtime errors. So now you have to stop all timers before removing the enemy scene out of the tree. This means that either your level scene now has to know about enemy’s internal implementation (bad design), or enemy objects now have a method to turn off timers. In other words, enemy objects now need a method to get them to a… drumroll state that makes them suitable for removal from scene tree. And that is no different than handling other object state, so in the the end you might as well just use a class that encapsulates object state and pass that between levels instead of passing around actual Enemy nodes/scenes.

When you go the route of storing state, you’re separating state from all behavior and logic, which seems like more work but is a more reliable and cleaner way to go about it. So while yes, as I’ve shown, you can remove Nodes from tree and re-add them in another level, it is my recommendation that you do not do this and instead pass around an object for each enemy that represents state of the enemy. Each enemy then can simply accept state as an optional parameter in it’s _init() func, and have a get_state() func that returns the current state. This approach you can reuse in many places in programming so it’s honestly a really good thing to learn if you haven’t already, imho.

Good point about the iterating and modifying the array while iterating it, as a C++ programmer, I should have known that :smile: :frowning:

Also, good point about timers. While I don’t have any at the moment, it’s going to be easy to add one and get confused when the :poop: hits the fan.

1 Like

Technical debt, our sweet old friend… :smiling_face_with_tear: