Last active
April 8, 2025 19:32
-
-
Save joelgomes1994/18c9430d55617d0042a9ed295e0c0662 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class_name ThreadInstancer | |
extends Node | |
## Instantiate a scene in a thread and adds its nodes to the scene tree in batches. | |
# Signals | |
signal scene_loaded(scene: PackedScene) | |
signal root_node_instantiated(node: Node) | |
signal node_instantiated(node: Node) | |
signal batch_instantiated | |
signal all_nodes_instantiated | |
# Variables | |
## Scene to be instanced. | |
@export var scene: PackedScene | |
## Optional scene path to be loaded in another thread. | |
@export_file("*.tscn", "*.scn") var scene_path: String | |
## Amount of nodes to be instanced each iteration. | |
@export_range(0, 1000) var iteration_amount := 100 | |
## Amount of frames to sleep each iteration. | |
@export_range(1, 1000) var sleep_frames := 5 | |
## Automatically add [code]root_node[/code] as child of current parent. | |
@export var auto_parent := false | |
var state: SceneState | |
var triggered := false | |
var cancelled := false | |
var root_node: Node | |
var node_index := 0 | |
var processed_nodes := 0 | |
var ignored_nodes := 0 | |
# Built-in overrides | |
func _ready() -> void: | |
if not scene and not scene_path: | |
push_error("Invalid instancer parameters: %s" % get_path()) | |
queue_free() | |
return | |
node_instantiated.connect(_on_node_instantiated) | |
batch_instantiated.connect(_on_batch_instantiated) | |
if not scene and scene_path: | |
scene = await scene_load_thread(scene_path).scene_loaded | |
if scene: | |
set_scene(scene) | |
instantiate() | |
func _exit_tree() -> void: | |
cancelled = true | |
# Public methods | |
## Load scene in another thread. Await for [code]scene_loaded[/code] for the result. | |
func scene_load_thread(_scene_path: String) -> ThreadInstancer: | |
var thread := Thread.new() | |
if thread.is_started(): | |
thread.wait_to_finish() | |
thread.start(func(): | |
if cancelled: | |
(func(): scene_loaded.emit(null)).call_deferred() | |
thread.wait_to_finish.call_deferred() | |
return | |
if not ResourceLoader.exists(_scene_path): | |
prints("Invalid scene:", _scene_path) | |
(func(): scene_loaded.emit(null)).call_deferred() | |
thread.wait_to_finish.call_deferred() | |
return | |
prints("Async loading scene: %s)" % _scene_path.get_file()) | |
var _scene: PackedScene = load(_scene_path) | |
prints("Scene loaded: %s" % _scene_path.get_file()) | |
(func(): scene_loaded.emit(_scene)).call_deferred() | |
thread.wait_to_finish.call_deferred() | |
) | |
return self | |
## Set scene and initialize state. | |
func set_scene(_scene: PackedScene) -> void: | |
scene = _scene | |
if not scene: | |
return | |
state = scene.get_state() | |
## Get node info from scene state. | |
func get_node_info(i: int) -> Dictionary: | |
var info := { | |
&"index": i, | |
&"name": state.get_node_name(i), | |
&"placeholder": state.get_node_instance_placeholder(i), | |
&"instance": state.get_node_instance(i), | |
&"type": state.get_node_type(i), | |
&"path": state.get_node_path(i), | |
&"parent": NodePath(str(state.get_node_path(i)).get_base_dir()) if i > 0 else NodePath(), | |
&"groups": state.get_node_groups(i), | |
&"properties": [], | |
} | |
for p in state.get_node_property_count(i): | |
info.properties.append({ | |
&"name": state.get_node_property_name(i, p), | |
&"value": state.get_node_property_value(i, p), | |
}) | |
return info | |
## Instantiate [code]scene[/code] in iterations. | |
func instantiate() -> void: | |
if triggered: | |
return | |
triggered = true | |
prints("Instantiating async scene: %s" % scene.resource_path.get_file()) | |
begin_batch_instantiate() | |
## Instantiate root node and set its properties. | |
func instantiate_root_node(node_info: Dictionary) -> void: | |
var node := node_instantiate(node_info) | |
node.scene_file_path = scene.resource_path | |
root_node = node | |
if auto_parent: | |
get_parent().add_child.call_deferred(root_node) | |
root_node_instantiated.emit(root_node) | |
## Begin batch instantiation from the last index. | |
func begin_batch_instantiate() -> void: | |
var node_infos: Array[Dictionary] = [] | |
for i in range(node_index, state.get_node_count()): | |
var node_info := get_node_info(i) | |
node_index = i + 1 | |
if i == 0: | |
instantiate_root_node(node_info) | |
continue | |
if node_info.placeholder: | |
ignored_nodes += 1 | |
continue | |
node_infos.push_back(node_info) | |
if i > 0 and i % iteration_amount == 0: | |
node_index += 1 | |
break | |
if node_infos.size() > 0: | |
batch_instantiate_thread(node_infos) | |
else: | |
prints("Scene instantiated: %s (%s/%s) (%s ignored)" % [scene.resource_path.get_file(), node_index, state.get_node_count(), ignored_nodes]) | |
queue_free() | |
## Batch instantiate nodes in another thread. | |
func batch_instantiate_thread(node_infos: Array[Dictionary]) -> ThreadInstancer: | |
var thread := Thread.new() | |
thread.start(func(): | |
for node_info in node_infos: | |
if cancelled: | |
break | |
var node := node_instantiate(node_info) | |
if not node: | |
continue | |
(func(): node_instantiated.emit(node)).call_deferred() | |
(func(): batch_instantiated.emit()).call_deferred() | |
thread.wait_to_finish.call_deferred() | |
) | |
return self | |
## Instantiate and set node properties. | |
func node_instantiate(node_info: Dictionary) -> Node: | |
var node: Node | |
if node_info.instance: | |
node = node_info.instance.instantiate() | |
elif node_info.type: | |
node = ClassDB.instantiate(node_info.type) | |
if not node: | |
push_warning("Invalid node: %s" % node_info.path) | |
return null | |
node.name = node_info.name | |
for group in node_info.groups: | |
if not group in node.get_groups(): | |
node.add_to_group(group) | |
for prop in node_info.properties: | |
node.set(prop.name, prop.value) | |
node.set_meta("node_info", node_info) | |
node.set_meta("path", node_info.path) | |
return node | |
# Event handlers | |
## Add instantiated as child of its parent. | |
func _on_node_instantiated(node: Node) -> void: | |
var node_info: Dictionary = node.get_meta("node_info") | |
node.set_meta("node_info", null) | |
var parent_node: Node = root_node.get_node(node_info.parent) | |
parent_node.add_child(node) | |
processed_nodes += 1 | |
## Wait after batch end and begin next batch. | |
func _on_batch_instantiated() -> void: | |
for i in sleep_frames: | |
await get_tree().process_frame | |
prints("Batch instantiated (%s/%s)" % [node_index, state.get_node_count()]) | |
begin_batch_instantiate() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment