extends Node3D @onready var _controller := XRHelpers.get_xr_controller(self) @export var ray: RayCast3D @export var timespan_click = 200.0 # Called when the node enters the scene tree for the first time. func _ready(): _controller.button_pressed.connect(_on_button_pressed) _controller.button_released.connect(_on_button_released) var _last_collided: Object = null var _is_pressed := false var _is_grabbed := false var _time_pressed := 0.0 var _moved := false var _click_point := Vector3.ZERO func _physics_process(delta): _handle_enter_leave() _handle_move() func _handle_move(): var time_passed = Time.get_ticks_msec() - _time_pressed if time_passed <= timespan_click || (_is_pressed == false && _is_grabbed == false): return _moved = true if _is_pressed: _call_fn(_last_collided, "_on_press_move") if _is_grabbed: _call_fn(_last_collided, "_on_grab_move") func _handle_enter_leave(): var collider = ray.get_collider() if collider == _last_collided || _is_grabbed || _is_pressed: return _call_fn(collider, "_on_ray_enter") _call_fn(_last_collided, "_on_ray_leave") _last_collided = collider func _on_button_pressed(button): var collider = ray.get_collider() if collider == null: return match button: "trigger_click": _is_pressed = true _time_pressed = Time.get_ticks_msec() _click_point = ray.get_collision_point() _call_fn(collider, "_on_press_down") "grip_click": _is_grabbed = true _click_point = ray.get_collision_point() _call_fn(collider, "_on_grab_down") func _on_button_released(button): if _last_collided == null: return match button: "trigger_click": if _is_pressed: if _moved == false: _call_fn(_last_collided, "_on_click") _call_fn(_last_collided, "_on_press_up") _is_pressed = false _last_collided = null _moved = false "grip_click": if _is_grabbed: _call_fn(_last_collided, "_on_grab_up") _is_grabbed = false _last_collided = null _moved = false func _call_fn(collider: Variant, fn_name: String, node: Node3D = null, event = null): if collider == null: return if node == null: node = collider event = { "controller": _controller, "ray": ray, "target": collider, } if node.has_method(fn_name): var result = node.call(fn_name, event) if result != null && result is Dictionary: result.merge(event, true) event = result if result != null && result is bool && result == false: # Stop the event from bubbling up return for child in node.get_children(): if child is Function && child.has_method(fn_name): child.call(fn_name, event) var parent = node.get_parent() if parent != null && parent is Node3D: _call_fn(collider, fn_name, parent, event) else: # in case the top has been reached _call_global_fn(fn_name, event) func _call_global_fn(fn_name: String, event = null): Events.get(fn_name.substr(1)).emit(event)