class_name Interactor extends Node3D const EnterSignals: Array[String] = ["area_entered", "body_entered"] const ExitSignals: Array[String] = ["area_exited", "body_exited"] @export var area: Area3D @export var root_node: Node = self class InteractableNode: var _body: CollisionObject3D var _interactable: Interactable var global_position: Vector3: get: return _body.global_position func _init(body: CollisionObject3D, interactable: Interactable): _body = body _interactable = interactable func interact(interactor: Node): _interactable.interact(interactor) func squared_distance_to(b: InteractableNode) -> float: return global_position.distance_squared_to(b.global_position) var interactables: Array[InteractableNode] = [] var _sorted: bool = false func _ready() -> void: _connect_many(area, EnterSignals, _on_object_entered) _connect_many(area, ExitSignals, _on_object_exited) func _connect_many(node: Node, signals: Array[String], fn: Callable): for signal_name in signals: node.connect(signal_name, fn) func _on_object_entered(object: CollisionObject3D): _sorted = false var child = NodeExt.find_child_variant(object, Interactable) if child.is_some(): interactables.append(InteractableNode.new(object, child.unwrap() as Interactable)) func is_same_object(object: CollisionObject3D, node: InteractableNode) -> bool: return object == node._body func _on_object_exited(object: CollisionObject3D): var index = interactables.find_custom( func(node): return is_same_object(object, node) ) if index >= 0: interactables.remove_at(index) func _distance_from(node: InteractableNode) -> float: return global_position.distance_squared_to(node.global_position) func _distance_compare(a: InteractableNode, b: InteractableNode): return _distance_from(a) > _distance_from(b) func _sort_by_distance(): interactables.sort_custom(_distance_compare) _sorted = true func interact(interactable: InteractableNode): interactable.interact(root_node) func interact_nearest(): if not _sorted: _sort_by_distance() if len(interactables) > 0: return interact(interactables[0])