MagicNStuff/source/components/collision/areas/dynamic_area_loader.gd
2025-02-25 22:07:11 +01:00

107 lines
3.8 KiB
GDScript

@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
## 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
## The reference to the currently loaded node that was loaded.
var loaded_node: Node
func _ready() -> void:
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.
func load_area(custom_scene: PackedScene = null) -> void:
if is_instance_valid(placeholder_node) and placeholder_node is InstancePlaceholder:
if is_instance_valid(loaded_node) or 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.
func load_area_threaded() -> void:
if is_instance_valid(loaded_node):
return
if not is_instance_valid(placeholder_node) or placeholder_node is not InstancePlaceholder:
return
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)
## 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()