How do I pass data between scenes WITHOUT using autoload/global variables?

Godot Version

4.2.1

Question

I’m making a grid based game at the moment and when a player steps onto a viable tile an enemy spawns. The enemy has variables such as a name, HP, Level etc. Enemies also don’t get removed when the player defeats them and they can go back to the same tile to fight the same enemy again. Hence the data associated with the enemy has to be permanent. It also needs to be unique to each enemy instance.

Issue is, I’m trying to pass this data onto a combat scene I made and I honestly don’t know how to. I don’t want to use autoload scripts since there can be a lot of enemies on the map at the same time and storing all the enemies the player has ever encountered in a global script doesn’t seem efficient.

This is the function that spawns the enemy and immediately moves the player to the combat scene:

func _on_area_2d_body_entered(body):
	var enemy = enemy_scene.instantiate()
	$TileMap/Area2D.call_deferred("add_child", enemy)
	get_tree().change_scene_to_file("res://Scenes/Game/CombatScene.tscn")

I don’t know all the details of your setup, but I’d probably set up the combat scene in away that you can pass an enemy object into it (I assume your enemy is a class).
Then your combat scene has all the data you’d need.

EDIT: sorry, just realized you’re changing the scene to a file.
In my case, I’d suggest not changing the whole scene, but instead instantiating the combat scene in your current game scene.
Of course this heavily depends on your setup, but creating a kind of game state for your main scene that flags when you’re in combat should enable you to basically have both active at the same time.

If that’s not possible for your kind of game, you could serialize all the data you need for the combat scene into a file and then have your combat scene load it. Depending on your data you can use JSON or a custom resource.

If you use get_tree().change_scene_to_file() or change_scene_to_packed then it will remove the currently loaded scene from the tree, so anything you added into the overworld scene while it was loaded, like that enemy you just created will be gone.

You could save all of the data you needed from the overworld before you leave it and restore it all when you load it again, but I think there’s a much simpler way to handle it:

You’ll want to make use of the process_mode of everything in your overworld scene that needs to be paused while you’re away, but if it’s all set to the default of Inherit then that’s exactly what you need already.

When you want to show a side-scene like the combat scene you first need to create an instance and add it to the tree, adding it into a CanvasLayer that is higher than the main scene’s canvas layer is a good way to make sure that nothing in the main scene will show up over the side-scene. Then the layer SideSceneLayer should have process_mode set to Pausable and the main scene will set itself to process_mode = Disabled when it adds a side scene.

So we do something like:

# In player or detector script or whatever
new_combat = preload("res://Scenes/Game/CombatScene.tscn").instantiate()
get_node("main scene node here").show_side_scene(new_combat)

# In the main scene root script
func show_side_scene(side_scene: Node) -> void:
  $SideSceneLayer.add_child(side_scene)
  process_mode = Node.PROCESS_MODE_DISABLED

# Connect to a signal on the canvas layer to be able to detect when
# the side scene is removed so we can re-enable the main scene.
func _on_SideSceneLayer_child_exiting_tree(_node: Node) -> void:
  process_mode = Node.PROCESS_MODE_PAUSABLE
  side_scene_finished.emit() # Maybe some objects want to know when we just came back

With this, anything in the main scene will have it’s processing disabled which should keep it suspended pretty nicely but there’s a few things to remember

  • If you are using global time values directly obviously you will have an issue when you resume
  • If you were using a get_tree().create_timer() that timer wont properly pause because the whole game isn’t paused, use a tween registered to the node itself (create_tween()) or a temporary Timer node instead
  • _process, _physics_process and _input wont be called on the suspended nodes but the physics engine itself wont be paused, however the default disable_mode on physics bodies removes them from the physics simulation when they have process disabled, you might need to play around with this setting on them if you use physics
  • Rendering might be faster if you hide some or all of the stuff in the main scene while it’s being covered (assuming you can’t see through the side scene) Hiding the main scene root should work because CanvasLayer doesn’t inherit the visibility of it’s parent.

This can also simplify passing data back and forth between the main scene and side scenes to pass in:

func show_side_scene(side_scene: Node, extra_stuff: Dictionary) -> void:
  $SideSceneLayer.add_child(side_scene)
  if extra_stuff:
    side_scene.custom_setup(extra_stuff) # Make sure that scene has a custom_setup method
  process_mode = Node.PROCESS_MODE_DISABLED

And to get stuff back, have the side scene emit a return_some_stuff signal and emit it right before it gets removed

# Whatever place we're starting the side scene
new_combat = preload("res://Scenes/Game/CombatScene.tscn").instantiate()
new_combat.return_some_data.connect(handle_stuff_returned, CONNECT_ONE_SHOT)
get_node("main scene node here").show_side_scene(new_combat)

func handle_stuff_returned(stuff: Dictionary) -> void:
  ...

If the side scene doesn’t always emit the return_some_stuff signal make sure to watch for when it leaves and disconnect the signal if you still have it connected.

2 Likes

I cover how I approach that in this video, the source code for which is available on itch for free. I don’t mean to just lazily drop a link but I made the video so I could easily help many :smiley:

2 Likes