@export vars within scene reset on instantiate

Godot Version

4.2.1

Question

I have a scene with @export vars that has been initialized in multiple levels.
The first level works perfectly fine(which was also instantiated by my scene_manager). When i instantiate the next level, the @export variable defaults to the scene’s (not the instance’s) value.

(tried to include images but the forum stopped me)
this works for one scene but not the other, and both are brought in the same way.

extends Area2D

@export var the_entrance: Node2D
@export var transition_name: String
@export var scene_to_load: PackedScene:
	set(value):
		scene_to_load = value
var should_load: bool = false

func _process(delta):
	if should_load:
		should_load = false
		SceneManager.goto_scene(scene_to_load.resource_path)

Is this a bug or am i using the system wrong? I’m honestly rather new to Godot so maybe i am expecting these to work like Unity’s [SerializeField].

From the code you provided, are the export variables that are taking the default values in the scene_to_load?

If so, then I imagine when goto_scene is called it instantiates the scene_to_load, and since you’re not then explicitly setting the export vars, its using the scene’s default values. You should be able to then set the export vars by setting them in code, e.g. scene_to_load.export_var = new_value.

Having not used PackedScene’s often, I might be wrong, but my approach is along these lines and it works fine:

  1. Sub-scenes (e.g. enemy, pickups, other entities) have export vars
  2. Level scenes contain numerous sub-scenes with values set for exports
  3. Scene manager selects “LevelScene” to load which it then instantiates. Level scenes don’t contain there own exports, so nothing needs setting when they’re loaded.

Since this is my first time implementing this type of thing, maybe i’m going about it entirely wrong,
my thought was this.

  1. Make a Scene that contains an Area2D and a script.

  2. instance that Scene in any area i want as a way to leave one level and enter another.

  3. change the export vars to tell it where that “portal” should lead to

  4. call SceneManager with that Scene to go there.

is there a better way to implement this?

That sounds pretty reasonable to me. You area is essentially a doorway to another scene, and contains export vars that configure which scene will be loaded when the player exits the doorway.

And those export vars aren’t working? Can you post some code to look at?

In your code, i assume scene_to_load is the scene that loads with the default values (aka the values that are set in the PackedScene)?

Could you post the code of your SceneManager goto_scene method?

I apologize in advance for some sorry-looking code and appreciate the help.

rest of the Area2D’s script

extends Area2D

@export var the_entrance: Node2D
@export var transition_name: String
@export var scene_to_load: PackedScene:
	set(value):
		scene_to_load = valuew
var should_load: bool = false

func _ready():
	pass

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	if should_load:
		should_load = false
		SceneManager.goto_scene(scene_to_load.resource_path)


func _on_body_entered(body):
	if body.get_groups()[0] == "Players":
		should_load = true
		body.areaTransitionName = transition_name

The issue im having is after we already loaded the ‘temp’ scene, and would be calling goto_scene again to unload the temp scene to go back to the Town scene we came from.

SceneManager

extends Node

var current_scene = null
var prev_scene = null
@onready var root = get_tree().root

func _ready():
	#var root = get_tree().root
	current_scene = root.get_child(root.get_child_count() - 1)
	print(current_scene.name)

func goto_scene(path):
	if current_scene == null:
		current_scene = root.get_child(root.get_child_count() - 1)
	if prev_scene != null:
		call_deferred("exit_temp_scene")
	elif current_scene.name == "Town":
		call_deferred("_deferred_goto_temp_scene", path)
	else:
		call_deferred("_deferred_goto_scene", path)
	
func _deferred_goto_scene(path):
	if current_scene.has_method("save_character"):
		current_scene.save_character()
	current_scene.free()
	
	var s = ResourceLoader.load(path)
	current_scene = s.instantiate()
	if current_scene.has_method("load_character"):
		current_scene.load_character()
	
	get_tree().root.add_child(current_scene)
	get_tree().current_scene = current_scene
	
func _deferred_goto_temp_scene(path):
	prev_scene = current_scene
	
	var s = ResourceLoader.load(path)
	current_scene = s.instantiate()
		
	get_tree().root.add_child(current_scene)
	prev_scene.get_node("PlayerPackage").reparent(current_scene)
	get_tree().root.remove_child(prev_scene)
	get_tree().current_scene = current_scene
	
func exit_temp_scene():
	get_tree().root.add_child(prev_scene)	
	current_scene.get_node("PlayerPackage").reparent(prev_scene)
	current_scene.free()
	current_scene = prev_scene
	prev_scene = null
	get_tree().current_scene = current_scene

The first level after the main screen i am loading uses the first _deferred_goto_scene(), whereas the method being used when i am encountering this is the _deferred_goto_temp_scene(), as i was originally going to keep the prior scene loaded to continue doing processing.

Considering this issue is noticeable in debug by the 'current_scene = s.instantiate()" line, could this be due to the fact that I’m not .free()-ing the prior scene?

