475 lines
13 KiB
GDScript3
475 lines
13 KiB
GDScript3
|
@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.
|
||
|
|
||
|
|
||
|
# Signal emitted when the user picks up this object
|
||
|
signal picked_up(pickable)
|
||
|
|
||
|
# Signal emitted when the user drops this object
|
||
|
signal dropped(pickable)
|
||
|
|
||
|
# 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 hold object
|
||
|
enum HoldMethod {
|
||
|
REMOTE_TRANSFORM, ## Object is held via a remote transform
|
||
|
REPARENT, ## Object is held by reparenting
|
||
|
}
|
||
|
|
||
|
## 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
|
||
|
}
|
||
|
|
||
|
## Current pickable object state
|
||
|
enum PickableState {
|
||
|
IDLE, ## Object not held
|
||
|
GRABBING_RANGED, ## Object being grabbed at range
|
||
|
HELD, ## Object held
|
||
|
}
|
||
|
|
||
|
enum ReleaseMode {
|
||
|
ORIGINAL = -1, ## Preserve original mode when picked up
|
||
|
UNFROZEN = 0, ## Release and unfreeze
|
||
|
FROZEN = 1, ## Release and freeze
|
||
|
}
|
||
|
|
||
|
|
||
|
# Default layer for held objects is 17:held-object
|
||
|
const DEFAULT_LAYER := 0b0000_0000_0000_0001_0000_0000_0000_0000
|
||
|
|
||
|
## Priority for grip poses
|
||
|
const GRIP_POSE_PRIORITY = 100
|
||
|
|
||
|
|
||
|
## 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
|
||
|
@export_flags_3d_physics var picked_up_layer = DEFAULT_LAYER
|
||
|
|
||
|
## Method used to hold an object
|
||
|
@export var hold_method : HoldMethod = HoldMethod.REMOTE_TRANSFORM
|
||
|
|
||
|
## 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
|
||
|
|
||
|
## 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
|
||
|
|
||
|
## Entity holding this item
|
||
|
var picked_up_by: Node3D = null
|
||
|
|
||
|
## Controller holding this item (may be null if held by snap-zone)
|
||
|
var by_controller : XRController3D = null
|
||
|
|
||
|
## What node "holds" the object
|
||
|
var hold_node : Node3D = null
|
||
|
|
||
|
## Hand holding this item (may be null if held by snap-zone)
|
||
|
var by_hand : XRToolsHand = null
|
||
|
|
||
|
## Collision hand holding this item (may be null)
|
||
|
var by_collision_hand : XRToolsCollisionHand = null
|
||
|
|
||
|
# Count of 'is_closest' grabbers
|
||
|
var _closest_count: int = 0
|
||
|
|
||
|
# Current state
|
||
|
var _state = PickableState.IDLE
|
||
|
|
||
|
# Remote transform
|
||
|
var _remote_transform: RemoteTransform3D = null
|
||
|
|
||
|
# Move-to node for performing remote grab
|
||
|
var _move_to: XRToolsMoveTo = null
|
||
|
|
||
|
# Array of grab points
|
||
|
var _grab_points : Array = []
|
||
|
|
||
|
# Currently active grab-point
|
||
|
var _active_grab_point : XRToolsGrabPoint
|
||
|
|
||
|
# 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_parent = get_parent()
|
||
|
@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)
|
||
|
|
||
|
|
||
|
# Test if this object can be picked up
|
||
|
func can_pick_up(_by: Node3D) -> bool:
|
||
|
return enabled and _state == PickableState.IDLE
|
||
|
|
||
|
|
||
|
# Test if this object is picked up
|
||
|
func is_picked_up():
|
||
|
return _state == PickableState.HELD
|
||
|
|
||
|
|
||
|
# 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)
|
||
|
|
||
|
|
||
|
func drop_and_free():
|
||
|
if picked_up_by:
|
||
|
picked_up_by.drop_object()
|
||
|
|
||
|
queue_free()
|
||
|
|
||
|
|
||
|
# Called when this object is picked up
|
||
|
func pick_up(by: Node3D, with_controller: XRController3D) -> void:
|
||
|
# Skip if disabled or already picked up
|
||
|
if not enabled or _state != PickableState.IDLE:
|
||
|
return
|
||
|
|
||
|
if picked_up_by:
|
||
|
let_go(Vector3.ZERO, Vector3.ZERO)
|
||
|
|
||
|
# remember who picked us up
|
||
|
picked_up_by = by
|
||
|
by_controller = with_controller
|
||
|
hold_node = with_controller if with_controller else by
|
||
|
by_hand = XRToolsHand.find_instance(by_controller)
|
||
|
by_collision_hand = XRToolsCollisionHand.find_instance(by_controller)
|
||
|
_active_grab_point = _get_grab_point(by)
|
||
|
|
||
|
# If we have been picked up by a hand then apply the hand-pose-override
|
||
|
# from the grab-point.
|
||
|
if by_hand and _active_grab_point:
|
||
|
var grab_point_hand := _active_grab_point as XRToolsGrabPointHand
|
||
|
if grab_point_hand and grab_point_hand.hand_pose:
|
||
|
by_hand.add_pose_override(self, GRIP_POSE_PRIORITY, grab_point_hand.hand_pose)
|
||
|
|
||
|
# If we have been picked up by a collision hand then add collision
|
||
|
# exceptions to prevent the hand and pickable colliding.
|
||
|
if by_collision_hand:
|
||
|
add_collision_exception_with(by_collision_hand)
|
||
|
by_collision_hand.add_collision_exception_with(self)
|
||
|
|
||
|
# 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
|
||
|
|
||
|
if by.picked_up_ranged:
|
||
|
if ranged_grab_method == RangedMethod.LERP:
|
||
|
_start_ranged_grab()
|
||
|
else:
|
||
|
_do_snap_grab()
|
||
|
elif _active_grab_point:
|
||
|
_do_snap_grab()
|
||
|
else:
|
||
|
_do_precise_grab()
|
||
|
|
||
|
|
||
|
# Called when this object is dropped
|
||
|
func let_go(p_linear_velocity: Vector3, p_angular_velocity: Vector3) -> void:
|
||
|
# Skip if idle
|
||
|
if _state == PickableState.IDLE:
|
||
|
return
|
||
|
|
||
|
# If held then detach from holder
|
||
|
if _state == PickableState.HELD:
|
||
|
match hold_method:
|
||
|
HoldMethod.REPARENT:
|
||
|
var original_transform = global_transform
|
||
|
picked_up_by.remove_child(self)
|
||
|
original_parent.add_child(self)
|
||
|
global_transform = original_transform
|
||
|
|
||
|
HoldMethod.REMOTE_TRANSFORM:
|
||
|
_remote_transform.remote_path = NodePath()
|
||
|
_remote_transform.queue_free()
|
||
|
_remote_transform = null
|
||
|
|
||
|
# 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
|
||
|
|
||
|
# If we are held by a hand then remove any hand-pose-override we may have
|
||
|
# given it.
|
||
|
if by_hand:
|
||
|
by_hand.remove_pose_override(self)
|
||
|
|
||
|
# If we are held by a cillision hand then remove any collision exceptions
|
||
|
# we may have added.
|
||
|
if by_collision_hand:
|
||
|
remove_collision_exception_with(by_collision_hand)
|
||
|
by_collision_hand.remove_collision_exception_with(self)
|
||
|
|
||
|
# we are no longer picked up
|
||
|
_state = PickableState.IDLE
|
||
|
picked_up_by = null
|
||
|
by_controller = null
|
||
|
by_hand = null
|
||
|
by_collision_hand = null
|
||
|
hold_node = null
|
||
|
|
||
|
# Stop any XRToolsMoveTo being used for remote grabbing
|
||
|
if _move_to:
|
||
|
_move_to.stop()
|
||
|
_move_to.queue_free()
|
||
|
_move_to = null
|
||
|
|
||
|
# let interested parties know
|
||
|
emit_signal("dropped", self)
|
||
|
|
||
|
|
||
|
## Get the controller currently holding this object
|
||
|
func get_picked_up_by_controller() -> XRController3D:
|
||
|
return by_controller
|
||
|
|
||
|
|
||
|
## Get the hand currently holding this object
|
||
|
func get_picked_up_by_hand() -> XRToolsHand:
|
||
|
return by_hand
|
||
|
|
||
|
|
||
|
## Get the active grab-point this object is held by
|
||
|
func get_active_grab_point() -> XRToolsGrabPoint:
|
||
|
return _active_grab_point
|
||
|
|
||
|
|
||
|
## Switch the active grab-point for this object
|
||
|
func switch_active_grab_point(grab_point : XRToolsGrabPoint):
|
||
|
# Verify switching from one grab point to another
|
||
|
if not _active_grab_point or not grab_point or _state != PickableState.HELD:
|
||
|
return
|
||
|
|
||
|
# Set the new active grab-point
|
||
|
_active_grab_point = grab_point
|
||
|
|
||
|
# Update the hold transform
|
||
|
match hold_method:
|
||
|
HoldMethod.REMOTE_TRANSFORM:
|
||
|
# Update the remote transform
|
||
|
_remote_transform.transform = _active_grab_point.transform.inverse()
|
||
|
|
||
|
HoldMethod.REPARENT:
|
||
|
# Update our transform
|
||
|
transform = _active_grab_point.global_transform.inverse() * global_transform
|
||
|
|
||
|
# Update the pose
|
||
|
if by_hand and _active_grab_point:
|
||
|
var grab_point_hand := _active_grab_point as XRToolsGrabPointHand
|
||
|
if grab_point_hand and grab_point_hand.hand_pose:
|
||
|
by_hand.add_pose_override(self, GRIP_POSE_PRIORITY, grab_point_hand.hand_pose)
|
||
|
else:
|
||
|
by_hand.remove_pose_override(self)
|
||
|
|
||
|
|
||
|
func _start_ranged_grab() -> void:
|
||
|
# Set state to grabbing at range and enable processing
|
||
|
_state = PickableState.GRABBING_RANGED
|
||
|
|
||
|
# Calculate the transform offset
|
||
|
var offset : Transform3D
|
||
|
if _active_grab_point:
|
||
|
offset = _active_grab_point.transform.inverse()
|
||
|
else:
|
||
|
offset = Transform3D.IDENTITY
|
||
|
|
||
|
# Create a XRToolsMoveTo to perform the remote-grab. The remote grab will move
|
||
|
# us to the pickup object at the ranged-grab speed, and also takes into account
|
||
|
# the center-pickup position
|
||
|
_move_to = XRToolsMoveTo.new()
|
||
|
_move_to.start(self, hold_node, offset, ranged_grab_speed)
|
||
|
_move_to.move_complete.connect(_ranged_grab_complete)
|
||
|
self.add_child(_move_to)
|
||
|
|
||
|
|
||
|
func _ranged_grab_complete() -> void:
|
||
|
# Discard the XRToolsMoveTo performing the remote-grab
|
||
|
_move_to.queue_free()
|
||
|
_move_to = null
|
||
|
|
||
|
# Perform the snap grab
|
||
|
_do_snap_grab()
|
||
|
|
||
|
|
||
|
func _do_snap_grab() -> void:
|
||
|
# Set state to held
|
||
|
_state = PickableState.HELD
|
||
|
|
||
|
# Perform the hold
|
||
|
match hold_method:
|
||
|
HoldMethod.REMOTE_TRANSFORM:
|
||
|
# Calculate the snap transform for remote-transforming
|
||
|
var snap_transform: Transform3D
|
||
|
if _active_grab_point:
|
||
|
snap_transform = _active_grab_point.transform.inverse()
|
||
|
else:
|
||
|
snap_transform = Transform3D.IDENTITY
|
||
|
|
||
|
# Construct the remote transform
|
||
|
_remote_transform = RemoteTransform3D.new()
|
||
|
_remote_transform.set_name("PickupRemoteTransform")
|
||
|
hold_node.add_child(_remote_transform)
|
||
|
_remote_transform.transform = snap_transform
|
||
|
_remote_transform.remote_path = _remote_transform.get_path_to(self)
|
||
|
|
||
|
HoldMethod.REPARENT:
|
||
|
# Calculate the snap transform for reparenting
|
||
|
var snap_transform: Transform3D
|
||
|
if _active_grab_point:
|
||
|
snap_transform = _active_grab_point.global_transform.inverse() * global_transform
|
||
|
else:
|
||
|
snap_transform = Transform3D.IDENTITY
|
||
|
|
||
|
# Reparent to the holder with snap transform
|
||
|
original_parent.remove_child(self)
|
||
|
hold_node.add_child(self)
|
||
|
transform = snap_transform
|
||
|
|
||
|
# Emit the picked up signal
|
||
|
emit_signal("picked_up", self)
|
||
|
|
||
|
|
||
|
func _do_precise_grab() -> void:
|
||
|
# Set state to held
|
||
|
_state = PickableState.HELD
|
||
|
|
||
|
# Reparent to the holder
|
||
|
match hold_method:
|
||
|
HoldMethod.REMOTE_TRANSFORM:
|
||
|
# Calculate the precise transform for remote-transforming
|
||
|
var precise_transform = hold_node.global_transform.inverse() * global_transform
|
||
|
|
||
|
# Construct the remote transform
|
||
|
_remote_transform = RemoteTransform3D.new()
|
||
|
_remote_transform.set_name("PickupRemoteTransform")
|
||
|
hold_node.add_child(_remote_transform)
|
||
|
_remote_transform.transform = hold_node.global_transform.inverse() * global_transform
|
||
|
_remote_transform.remote_path = _remote_transform.get_path_to(self)
|
||
|
|
||
|
HoldMethod.REPARENT:
|
||
|
# Calculate the precise transform for reparenting
|
||
|
var precise_transform = global_transform
|
||
|
|
||
|
# Reparent to the holder with precise transform
|
||
|
original_parent.remove_child(self)
|
||
|
hold_node.add_child(self)
|
||
|
global_transform = precise_transform
|
||
|
|
||
|
# Emit the picked up signal
|
||
|
emit_signal("picked_up", self)
|
||
|
|
||
|
|
||
|
## Find the first grab-point for the grabber
|
||
|
func _get_grab_point(_grabber : Node) -> XRToolsGrabPoint:
|
||
|
# Iterate over all grab points
|
||
|
for g in _grab_points:
|
||
|
var grab_point : XRToolsGrabPoint = g
|
||
|
if grab_point.can_grab(_grabber):
|
||
|
return grab_point
|
||
|
|
||
|
# No suitable grab-point found
|
||
|
return null
|
||
|
|
||
|
|
||
|
func _set_ranged_grab_method(new_value: int) -> void:
|
||
|
ranged_grab_method = new_value
|
||
|
can_ranged_grab = new_value != RangedMethod.NONE
|