@tool @icon("res://addons/godot-xr-tools/editor/icons/function.svg") class_name XRToolsFunctionPickup extends Node3D ## XR Tools Function Pickup Script ## ## This script implements picking up of objects. Most pickable ## objects are instances of the [XRToolsPickable] class. ## ## Additionally this script can work in conjunction with the ## [XRToolsMovementProvider] class support climbing. Most climbable objects are ## instances of the [XRToolsClimbable] class. ## Signal emitted when the pickup picks something up signal has_picked_up(what) ## Signal emitted when the pickup drops something signal has_dropped # Default pickup collision mask of 3:pickable and 19:handle const DEFAULT_GRAB_MASK := 0b0000_0000_0000_0100_0000_0000_0000_0100 # Default pickup collision mask of 3:pickable const DEFAULT_RANGE_MASK := 0b0000_0000_0000_0000_0000_0000_0000_0100 # Constant for worst-case grab distance const MAX_GRAB_DISTANCE2: float = 1000000.0 ## Pickup enabled property @export var enabled : bool = true ## Grip controller axis @export var pickup_axis_action : String = "grip" ## Action controller button @export var action_button_action : String = "trigger_click" ## Grab distance @export var grab_distance : float = 0.3: set = _set_grab_distance ## Grab collision mask @export_flags_3d_physics \ var grab_collision_mask : int = DEFAULT_GRAB_MASK: set = _set_grab_collision_mask ## If true, ranged-grabbing is enabled @export var ranged_enable : bool = true ## Ranged-grab distance @export var ranged_distance : float = 5.0: set = _set_ranged_distance ## Ranged-grab angle @export_range(0.0, 45.0) var ranged_angle : float = 5.0: set = _set_ranged_angle ## Ranged-grab collision mask @export_flags_3d_physics \ var ranged_collision_mask : int = DEFAULT_RANGE_MASK: set = _set_ranged_collision_mask ## Throw impulse factor @export var impulse_factor : float = 1.0 ## Throw velocity averaging @export var velocity_samples: int = 5 # Public fields var closest_object : Node3D = null var picked_up_object : Node3D = null var picked_up_ranged : bool = false var grip_pressed : bool = false # Private fields var _object_in_grab_area := Array() var _object_in_ranged_area := Array() var _velocity_averager := XRToolsVelocityAverager.new(velocity_samples) var _grab_area : Area3D var _grab_collision : CollisionShape3D var _ranged_area : Area3D var _ranged_collision : CollisionShape3D ## Controller @onready var _controller := XRHelpers.get_xr_controller(self) ## Grip threshold (from configuration) @onready var _grip_threshold : float = XRTools.get_grip_threshold() # Add support for is_xr_class on XRTools classes func is_xr_class(name : String) -> bool: return name == "XRToolsFunctionPickup" # Called when the node enters the scene tree for the first time. func _ready(): # Skip creating grab-helpers if in the editor if Engine.is_editor_hint(): return # Create the grab collision shape _grab_collision = CollisionShape3D.new() _grab_collision.set_name("GrabCollisionShape") _grab_collision.shape = SphereShape3D.new() _grab_collision.shape.radius = grab_distance # Create the grab area _grab_area = Area3D.new() _grab_area.set_name("GrabArea") _grab_area.collision_layer = 0 _grab_area.collision_mask = grab_collision_mask _grab_area.add_child(_grab_collision) _grab_area.area_entered.connect(_on_grab_entered) _grab_area.body_entered.connect(_on_grab_entered) _grab_area.area_exited.connect(_on_grab_exited) _grab_area.body_exited.connect(_on_grab_exited) add_child(_grab_area) # Create the ranged collision shape _ranged_collision = CollisionShape3D.new() _ranged_collision.set_name("RangedCollisionShape") _ranged_collision.shape = CylinderShape3D.new() _ranged_collision.transform.basis = Basis(Vector3.RIGHT, PI/2) # Create the ranged area _ranged_area = Area3D.new() _ranged_area.set_name("RangedArea") _ranged_area.collision_layer = 0 _ranged_area.collision_mask = ranged_collision_mask _ranged_area.add_child(_ranged_collision) _ranged_area.area_entered.connect(_on_ranged_entered) _ranged_area.body_entered.connect(_on_ranged_entered) _ranged_area.area_exited.connect(_on_ranged_exited) _ranged_area.body_exited.connect(_on_ranged_exited) add_child(_ranged_area) # Update the colliders _update_colliders() # Monitor Grab Button _controller.connect("button_pressed", _on_button_pressed) _controller.connect("button_released", _on_button_released) # Called on each frame to update the pickup func _process(delta): # Do not process if in the editor if Engine.is_editor_hint(): return # Skip if disabled, or the controller isn't active if !enabled or !_controller.get_is_active(): return # Handle our grip var grip_value = _controller.get_float(pickup_axis_action) if (grip_pressed and grip_value < (_grip_threshold - 0.1)): grip_pressed = false _on_grip_release() elif (!grip_pressed and grip_value > (_grip_threshold + 0.1)): grip_pressed = true _on_grip_pressed() # Calculate average velocity if is_instance_valid(picked_up_object) and picked_up_object.is_picked_up(): # Average velocity of picked up object _velocity_averager.add_transform(delta, picked_up_object.global_transform) else: # Average velocity of this pickup _velocity_averager.add_transform(delta, global_transform) _update_closest_object() ## Find an [XRToolsFunctionPickup] node. ## ## This function searches from the specified node for an [XRToolsFunctionPickup] ## assuming the node is a sibling of the pickup under an [XRController3D]. static func find_instance(node : Node) -> XRToolsFunctionPickup: return XRTools.find_xr_child( XRHelpers.get_xr_controller(node), "*", "XRToolsFunctionPickup") as XRToolsFunctionPickup ## Find the left [XRToolsFunctionPickup] node. ## ## This function searches from the specified node for the left controller ## [XRToolsFunctionPickup] assuming the node is a sibling of the [XOrigin3D]. static func find_left(node : Node) -> XRToolsFunctionPickup: return XRTools.find_xr_child( XRHelpers.get_left_controller(node), "*", "XRToolsFunctionPickup") as XRToolsFunctionPickup ## Find the right [XRToolsFunctionPickup] node. ## ## This function searches from the specified node for the right controller ## [XRToolsFunctionPickup] assuming the node is a sibling of the [XROrigin3D]. static func find_right(node : Node) -> XRToolsFunctionPickup: return XRTools.find_xr_child( XRHelpers.get_right_controller(node), "*", "XRToolsFunctionPickup") as XRToolsFunctionPickup ## Get the [XRController3D] driving this pickup. func get_controller() -> XRController3D: return _controller # Called when the grab distance has been modified func _set_grab_distance(new_value: float) -> void: grab_distance = new_value if is_inside_tree(): _update_colliders() # Called when the grab collision mask has been modified func _set_grab_collision_mask(new_value: int) -> void: grab_collision_mask = new_value if is_inside_tree() and _grab_collision: _grab_collision.collision_mask = new_value # Called when the ranged-grab distance has been modified func _set_ranged_distance(new_value: float) -> void: ranged_distance = new_value if is_inside_tree(): _update_colliders() # Called when the ranged-grab angle has been modified func _set_ranged_angle(new_value: float) -> void: ranged_angle = new_value if is_inside_tree(): _update_colliders() # Called when the ranged-grab collision mask has been modified func _set_ranged_collision_mask(new_value: int) -> void: ranged_collision_mask = new_value if is_inside_tree() and _ranged_collision: _ranged_collision.collision_mask = new_value # Update the colliders geometry func _update_colliders() -> void: # Update the grab sphere if _grab_collision: _grab_collision.shape.radius = grab_distance # Update the ranged-grab cylinder if _ranged_collision: _ranged_collision.shape.radius = tan(deg_to_rad(ranged_angle)) * ranged_distance _ranged_collision.shape.height = ranged_distance _ranged_collision.transform.origin.z = -ranged_distance * 0.5 # Called when an object enters the grab sphere func _on_grab_entered(target: Node3D) -> void: # reject objects which don't support picking up if not target.has_method('pick_up'): return # ignore objects already known if _object_in_grab_area.find(target) >= 0: return # Add to the list of objects in grab area _object_in_grab_area.push_back(target) # Called when an object enters the ranged-grab cylinder func _on_ranged_entered(target: Node3D) -> void: # reject objects which don't support picking up rangedly if not 'can_ranged_grab' in target or not target.can_ranged_grab: return # ignore objects already known if _object_in_ranged_area.find(target) >= 0: return # Add to the list of objects in grab area _object_in_ranged_area.push_back(target) # Called when an object exits the grab sphere func _on_grab_exited(target: Node3D) -> void: _object_in_grab_area.erase(target) # Called when an object exits the ranged-grab cylinder func _on_ranged_exited(target: Node3D) -> void: _object_in_ranged_area.erase(target) # Update the closest object field with the best choice of grab func _update_closest_object() -> void: # Find the closest object we can pickup var new_closest_obj: Node3D = null if not picked_up_object: # Find the closest in grab area new_closest_obj = _get_closest_grab() if not new_closest_obj and ranged_enable: # Find closest in ranged area new_closest_obj = _get_closest_ranged() # Skip if no change if closest_object == new_closest_obj: return # remove highlight on old object if is_instance_valid(closest_object): closest_object.request_highlight(self, false) # add highlight to new object closest_object = new_closest_obj if is_instance_valid(closest_object): closest_object.request_highlight(self, true) # Find the pickable object closest to our hand's grab location func _get_closest_grab() -> Node3D: var new_closest_obj: Node3D = null var new_closest_distance := MAX_GRAB_DISTANCE2 for o in _object_in_grab_area: # skip objects that can not be picked up if not o.can_pick_up(self): continue # Save if this object is closer than the current best var distance_squared := global_transform.origin.distance_squared_to( o.global_transform.origin) if distance_squared < new_closest_distance: new_closest_obj = o new_closest_distance = distance_squared # Return best object return new_closest_obj # Find the rangedly-pickable object closest to our hand's pointing direction func _get_closest_ranged() -> Node3D: var new_closest_obj: Node3D = null var new_closest_angle_dp := cos(deg_to_rad(ranged_angle)) var hand_forwards := -global_transform.basis.z for o in _object_in_ranged_area: # skip objects that can not be picked up if not o.can_pick_up(self): continue # Save if this object is closer than the current best var object_direction: Vector3 = o.global_transform.origin - global_transform.origin object_direction = object_direction.normalized() var angle_dp := hand_forwards.dot(object_direction) if angle_dp > new_closest_angle_dp: new_closest_obj = o new_closest_angle_dp = angle_dp # Return best object return new_closest_obj ## Drop the currently held object func drop_object() -> void: if not is_instance_valid(picked_up_object): return # let go of this object picked_up_object.let_go( _velocity_averager.linear_velocity() * impulse_factor, _velocity_averager.angular_velocity()) picked_up_object = null emit_signal("has_dropped") func _pick_up_object(target: Node3D) -> void: # check if already holding an object if is_instance_valid(picked_up_object): # skip if holding the target object if picked_up_object == target: return # holding something else? drop it drop_object() # skip if target null or freed if not is_instance_valid(target): return # Handle snap-zone var snap := target as XRToolsSnapZone if snap: target = snap.picked_up_object snap.drop_object() # Pick up our target. Note, target may do instant drop_and_free picked_up_ranged = not _object_in_grab_area.has(target) picked_up_object = target target.pick_up(self, _controller) # If object picked up then emit signal if is_instance_valid(picked_up_object): picked_up_object.request_highlight(self, false) emit_signal("has_picked_up", picked_up_object) func _on_button_pressed(p_button) -> void: if p_button == action_button_action: if is_instance_valid(picked_up_object) and picked_up_object.has_method("action"): picked_up_object.action() func _on_button_released(_p_button) -> void: pass func _on_grip_pressed() -> void: if is_instance_valid(picked_up_object) and !picked_up_object.press_to_hold: drop_object() elif is_instance_valid(closest_object): _pick_up_object(closest_object) func _on_grip_release() -> void: if is_instance_valid(picked_up_object) and picked_up_object.press_to_hold: drop_object()