@tool @icon("dynamic_area_loader.svg") class_name DynamicAreaLoader3D extends Area3D ## An [Area3D] that dynamicly load a scene based on proximity. ## ## [b]Note:[/b] By default the [signal body_entered] and [signal body_exited] signals are linked ## to private [method _on_body_entered] and [method _on_body_exited] functions that check ## if the [param body] is the [member GameGlobals.player] and (un)load the area based on that. ## The signal emitted when the area to be loaded finished loading and was added to the scene tree.[br] ## You can combine this for eg. a door that only opens once the area behind it finished loading. signal load_finished(loaded_node: Node) ## The node to load and add.[br][br] ## [b]IMPORTANT:[/b] The node needs to be marked as [b]Load as Placeholder[/b], otherwise nothing ## happens when loading (see [InstancePlaceholder]). @export var placeholder_node: Node: set(value): placeholder_node = value update_configuration_warnings() ## If [code]true[/code], the actual loading will be done separately in a thread.[br] ## This prevents the game from freezing for a moment when loading a large scene.[br] ## However, loading can be slower on lower-end hardware. @export var threaded: bool = true ## How long [i](in seconds)[/i] the area will still be loaded and inside the scene tree ## when the [PlayerCharacter] exits this area.[br]This is to prevent the [PlayerCharacter] ## from keep re-loading the area by just walking back and fourth through the load zones. @export_range(0.0, 10.0, 0.01, "or_greater", "suffix:s") var keep_loaded_duration: float = 3.0 ## Automatically load the area when entering the scene tree, if the [member GameGlobals.spawn_point] ## is set to any of these values. @export var loaded_if_spawnpoint: Array[int] = [] ## The reference to the currently loaded node that was loaded. var loaded_node: Node func _ready() -> void: if Engine.is_editor_hint(): return if loaded_if_spawnpoint.has(GameGlobals.spawn_index): load_area(null, true) body_entered.connect(_on_body_entered) body_exited.connect(_on_body_exited) ## Instances the area defined by [member placeholder_node].[br] ## See [method load_area_threaded] if you want to load the area in a thread instead ## of having a freeze during load.[br][br] ## If [param forced] is [code]true[/code], don't check if the player is colliding when calling this function. func load_area(custom_scene: PackedScene = null, forced: bool = false) -> void: assert(is_instance_valid(placeholder_node), "[placeholder_node] needs to be set.") assert(placeholder_node is InstancePlaceholder, "[placeholder_node] needs to be marked as 'Load As Placeholder'.") if is_instance_valid(loaded_node) or (not forced and not overlaps_body(GameGlobals.get_player())): return loaded_node = placeholder_node.create_instance(false, custom_scene) load_finished.emit(loaded_node) ## Loads the scene defined in ## [member placeholder_node] ([member InstancePlaceholder.get_instance_path]) threaded.[br] ## Calls [method load_area] with the [param custom_scene] set to the loaded scene.[br][br] ## [param forced] does the same thing as in [method load_area]. func load_area_threaded(forced: bool = false) -> void: assert(is_instance_valid(placeholder_node), "[placeholder_node] needs to be set.") assert(placeholder_node is InstancePlaceholder, "[placeholder_node] needs to be marked as 'Load As Placeholder'.") placeholder_node = placeholder_node as InstancePlaceholder var path: String = placeholder_node.get_instance_path() ResourceLoader.load_threaded_request(path, "", true) while ResourceLoader.load_threaded_get_status(path) == ResourceLoader.THREAD_LOAD_IN_PROGRESS: if not is_inside_tree(): return await get_tree().process_frame var scene: PackedScene = ResourceLoader.load_threaded_get(path) load_area(scene, forced) ## Unloads ([method Node.queue_free]) the [member loaded_node].[br] ## If [param instantly_unload] is [code]true[/code], don't wait for the ## [member keep_loaded_duration] to end and instantly free the node. func unload_area(instantly_unload: bool = false) -> void: if not is_instance_valid(loaded_node): return if not instantly_unload and keep_loaded_duration > 0.0: var duration: float = keep_loaded_duration while is_inside_tree() and duration > 0.0: if overlaps_body(GameGlobals.get_player()): return duration -= get_process_delta_time() await get_tree().process_frame loaded_node.queue_free() func _on_body_entered(body: Node3D) -> void: if body == GameGlobals.get_player(): if threaded: load_area_threaded() else: load_area() func _on_body_exited(body: Node3D) -> void: if body == GameGlobals.get_player(): unload_area() func _get_configuration_warnings() -> PackedStringArray: if not is_instance_valid(placeholder_node): return ["'placeholder_node' is not set."] # TODO: Find a way to detect that in the editor. if placeholder_node is not InstancePlaceholder and false: return ["'placeholder_node' needs to be marked as 'Load As Placeholder'"] return []