immersive-home/addons/godot-xr-tools/objects/pickable.gd

401 lines
10 KiB
GDScript3
Raw Normal View History

2023-10-16 20:10:20 +03:00
@tool
class_name XRToolsPickable
extends RigidBody3D
## XR Tools Pickable Object
##
## This script allows a [RigidBody3D] to be picked up by an
## [XRToolsFunctionPickup] attached to a players controller.
##
## Additionally pickable objects may support being snapped into
## [XRToolsSnapZone] areas.
##
## Grab-points can be defined by adding different types of [XRToolsGrabPoint]
## child nodes controlling hand and snap-zone grab locations.
2023-12-14 01:03:03 +02:00
# Signal emitted when this object is picked up (held by a player or snap-zone)
2023-10-16 20:10:20 +03:00
signal picked_up(pickable)
2023-12-14 01:03:03 +02:00
# Signal emitted when this object is dropped
2023-10-16 20:10:20 +03:00
signal dropped(pickable)
2023-12-14 01:03:03 +02:00
# Signal emitted when this object is grabbed (primary or secondary)
signal grabbed(pickable, by)
# Signal emitted when this object is released (primary or secondary)
signal released(pickable, by)
2023-10-16 20:10:20 +03:00
# Signal emitted when the user presses the action button while holding this object
signal action_pressed(pickable)
# Signal emitted when the highlight state changes
signal highlight_updated(pickable, enable)
## Method used to grab object at range
enum RangedMethod {
NONE, ## Ranged grab is not supported
SNAP, ## Object snaps to holder
LERP, ## Object lerps to holder
}
enum ReleaseMode {
ORIGINAL = -1, ## Preserve original mode when picked up
UNFROZEN = 0, ## Release and unfreeze
FROZEN = 1, ## Release and freeze
}
2023-12-14 01:03:03 +02:00
enum SecondHandGrab {
IGNORE, ## Ignore second grab
SWAP, ## Swap to second hand
SECOND, ## Second hand grab
}
2023-10-16 20:10:20 +03:00
# Default layer for held objects is 17:held-object
const DEFAULT_LAYER := 0b0000_0000_0000_0001_0000_0000_0000_0000
## If true, the pickable supports being picked up
@export var enabled : bool = true
## If true, the grip control must be held to keep the object picked up
@export var press_to_hold : bool = true
## Layer for this object while picked up
2023-12-14 01:03:03 +02:00
@export_flags_3d_physics var picked_up_layer : int = DEFAULT_LAYER
2023-10-16 20:10:20 +03:00
## Release mode to use when releasing the object
@export var release_mode : ReleaseMode = ReleaseMode.ORIGINAL
## Method used to perform a ranged grab
@export var ranged_grab_method : RangedMethod = RangedMethod.SNAP: set = _set_ranged_grab_method
2023-12-14 01:03:03 +02:00
## Second hand grab mode
@export var second_hand_grab : SecondHandGrab = SecondHandGrab.IGNORE
2023-10-16 20:10:20 +03:00
## Speed for ranged grab
@export var ranged_grab_speed : float = 20.0
## Refuse pick-by when in the specified group
@export var picked_by_exclude : String = ""
## Require pick-by to be in the specified group
@export var picked_by_require : String = ""
## If true, the object can be picked up at range
var can_ranged_grab: bool = true
## Frozen state to restore to when dropped
var restore_freeze : bool = false
# Count of 'is_closest' grabbers
var _closest_count: int = 0
2023-12-14 01:03:03 +02:00
# Grab Driver to control position while grabbed
var _grab_driver: XRToolsGrabDriver = null
2023-10-16 20:10:20 +03:00
# Array of grab points
2023-12-14 01:03:03 +02:00
var _grab_points : Array[XRToolsGrabPoint] = []
2023-10-16 20:10:20 +03:00
# Dictionary of nodes requesting highlight
var _highlight_requests : Dictionary = {}
# Is this node highlighted
var _highlighted : bool = false
# Remember some state so we can return to it when the user drops the object
@onready var original_collision_mask : int = collision_mask
@onready var original_collision_layer : int = collision_layer
# Add support for is_xr_class on XRTools classes
func is_xr_class(name : String) -> bool:
return name == "XRToolsPickable"
# Called when the node enters the scene tree for the first time.
func _ready():
# Get all grab points
for child in get_children():
var grab_point := child as XRToolsGrabPoint
if grab_point:
_grab_points.push_back(grab_point)
2023-12-14 01:03:03 +02:00
# Called when the node exits the tree
func _exit_tree():
# Skip if not picked up
if not is_instance_valid(_grab_driver):
return
# Release primary grab
if _grab_driver.primary:
_grab_driver.primary.release()
# Release secondary grab
if _grab_driver.secondary:
_grab_driver.secondary.release()
2023-10-16 20:10:20 +03:00
# Test if this object can be picked up
2023-12-14 01:03:03 +02:00
func can_pick_up(by: Node3D) -> bool:
# Refuse if not enabled
if not enabled:
return false
# Allow if not held by anything
if not is_picked_up():
return true
# Fail if second hand grabbing isn't allowed
if second_hand_grab == SecondHandGrab.IGNORE:
return false
# Fail if either pickup isn't by a hand
if not _grab_driver.primary.pickup or not by is XRToolsFunctionPickup:
return false
# Allow second hand grab
return true
2023-10-16 20:10:20 +03:00
# Test if this object is picked up
2023-12-14 01:03:03 +02:00
func is_picked_up() -> bool:
return _grab_driver and _grab_driver.primary
2023-10-16 20:10:20 +03:00
# action is called when user presses the action button while holding this object
func action():
# let interested parties know
emit_signal("action_pressed", self)
## This method requests highlighting of the [XRToolsPickable].
## If [param from] is null then all highlighting requests are cleared,
## otherwise the highlight request is associated with the specified node.
func request_highlight(from : Node, on : bool = true) -> void:
# Save if we are highlighted
var old_highlighted := _highlighted
# Update the highlight requests dictionary
if not from:
_highlight_requests.clear()
elif on:
_highlight_requests[from] = from
else:
_highlight_requests.erase(from)
# Update the highlighted state
_highlighted = _highlight_requests.size() > 0
# Report any changes
if _highlighted != old_highlighted:
emit_signal("highlight_updated", self, _highlighted)
2023-12-14 01:03:03 +02:00
func drop():
# Skip if not picked up
if not is_picked_up():
return
# Request secondary grabber to drop
if _grab_driver.secondary:
_grab_driver.secondary.by.drop_object()
# Request primary grabber to drop
_grab_driver.primary.by.drop_object()
2023-10-16 20:10:20 +03:00
2023-12-14 01:03:03 +02:00
func drop_and_free():
drop()
2023-10-16 20:10:20 +03:00
queue_free()
# Called when this object is picked up
2023-12-14 01:03:03 +02:00
func pick_up(by: Node3D) -> void:
# Skip if not enabled
if not enabled:
2023-10-16 20:10:20 +03:00
return
2023-12-14 01:03:03 +02:00
# Find the grabber information
var grabber := Grabber.new(by)
# Test if we're already picked up:
if is_picked_up():
# Ignore if we don't support second-hand grab
if second_hand_grab == SecondHandGrab.IGNORE:
print_verbose("%s> second-hand grab not enabled" % name)
return
# Ignore if either pickup isn't by a hand
if not _grab_driver.primary.pickup or not grabber.pickup:
return
# Construct the second grab
if second_hand_grab != SecondHandGrab.SWAP:
# Grab the object
var by_grab_point := _get_grab_point(by, _grab_driver.primary.point)
var grab := Grab.new(grabber, self, by_grab_point, true)
_grab_driver.add_grab(grab)
# Report the secondary grab
grabbed.emit(self, by)
return
# Swapping hands, let go with the primary grab
print_verbose("%s> letting go to swap hands" % name)
let_go(_grab_driver.primary.by, Vector3.ZERO, Vector3.ZERO)
2023-10-16 20:10:20 +03:00
# Remember the mode before pickup
match release_mode:
ReleaseMode.UNFROZEN:
restore_freeze = false
ReleaseMode.FROZEN:
restore_freeze = true
_:
restore_freeze = freeze
# turn off physics on our pickable object
freeze = true
collision_layer = picked_up_layer
collision_mask = 0
2023-12-14 01:03:03 +02:00
# Find a suitable primary hand grab
var by_grab_point := _get_grab_point(by, null)
# Construct the grab driver
2023-10-16 20:10:20 +03:00
if by.picked_up_ranged:
if ranged_grab_method == RangedMethod.LERP:
2023-12-14 01:03:03 +02:00
var grab := Grab.new(grabber, self, by_grab_point, false)
_grab_driver = XRToolsGrabDriver.create_lerp(self, grab, ranged_grab_speed)
2023-10-16 20:10:20 +03:00
else:
2023-12-14 01:03:03 +02:00
var grab := Grab.new(grabber, self, by_grab_point, false)
_grab_driver = XRToolsGrabDriver.create_snap(self, grab)
2023-10-16 20:10:20 +03:00
else:
2023-12-14 01:03:03 +02:00
var grab := Grab.new(grabber, self, by_grab_point, true)
_grab_driver = XRToolsGrabDriver.create_snap(self, grab)
# Report picked up and grabbed
picked_up.emit(self)
grabbed.emit(self, by)
2023-10-16 20:10:20 +03:00
# Called when this object is dropped
2023-12-14 01:03:03 +02:00
func let_go(by: Node3D, p_linear_velocity: Vector3, p_angular_velocity: Vector3) -> void:
# Skip if not picked up
if not is_picked_up():
return
# Get the grab information
var grab := _grab_driver.get_grab(by)
if not grab:
2023-10-16 20:10:20 +03:00
return
2023-12-14 01:03:03 +02:00
# Remove the grab from the driver and release the grab
_grab_driver.remove_grab(grab)
grab.release()
# Test if still grabbing
if _grab_driver.primary:
# Test if we need to swap grab-points
if is_instance_valid(_grab_driver.primary.hand_point):
# Verify the current primary grab point is a valid primary grab point
if _grab_driver.primary.hand_point.mode != XRToolsGrabPointHand.Mode.SECONDARY:
return
2023-10-16 20:10:20 +03:00
2023-12-14 01:03:03 +02:00
# Find a more suitable grab-point
var new_grab_point := _get_grab_point(_grab_driver.primary.by, null)
print_verbose("%s> held only by secondary, swapping grab points" % name)
switch_active_grab_point(new_grab_point)
# Grab is still good
return
# Drop the grab-driver
print_verbose("%s> dropping" % name)
_grab_driver.discard()
_grab_driver = null
2023-10-16 20:10:20 +03:00
# Restore RigidBody mode
freeze = restore_freeze
collision_mask = original_collision_mask
collision_layer = original_collision_layer
# Set velocity
linear_velocity = p_linear_velocity
angular_velocity = p_angular_velocity
# let interested parties know
2023-12-14 01:03:03 +02:00
dropped.emit(self)
## Get the node currently holding this object
func get_picked_up_by() -> Node3D:
# Skip if not picked up
if not is_picked_up():
return null
# Get the primary pickup
return _grab_driver.primary.by
2023-10-16 20:10:20 +03:00
## Get the controller currently holding this object
func get_picked_up_by_controller() -> XRController3D:
2023-12-14 01:03:03 +02:00
# Skip if not picked up
if not is_picked_up():
return null
2023-10-16 20:10:20 +03:00
2023-12-14 01:03:03 +02:00
# Get the primary pickup controller
return _grab_driver.primary.controller
2023-10-16 20:10:20 +03:00
## Get the active grab-point this object is held by
func get_active_grab_point() -> XRToolsGrabPoint:
2023-12-14 01:03:03 +02:00
# Skip if not picked up
if not is_picked_up():
return null
return _grab_driver.primary.point
2023-10-16 20:10:20 +03:00
## Switch the active grab-point for this object
func switch_active_grab_point(grab_point : XRToolsGrabPoint):
2023-12-14 01:03:03 +02:00
# Skip if not picked up
if not is_picked_up():
return null
# Apply the grab point
_grab_driver.primary.set_grab_point(grab_point)
## Find the most suitable grab-point for the grabber
func _get_grab_point(grabber : Node3D, current : XRToolsGrabPoint) -> XRToolsGrabPoint:
# Find the best grab-point
var fitness := 0.0
var point : XRToolsGrabPoint = null
for p in _grab_points:
var f := p.can_grab(grabber, current)
if f > fitness:
fitness = f
point = p
# Resolve redirection
while point is XRToolsGrabPointRedirect:
point = point.target
# Return the best grab point
print_verbose("%s> picked grab-point %s" % [name, point])
return point
2023-10-16 20:10:20 +03:00
func _set_ranged_grab_method(new_value: int) -> void:
ranged_grab_method = new_value
can_ranged_grab = new_value != RangedMethod.NONE