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

322 lines
8.1 KiB
GDScript3
Raw Normal View History

2023-10-16 20:10:20 +03:00
@tool
class_name XRToolsSnapZone
extends Area3D
## Signal emitted when the snap-zone picks something up
signal has_picked_up(what)
## Signal emitted when the snap-zone drops something
signal has_dropped
# Signal emitted when the highlight state changes
signal highlight_updated(pickable, enable)
# Signal emitted when the highlight state changes
signal close_highlight_updated(pickable, enable)
## Enumeration of snap mode
enum SnapMode {
DROPPED, ## Snap only when the object is dropped
RANGE, ## Snap whenever an object is in range
}
## Enable or disable snap-zone
@export var enabled : bool = true: set = _set_enabled
## Optional audio stream to play when a object snaps to the zone
@export var stash_sound : AudioStream
## Grab distance
@export var grab_distance : float = 0.3: set = _set_grab_distance
## Snap mode
@export var snap_mode : SnapMode = SnapMode.DROPPED: set = _set_snap_mode
## Require snap items to be in specified group
@export var snap_require : String = ""
## Deny snapping items in the specified group
@export var snap_exclude : String = ""
## Require grab-by to be in the specified group
@export var grab_require : String = ""
## Deny grab-by
@export var grab_exclude : String= ""
## Initial object in snap zone
@export var initial_object : NodePath
# Public fields
var closest_object : Node3D = null
var picked_up_object : Node3D = null
var picked_up_ranged : bool = true
# Private fields
var _object_in_grab_area = Array()
# Add support for is_xr_class on XRTools classes
func is_xr_class(name : String) -> bool:
return name == "XRToolsSnapZone"
func _ready():
# Set collision shape radius
$CollisionShape3D.shape.radius = grab_distance
# Perform updates
_update_snap_mode()
# Perform the initial object check when next idle
if not Engine.is_editor_hint():
_initial_object_check.call_deferred()
# Called on each frame to update the pickup
func _process(_delta):
# Skip if in editor or not enabled
if Engine.is_editor_hint() or not enabled:
return
# Skip if we aren't doing range-checking
if snap_mode != SnapMode.RANGE:
return
# Skip if already holding a valid object
if is_instance_valid(picked_up_object):
return
# Check for any object in range that can be grabbed
for o in _object_in_grab_area:
# skip objects that can not be picked up
if not o.can_pick_up(self):
continue
# pick up our target
pick_up_object(o)
return
# Pickable Method: snap-zone can be grabbed if holding object
func can_pick_up(by: Node3D) -> bool:
# Refuse if not enabled
if not enabled:
return false
# Refuse if no object is held
if not is_instance_valid(picked_up_object):
return false
# Refuse if the grab-by is not in the required group
if not grab_require.is_empty() and not by.is_in_group(grab_require):
return false
# Refuse if the grab-by is in the excluded group
if not grab_exclude.is_empty() and by.is_in_group(grab_exclude):
return false
# Grab is permitted
return true
# Pickable Method: Snap points can't be picked up
func is_picked_up() -> bool:
return false
# Pickable Method: Gripper-actions can't occur on snap zones
func action():
pass
# Ignore highlighting requests from XRToolsFunctionPickup
func request_highlight(from : Node, on : bool = true) -> void:
if picked_up_object:
picked_up_object.request_highlight(from, on)
# Pickable Method: Object being grabbed from this snap zone
func pick_up(_by: Node3D, _with_controller: XRController3D) -> void:
pass
# Pickable Method: Player never graps snap-zone
func let_go(_p_linear_velocity: Vector3, _p_angular_velocity: Vector3) -> void:
pass
# Pickup Method: Drop the currently picked up object
func drop_object() -> void:
if not is_instance_valid(picked_up_object):
return
# let go of this object
picked_up_object.let_go(Vector3.ZERO, Vector3.ZERO)
picked_up_object = null
has_dropped.emit()
highlight_updated.emit(self, enabled)
# Check for an initial object pickup
func _initial_object_check() -> void:
# Check for an initial object
if initial_object:
# Force pick-up the initial object
pick_up_object(get_node(initial_object))
else:
# Show highlight when empty and enabled
highlight_updated.emit(self, enabled)
# Called when a body enters the snap zone
func _on_snap_zone_body_entered(target: Node3D) -> void:
# Ignore objects already known about
if _object_in_grab_area.find(target) >= 0:
return
# Reject objects which don't support picking up
if not target.has_method('pick_up'):
return
# Reject objects not in the required snap group
if not snap_require.is_empty() and not target.is_in_group(snap_require):
return
# Reject objects in the excluded snap group
if not snap_exclude.is_empty() and target.is_in_group(snap_exclude):
return
# Reject climbable objects
if target is XRToolsClimbable:
return
# Add to the list of objects in grab area
_object_in_grab_area.push_back(target)
# If this snap zone is configured to snap objects that are dropped, then
# start listening for the objects dropped signal
if snap_mode == SnapMode.DROPPED and target.has_signal("dropped"):
target.connect("dropped", _on_target_dropped, CONNECT_DEFERRED)
# Show highlight when something could be snapped
if not is_instance_valid(picked_up_object):
close_highlight_updated.emit(self, enabled)
# Called when a body leaves the snap zone
func _on_snap_zone_body_exited(target: Node3D) -> void:
# Ensure the object is not in our list
_object_in_grab_area.erase(target)
# Stop listening for dropped signals
if target.has_signal("dropped") and target.is_connected("dropped", _on_target_dropped):
target.disconnect("dropped", _on_target_dropped)
# Hide highlight when nothing could be snapped
if _object_in_grab_area.is_empty():
close_highlight_updated.emit(self, false)
# Test if this snap zone has a picked up object
func has_snapped_object() -> bool:
return is_instance_valid(picked_up_object)
# Pick up the specified object
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
# Pick up our target. Note, target may do instant drop_and_free
picked_up_object = target
var player = get_node("AudioStreamPlayer3D")
if is_instance_valid(player):
if player.playing:
player.stop()
player.stream = stash_sound
player.play()
target.pick_up(self, null)
# If object picked up then emit signal
if is_instance_valid(picked_up_object):
has_picked_up.emit(picked_up_object)
highlight_updated.emit(self, false)
# Called when the enabled property has been modified
func _set_enabled(p_enabled: bool) -> void:
enabled = p_enabled
if is_inside_tree:
highlight_updated.emit(
self,
enabled and not is_instance_valid(picked_up_object))
# Called when the grab distance has been modified
func _set_grab_distance(new_value: float) -> void:
grab_distance = new_value
if is_inside_tree() and $CollisionShape3D:
$CollisionShape3D.shape.radius = grab_distance
# Called when the snap mode property has been modified
func _set_snap_mode(new_value: SnapMode) -> void:
snap_mode = new_value
if is_inside_tree():
_update_snap_mode()
# Handle changes to the snap mode
func _update_snap_mode() -> void:
match snap_mode:
SnapMode.DROPPED:
# Disable _process as we aren't using RANGE pickups
set_process(false)
# Start monitoring all objects in range for drop
for o in _object_in_grab_area:
o.connect("dropped", _on_target_dropped, CONNECT_DEFERRED)
SnapMode.RANGE:
# Enable _process to scan for RANGE pickups
set_process(true)
# Clear any dropped signal hooks
for o in _object_in_grab_area:
o.disconnect("dropped", _on_target_dropped)
# Called when a target in our grab area is dropped
func _on_target_dropped(target: Node3D) -> void:
# Skip if not enabled
if not enabled:
return
# Skip if already holding a valid object
if is_instance_valid(picked_up_object):
return
# Skip if the target is not valid
if not is_instance_valid(target):
return
# Pick up the target if we can
if target.can_pick_up(self):
pick_up_object(target)