Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

change_scene_to_* needs to await for two process_frames #86286

Open
timothyqiu opened this issue Dec 18, 2023 · 9 comments
Open

change_scene_to_* needs to await for two process_frames #86286

timothyqiu opened this issue Dec 18, 2023 · 9 comments

Comments

@timothyqiu
Copy link
Member

timothyqiu commented Dec 18, 2023

Tested versions

Godot 4.2+

System information

Arch Linux

Issue description

change_scene_to_*() methods always change scene in a deferred way.

The documentation says scene change happens at the end of the frame. But I have to await SceneTree.process_frame twice instead of once since #78988.

1. The current scene node is immediately removed from the tree. From that point, [method Node.get_tree] called on the current (outgoing) scene will return [code]null[/code]. [member current_scene] will be [code]null[/code], too, because the new scene is not available yet.
2. At the end of the frame, the formerly current scene, already removed from the tree, will be deleted (freed from memory) and then the new scene will be instantiated and added to the tree. [method Node.get_tree] and [member current_scene] will be back to working as usual.

var tree := get_tree()

tree.change_scene_to_file(path)

await tree.process_frame
# expected & old behavior: new scene available here
# actual: still not available

await tree.process_frame
# actual: new scene available here

Steps to reproduce

See above, or use the MRP.

Minimal reproduction project (MRP)

test-change-scene.zip

@timothyqiu
Copy link
Member Author

Okay, I thought it's because I put the code snippet inside _unhandled_input() and process_frame signals the start of process().

But doesn't _unhandled_input() happen at the end of last frame by default?

@timothyqiu timothyqiu reopened this Dec 18, 2023
@Sauermann
Copy link
Contributor

_unhandled_input() happens without delay right after _input() and the other _*_input() calls.
The execution of physics picking happens asynchronously in the next frame.

@timothyqiu
Copy link
Member Author

@Sauermann Where does it happen inside a frame relative to _process()?

  • Before _process(): Then that explains it. But what signal should I await for (besides awaiting for process_frame twice) in order to access the new scene?
  • After _process(): Then it's a bug.

@Sauermann
Copy link
Contributor

That I don't know. I haven't yet looked into the order of things happening within a single frame.

@Lippanon
Copy link

func change_scene(path: String) -> void:
	print("------ changing to %s ------" % path)
	var tree := get_tree()
	
	print("[Before] %s" % tree.current_scene, " - frame: ", Engine.get_process_frames())
	tree.change_scene_to_file(path)
	
	await tree.process_frame
	print("[after tree.process_frame] %s" % tree.current_scene, " - frame: ", Engine.get_process_frames())
	
	await tree.process_frame
	print("[after tree.process_frame] %s" % tree.current_scene, " - frame: ", Engine.get_process_frames())

Output:

[Before] OldScene:<Node2D#27095205063> - frame: 65
[after tree.process_frame] <Object#null> - frame: 65
[after tree.process_frame] NewScene:<Node2D#29645341898> - frame: 66

The first await isn't really waiting, when the call comes from _unhandled_input or _input.
However, if you instead use _process:

func _process(delta: float) -> void:
	if Input.is_action_just_pressed("ui_accept"):
		Autoload.change_scene("res://old_scene.tscn")

The output will now be:

[Before] OldScene:<Node2D#27095205063> - frame: 101
[after tree.process_frame 1] NewScene:<Node2D#31088182474> - frame: 102
[after tree.process_frame 2] NewScene:<Node2D#31088182474> - frame: 103

If nothing else, this works as a work-around for the MRP.

@YuriSizov
Copy link
Contributor

This seems like an important caveat with await tree.process_frame that we should document.

@kleonc
Copy link
Member

kleonc commented Dec 18, 2023

Where does it happen inside a frame relative to _process()?

  • Before _process(): Then that explains it. But what signal should I await for (besides awaiting for process_frame twice) in order to access the new scene?

  • After _process(): Then it's a bug.

It's kinda irrelevant whether it happens before/after _process(). If change_scene_to_* is called anywhere outside process() (within the red interval in the diagram below) then it's guaranteed that after awaiting process_frame signal the scene change has not happened yet because flushing scene change is done within process() after emitting process_frame signal.

The same can be told about the black interval in the diagram below.

For awaiting single process_frame to be enough the change_scene_to_* would need to be called somewhere within the pink interval (so e.g. in _process(), like in #86286 (comment)).

dCDWa8XAe4

Note

I kinda ignored threading when analyzing this so it might be not that simple. 🙃


Anyway here's I think a somehow reliable way to detect/await the scene change:

extends Node

signal _scene_changed

@onready var _tree: SceneTree = get_tree()

func change_scene(path: String) -> Error:
	var error: Error = _tree.change_scene_to_file(path)
	if error == OK:
		if not _tree.node_added.is_connected(_on_node_added):
			_tree.node_added.connect(_on_node_added)
		await _scene_changed
		# Scene changed (and ready).
	return error

func _on_node_added(node: Node) -> void:
	if node == _tree.current_scene:
		_tree.node_added.disconnect(_on_node_added)
		# Here `_tree.current_scene` is already changed.
		# But note we are in the middle of executing `tree.root.add_child(current_scene)`.
		# So likely deferring it a little further is wanted, e.g. until its `ready` signal.
		await _tree.current_scene.ready
		_scene_changed.emit()

Given it's autoloaded as SceneChanger it could be used like var error = await SceneChanger.change_scene(path).

It works because current_scene is assigned before add_child():

void SceneTree::_flush_scene_change() {
if (prev_scene) {
memdelete(prev_scene);
prev_scene = nullptr;
}
current_scene = pending_new_scene;
root->add_child(pending_new_scene);
pending_new_scene = nullptr;
// Update display for cursor instantly.
root->update_mouse_cursor_state();
}

@timothyqiu
Copy link
Member Author

Thanks for the workaround. I think await tree.tree_changed also works if the user can guarentee autoloads don't touch the scene tree during the scene change.

@berarma
Copy link
Contributor

berarma commented Nov 7, 2024

I wish there was a signal scene_changed built into SceneTree. There's no signal that I know which happens at the end of the frame, after the physics and process calls. That's when the scene change happens.

Is there some proposal for this to happen?

For reference, here's a processing diagram that I did for Godot 4. Here we can see how the input is handled at the very start of the frame and the scene is changed after the SceneTree.process_frame signal. Exactly as @kleonc explained.

Another possible workaround could be a timer in idle processing mode. It would trigger just after the scene change. So await get_tree().create_timer(0).timeout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants