Issue with export PackedScene var becomming null when parent instantiated

Godot Version

v4.5.stable.official [876b29033]

Question

Hello all! I’m hoping to get advice with options to solve my issue and if it is caused by my implementation or a bug with the engine.

In short I’m building a system to handle level selection and sending the player back to the hub scene; however, one of my instanced custom class’s export variable becomes null in very specific circumstances.


Before I get into my custom warp class I should probably explain my project structure as that is likely the cause of the issue.

The project’s Main Scene is set to main.tscn and has a custom class root node called ‘Main’:

extends Node
class_name Main

@onready var scene_holder: Node = %SceneHolder
@onready var menu_holder: Node = %MenuHolder

@export var title_scene:PackedScene = null
@export var menu_scene:PackedScene = null

var scene:Scene = null
var menu:Menu = null
var allow_menu_toggle = false


func _ready() -> void:
	update_scene(title_scene)
	menu = menu_scene.instantiate()
	menu_holder.add_child(menu)
	menu.canvas_layer.visible = false
	menu.connect('scene_update_request',update_scene)


func _input(event: InputEvent) -> void:
	if allow_menu_toggle and event.is_action_pressed("ToggleMenu"):
		menu.canvas_layer.visible = !menu.canvas_layer.visible


func update_scene(new_scene:PackedScene) -> void:
	var scenes := scene_holder.get_children()
	for s in scenes: 
		s.disconnect('scene_update_request',update_scene)
		s.disconnect('allow_menu_toggle',update_allow_menu_toggle)
		s.queue_free()
	scene = new_scene.instantiate()
	scene_holder.add_child(scene)
	scene.connect('scene_update_request',update_scene)
	scene.connect('allow_menu_toggle',update_allow_menu_toggle)


func update_allow_menu_toggle(allow:bool,toggle_on:bool=false) -> void:
	allow_menu_toggle = allow
	if allow and toggle_on: menu.canvas_layer.visible = true

The idea being that with the above class object I can have a settings menu as a child of MenuHolder and the current level as a child of SceneHolder. This should allow me to update the current level without disrupting the continuity of the menu settings and other game state variables stored in Main without having to save/load data each time a level is changed.

Note the title_scene and menu_scene are export vars defined in main.tscn. For now the title_scene can just be pointed to the hub level for testing.


Which brings us to problem. I’ve created a Warp class that extends Area3D with the following code:

extends Area3D
class_name Warp

@export var warp_location:PackedScene


func _ready() -> void:
	print('warp_location: ',warp_location)
	connect("body_entered",_load_new_scene)


func _load_new_scene(body:PlayerController) -> void:
	print('warp_location: ',warp_location,' (in load)')
	if !warp_location or warp_location is not PackedScene: return
	if body is not PlayerController: return
	var main:Main = get_tree().get_first_node_in_group("Main")
	main.update_scene(warp_location)

The above class has the exported variable warp_location which will be the level loaded as a child of Main.SceneHolder via the update_scene function.

I have 2 levels, hub.tscn and hub2.tscn with an instance of the Warp class and the export variable assigned to the other level.

When I load into the hub.tscn I am able to trigger the update scene function without issue and am dropped into hub2.tscn; however, in hub2.tscn the warp fails as the warp_location variable is null. I verified this with the print statements I added to the Warp class. And when checking hub2.tscn I can see the export variable is assigned as hub.tscn

To add to the confusion I created a hub3.tscn level and when I point hub2’s warp to hub3.tscn it works without issue, but now hub3.tscn’s warp will not work when it references hub or hub2.

It seems that when I attempt to reference a PackedScene that has already been instantiated previously the Warp’s export variable becomes null.

The extra icing on top is when I close the project and open it up again I’m unable to open any of the hub tscn files due to a dependency issue. I can fix my clicking fix dependency and assigning another random packed scene in place where it used to reference the other hub.

Note: I am using export vars as I’m attempting to avoid hard-coding file paths. But maybe this is my only solution…


Output:

warp_location: <PackedScene#-9223371998233623023>
warp_location: <PackedScene#-9223371998233623023> (in load)
warp_location: <PackedScene#-9223371998384617970>
warp_location: <PackedScene#-9223371998384617970> (in load)
warp_location: <null>
warp_location: <null> (in load)

Is this a bug, is this the engine trying to prevent a circular dependency, do I have some misunderstanding in my logic?

Thanks for reading and providing thought!

Where are these PackedScenes being loaded?

This is the engine preventing circular dependency! @export is kind of like preloading the resources. For large scenes and circular changes it’s better to use @export_file("*.tscn") and load it as needed

1 Like

Thanks @gertkeno your insight has led to a solution! I had no idea that using @export was basically doing a pre_load() of the resource. So no wonder it was breaking… when you load a resource that is then trying to load you back.


Changes I made to solve the issue are:

  1. Updated Warp to use @export_file("*.tscn") as suggested. It now looks like:
@export_file("*.tscn") var warp_location:String
  1. Updated Main’s update_scene() method to the following:
func update_scene(new_scene) -> void:
	if new_scene is not String and new_scene is not PackedScene: return
	var scenes := scene_holder.get_children()
	for s in scenes: 
		s.disconnect('scene_update_request',update_scene)
		s.disconnect('allow_menu_toggle',update_allow_menu_toggle)
		s.queue_free()
	if new_scene is String: scene = load(new_scene).instantiate()
	elif new_scene is PackedScene: scene = new_scene.instantiate()
	scene_holder.add_child(scene)
	scene.connect('scene_update_request',update_scene)
	scene.connect('allow_menu_toggle',update_allow_menu_toggle)

I now don’t specify type for the new_scene var and added a check to return if the var is not a String or PackedScene

And I convert String to a Resource with load()


After updating the Warp objects in each hub file I can fly through and teleport through all 3 areas without issue!

Happy to solve the issue, not have to have a massive change in structure, and still not include hard-coded file paths!