That is the only difference at this point of the load process… but i eventually want NPCs with their own patterns to be processed while the player is in other levels, and I’m unsure of how to accomplish this if i need to .free() each scene as i leave it.

That issue may be it’s own and requiring a separate thread, however.

So, I was researching another test project i had, and when trying to reload this one, i got errors about broken dependencies…

Apparently, having 2 scenes that have each other as values causes an issue as it can resolve one without loading the other. basically preventing one from working…

So it’s back to the drawing board…

EDIT: If i just used NodePaths to the scenes instead of trying to attach the scene itself, it works fine…

I’ve got a bit lost in where the problem is now, as having 2 scenes reference each other is a slightly different issue than the export vars issue. However, avoiding circular dependencies is generally a good idea, especially in your case, where it clearly breaks the design you wanted to follow.

I guess the key question is why do you want scenes to reference each other directly? Is it for sharing (or moving) state between them, or is there another reason?

I’m not sure if this is at all helpful, but I personally use ResourcePaths (e.g. res://) for navigating between scenes, and if there’s any state that needs to cross between them, I do it through an autoload.

Essentially my design looks roughly like this:

  • GameManager (autoload) - stores state for the game (e.g. player lives, score, and other data that needs to survive across scenes). It also contains a dictionary with scene names and res paths, and some functions to navigate between the scenes, for example, load_scene(scene_name : String).
  • EventBus (autoload) - defines a load of game-related signals
  • ocean_scene.tscn - contains my ocean scene.
  • forest_scene.tscn - contains my forest scene.
  • interstitial_scene.tscn - contains a transient scene for showing when transitioning.

A scene can then signal it wants a transition by emitting the navigate_scene(scene_name : String) signal. The GameManager listens for that signal, and then handles loading the interstitial_scene, loading the next scene, and transferring between them. Any state that needs to cross between the scenes is held by the game manager.

The scenes are therefore loosely coupled and know v.little about each other, except for each others names.

1 Like

This was something I’ve started to realize as well as i looked further into the issue.

I am very likely going to do exactly that and redesign my SceneManager to be the master of the scene references.

Since Autoloads are generally siblings of the node that get added to root, does that bring in complications with listening to the signals being created by the scenes or their children?
Or does having it as an Autoload simplify that?

May i also ask if you just switch between scenes within the root or do you keep them contained in another scene(i.e. Main)?

Thank you for this as I’ve had a hard time gleaming this information from the Docs.

My approach to my “game level” events is broadly inline with the GDQuest approach outlined in The Events bus singleton · GDQuest .

My scenes have a dependency on the event bus, in that they fire the signals it defines, and my GameManager has a dependency on the event bus, in that it listens for those signals. The event bus being a sibling of the root-added nodes hasn’t caused me any problems, and I believe its fine.

And I switch within the root, rather than containing them in another scene. I actually use Glass Brick’s scene manager to do the transitions (GitHub - glass-brick/Scene-Manager: Godot plugin for managing scenes and transitions), and it’s code looks like this:

func _replace_scene(path: String, options: Dictionary) -> void:
	_current_scene.queue_free()
	scene_unloaded.emit()
	var following_scene: PackedScene = ResourceLoader.load(path, "PackedScene", 0)
	_current_scene = following_scene.instantiate()
	_current_scene.tree_entered.connect(options["on_tree_enter"].bind(_current_scene))
	_current_scene.ready.connect(options["on_ready"].bind(_current_scene))
	await _tree.create_timer(0.0).timeout
	_root.add_child(_current_scene)
	_tree.set_current_scene(_current_scene)
1 Like

Ah ok, I should definitely refer to more of the GDQuest content going forward then.

and i assume your GameManager maintains your player states as well as handles their loading into the level?

I was planning on having NPCs that continue to follow their patterns even when the player is in another scene, but there may be a better method to simulate that than to actually have the characters and scene being processed.

In my case there’s actually v.little player state, so little, in fact, that the in-scene player doesn’t need to know any of the state. If my in-level player needed the state then I guess I’d have done what you suggested, and had the GameManager load it into the scene player.

The NPCs continuing to run is also an interesting challenge that I haven’t had to deal with. Clearly you’ll need something that maintains some of their state, and maybe runs or simulates a crude version of their in-scene behaviour.

If I had to retrofit it to my game I think I’d add some kind of NPCManager class, and possibly hang that off my GameManager, but there might be better approaches.

The NPCManager idea is something I’ve seen brought up in other areas, so i will investigate this concept further.

Thanks for all of your help!

1 Like

Oh, and might be worth checking out this thread on reddit:
https://www.reddit.com/r/godot/comments/l6i6an/could_i_get_an_npc_to_move_between_scenes_on_its/

The idea of running some minimal NPC code makes sense, so it might be the NPCManager singleton is used to “manage” NPCs that move in or out of scene

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.