My game has a custom level switcher for online multiplayer since it has to stay on the same scene. It stores the node of the current level in a variable called _current_level. When the level is cleared, it gets deleted with this line. This works totally fine in Godot 4.4. After I updated to Godot 4.5.1 the queue_free() call always crashes the game without error. This happens in both singleplayer and multiplayer modes so it’s not a specifically multiplayer bug. Is this a problem in Godot 4.5+? The only thing that works for me is downgrading engine version.
func clear_level() -> void:
if _current_level:
_current_level.queue_free()
I don’t know really, just doubt crashing on queue_free is a specific quirk of 4.5.1 . That would be a hard to miss flaw and you should be able to find it already reported at https://github.com/godotengine/godot/issues . Kinda more likely this is something with the project specifically.
The game crashes without throwing an error every time this happens in Godot 4.5+. I have tested the game multiple times with and without removing that line and it is always the source of the crash. Nothing crashes except freeing the previously loaded level in 4.5+.
Thanks for the help! The code works totally fine in every version of Godot except 4.5+. So the issue must be related to Godot in some strange way.
I am 100% sure that nothing gets printed to the debugger. No errors, warnings, anything useful.
Like you requested, this is the print output. As you can see, it’s nothing unexpected.
Lobby:<Node3D#272864642474> /root/RaceSession/Level/Lobby
Clear level is called after a call of load level which uses a resource loader threaded request. I don’t need to get into all the logic of how the multiplayer features work where it waits for all peers to load the scene but here is the some of basic functionality:
func _process(_delta: float) -> void:
if loading:
var progress : Array
var _scene_load_status = ResourceLoader.load_threaded_get_status(load_path, progress)
load_screen.set_load_progress(progress[0])
if _scene_load_status == ResourceLoader.THREAD_LOAD_LOADED:
loading = false
loaded_scene = ResourceLoader.load_threaded_get(load_path).instantiate()
I think something related to the resource loader may have changed in Godot 4.5 since loading stuff can be a little sensitive and cause crashes. That’s my best guess.
The clear level is basically called at the end of load step where it deletes the current one and adds in the new scene which was just loaded. And once again this works perfectly in Godot 4.4 and crashes without giving any reason or error only in Godot 4.5+.
I don’t think the resource loader has too much to do with it though since the crash happens on queue free. I was just throwing that out there but it’s likely not relevant to the problem.
Here’s the entire game session script. This is a scene that acts a custom level switcher to keep everything in the same scene for multiplayer.
_current_level is reinitialized in the finish_load() function. It sets it to the instance of the new level.
A new load is called when the player in singleplayer calls change_level() or the server calls that, which tells everyone to load the next level and not go to it until it has loaded for everyone. I’m not worried about this functionality because I have tested it and it works.
class_name GameSession
extends Node
@onready var level = $Level
@onready var load_screen = $LoadScreen
@export var lobby_path : String
var _current_level : Node
var loading : bool = false
var load_path : String
var _loaded_peers : Array[int] = []
var course_index : int = 0
var loaded_scene
func _ready() -> void:
GameManager.current_session = self
load_initial_lobby()
func reload_level() -> void:
change_level(load_path)
func _process(_delta: float) -> void:
if loading:
var progress : Array
var _scene_load_status = ResourceLoader.load_threaded_get_status(load_path, progress)
load_screen.set_load_progress(progress[0])
if _scene_load_status == ResourceLoader.THREAD_LOAD_LOADED:
loading = false
loaded_scene = ResourceLoader.load_threaded_get(load_path).instantiate()
rpc("peer_loaded", NetworkManager.get_peer_id())
load_screen.visible = not level.get_child_count() > 0
func load_lobby() -> void:
change_level(lobby_path)
func load_initial_lobby():
var lobby_instance = load(lobby_path).instantiate()
loaded_scene = lobby_instance
finish_load()
func change_level(level_path : String) -> void:
if not NetworkManager.is_server(): return
_loaded_peers.clear()
print("Changing level " + level_path)
if not loading:
rpc("load_level", level_path)
@rpc("authority", "call_local", "reliable")
func clear_level() -> void:
prints(_current_level, _current_level.get_path())
if _current_level:
_current_level.queue_free()
@rpc("authority", "call_local", "reliable")
func load_level(path : String) -> void:
load_path = path
ResourceLoader.load_threaded_request(path)
loading = true
load_screen.reset_load_progress()
clear_level()
@rpc("any_peer", "call_local", "reliable")
func peer_loaded(peer_id : int) -> void:
if is_multiplayer_authority():
_loaded_peers.append(peer_id)
if NetworkManager.online:
if _loaded_peers.size() == NetworkManager.get_peers().size():
rpc("finish_load")
else:
finish_load()
@rpc("authority", "call_local", "reliable")
func finish_load() -> void:
level.add_child(loaded_scene, true)
_current_level = loaded_scene
func next_course():
var current_course_series = GameManager.current_course_series
if course_index < current_course_series.size():
var next_level_path = current_course_series[course_index].course_path
change_level(next_level_path)
course_index += 1
else:
load_lobby()
course_index = 0
The level is a child node of the Level node. It seems to not crash immediately on the queue_free() call but at the same time the game doesn’t crash unless queue_free() is called.
Current level is a private variable and is only ever used in the game_session script. The script I sent earlier has every reference to _current_level that you can get. If that’s what you meant? Or do you mean who else holds a reference to the node that is stored in _current_level?
The latter. Comment out all code in other scripts that do something with _current_level node and see what happens with the crash.
_current_level is invalid between the time you call load_level() and the time the level finished loading. If someone held a reference to that node and tried to access it in this gap, the node won’t be there.
Ideally you’d want to swap at the same time when loading finishes, keeping _current_level valid at all times. Pseudo:
After searching across the game’s code, nothing else does. The root node of the level scene is not of concern to any other scripts since all the logic happens in the children. The root node has no functionality.
I even removed the current_level variable and tried this slightly hacky change but it still crashes:
func clear_level() -> void:
if level.get_child_count() > 0:
level.get_child(0).queue_free()
As much as this sounds like it might be related to a reference when the node does not exist, it’s still important to note that this only happens in 4.5+ and the logic was totally fine before. And I don’t know of anything in the game that would reference the root node of a level since it has no functionality.
I’ll try that code you just posted and see what happens! Btw thank you so much for the quick replies and all the help!
Can you make a minimal reproduction project without any rpc? That would also be helpful when reporting a bug if this is indeed a bug. The strange thing is that noting gets reported in the debugger.
It’s not just that node, anything underneath would count as well. If there are any references to anything underneath, they need to be updated as soon as _current_level is gone or swapped.