493 lines
13 KiB
GDScript
493 lines
13 KiB
GDScript
@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 this object is picked up (held by a player or snap-zone)
|
|
signal picked_up(pickable)
|
|
|
|
# Signal emitted when this object is dropped
|
|
signal dropped(pickable)
|
|
|
|
# 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)
|
|
|
|
# Signal emitted when the user presses the action button while holding this object
|
|
signal action_pressed(pickable)
|
|
|
|
# Signal emitted when the user releases the action button while holding this object
|
|
signal action_released(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
|
|
}
|
|
|
|
enum SecondHandGrab {
|
|
IGNORE, ## Ignore second grab
|
|
SWAP, ## Swap to second hand
|
|
SECOND, ## Second hand grab
|
|
}
|
|
|
|
|
|
# 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
|
|
@export_flags_3d_physics var picked_up_layer : int = DEFAULT_LAYER
|
|
|
|
## 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
|
|
|
|
## Second hand grab mode
|
|
@export var second_hand_grab : SecondHandGrab = SecondHandGrab.IGNORE
|
|
|
|
## 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
|
|
|
|
# Grab Driver to control position while grabbed
|
|
var _grab_driver: XRToolsGrabDriver = null
|
|
|
|
# Array of grab points
|
|
var _grab_points : Array[XRToolsGrabPoint] = []
|
|
|
|
# Dictionary of nodes requesting highlight
|
|
var _highlight_requests : Dictionary = {}
|
|
|
|
# Is this node highlighted
|
|
var _highlighted : bool = false
|
|
|
|
var _controller : XRController3D
|
|
|
|
@onready var _gpu_particles: GPUParticles3D = get_node("GPUParticles3D")
|
|
|
|
|
|
# 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():
|
|
if _gpu_particles != null:
|
|
_gpu_particles.amount_ratio = 0.0
|
|
# Get all grab points
|
|
for child in get_children():
|
|
var grab_point := child as XRToolsGrabPoint
|
|
if grab_point:
|
|
_grab_points.push_back(grab_point)
|
|
|
|
@onready var rumble_timer = 0.0
|
|
@onready var shoot_timer = 0.0
|
|
|
|
func _process(delta):
|
|
if _controller:
|
|
var trigger_level = _controller.get_float("trigger")
|
|
if _gpu_particles != null and is_picked_up():
|
|
_gpu_particles.amount_ratio = trigger_level
|
|
rumble_timer -= delta
|
|
shoot_timer -= delta
|
|
if shoot_timer <= 0.0 && trigger_level >= 0.1:
|
|
shoot_projectile()
|
|
shoot_timer = 0.1;
|
|
|
|
if rumble_timer <= 0.0:
|
|
_controller.trigger_haptic_pulse(&"haptic", 30, trigger_level, 0.11, 0.0)
|
|
rumble_timer = 0.1
|
|
else:
|
|
shoot_timer = 0.0
|
|
rumble_timer = 0.0
|
|
|
|
func shoot_projectile() -> void:
|
|
var projectile = Area3D.new()
|
|
|
|
# Enable monitoring for overlaps
|
|
projectile.monitoring = true
|
|
projectile.monitorable = true
|
|
|
|
get_tree().current_scene.add_child(projectile)
|
|
|
|
# Position it a bit forward/up from current position
|
|
projectile.global_transform = global_transform
|
|
projectile.global_transform.origin += Vector3(0.0, 0.2, 0.0)
|
|
|
|
# Add a collision shape (SphereShape3D)
|
|
var collider = CollisionShape3D.new()
|
|
var shape = SphereShape3D.new()
|
|
shape.radius = 0.5
|
|
collider.shape = shape
|
|
projectile.add_child(collider)
|
|
|
|
# Connect signal with new syntax and bind projectile instance
|
|
projectile.body_entered.connect(Callable(self, "_on_projectile_body_entered").bind(projectile))
|
|
|
|
projectile.set_meta("velocity", -global_transform.basis.z.normalized() * 1.0)
|
|
|
|
var script := GDScript.new()
|
|
script.source_code = '''
|
|
extends Area3D
|
|
|
|
func _start_lifetime_timer():
|
|
await get_tree().create_timer(0.5).timeout
|
|
if is_inside_tree():
|
|
queue_free()
|
|
'''
|
|
|
|
script.reload()
|
|
projectile.set_script(script)
|
|
|
|
# Start lifetime timer (using deferred call to ensure it's on the scene tree)
|
|
projectile.call_deferred("_start_lifetime_timer")
|
|
|
|
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
# Move all Area3D projectiles manually
|
|
var current_scene = get_tree().current_scene
|
|
if current_scene:
|
|
for projectile in get_tree().current_scene.get_children():
|
|
if projectile is Area3D and projectile.has_meta("velocity"):
|
|
projectile.global_translate(projectile.get_meta("velocity") * delta)
|
|
|
|
|
|
func _on_projectile_body_entered(body: Node, projectile: Area3D) -> void:
|
|
if projectile and projectile.is_inside_tree():
|
|
projectile.queue_free()
|
|
# 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()
|
|
|
|
|
|
# Test if this object can be picked up
|
|
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
|
|
|
|
|
|
# Test if this object is picked up
|
|
func is_picked_up() -> bool:
|
|
return _grab_driver and _grab_driver.primary
|
|
|
|
|
|
# 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():
|
|
# 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()
|
|
|
|
|
|
func drop_and_free():
|
|
drop()
|
|
queue_free()
|
|
|
|
|
|
# Called when this object is picked up
|
|
func pick_up(by: Node3D) -> void:
|
|
# Skip if not enabled
|
|
if not enabled:
|
|
return
|
|
|
|
# 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)
|
|
|
|
# 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
|
|
|
|
# Find a suitable primary hand grab
|
|
var by_grab_point := _get_grab_point(by, null)
|
|
|
|
# Construct the grab driver
|
|
if by.picked_up_ranged:
|
|
if ranged_grab_method == RangedMethod.LERP:
|
|
var grab := Grab.new(grabber, self, by_grab_point, false)
|
|
_grab_driver = XRToolsGrabDriver.create_lerp(self, grab, ranged_grab_speed)
|
|
else:
|
|
var grab := Grab.new(grabber, self, by_grab_point, false)
|
|
_grab_driver = XRToolsGrabDriver.create_snap(self, grab)
|
|
else:
|
|
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)
|
|
|
|
|
|
# Called when this object is dropped
|
|
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
|
|
|
|
if _gpu_particles != null:
|
|
_gpu_particles.amount_ratio = 0.0
|
|
|
|
# Get the grab information
|
|
var grab := _grab_driver.get_grab(by)
|
|
if not grab:
|
|
return
|
|
|
|
# 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
|
|
|
|
# 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
|
|
|
|
# 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
|
|
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
|
|
|
|
|
|
## Get the controller currently holding this object
|
|
func get_picked_up_by_controller() -> XRController3D:
|
|
# Skip if not picked up
|
|
if not is_picked_up():
|
|
return null
|
|
|
|
# Get the primary pickup controller
|
|
return _grab_driver.primary.controller
|
|
|
|
|
|
## Get the active grab-point this object is held by
|
|
func get_active_grab_point() -> XRToolsGrabPoint:
|
|
# Skip if not picked up
|
|
if not is_picked_up():
|
|
return null
|
|
|
|
return _grab_driver.primary.point
|
|
|
|
|
|
## Switch the active grab-point for this object
|
|
func switch_active_grab_point(grab_point : XRToolsGrabPoint):
|
|
# 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
|
|
|
|
|
|
func _set_ranged_grab_method(new_value: int) -> void:
|
|
ranged_grab_method = new_value
|
|
can_ranged_grab = new_value != RangedMethod.NONE
|
|
|
|
func _on_action_pressed(variant: Variant):
|
|
_controller = get_picked_up_by_controller()
|
|
|