init
This commit is contained in:
126
addons/godot-xr-tools/interactables/interactable_area_button.gd
Normal file
126
addons/godot-xr-tools/interactables/interactable_area_button.gd
Normal file
@@ -0,0 +1,126 @@
|
||||
@tool
|
||||
class_name XRToolsInteractableAreaButton
|
||||
extends Area3D
|
||||
|
||||
|
||||
## XR Tools Interactable Area Button script
|
||||
##
|
||||
## The interactable area button detects objects and areas intering its
|
||||
## area, and moves an associated button object using a tween to animate
|
||||
## the movement.
|
||||
|
||||
|
||||
## Button pressed event
|
||||
signal button_pressed(button)
|
||||
|
||||
## Button released event
|
||||
signal button_released(button)
|
||||
|
||||
|
||||
## Button object
|
||||
@export var button := NodePath()
|
||||
|
||||
## Displacement when pressed
|
||||
@export var displacement : Vector3 = Vector3(0.0, -0.02, 0.0)
|
||||
|
||||
## Displacement duration
|
||||
@export var duration : float = 0.1
|
||||
|
||||
|
||||
## If true, the button is pressed
|
||||
var pressed : bool = false
|
||||
|
||||
## Dictionary of trigger items pressing the button
|
||||
var _trigger_items := {}
|
||||
|
||||
## Tween for animating button
|
||||
var _tween: Tween
|
||||
|
||||
|
||||
# Node references
|
||||
@onready var _button: Node3D = get_node(button)
|
||||
|
||||
# Button positions
|
||||
@onready var _button_up := _button.transform.origin
|
||||
@onready var _button_down := _button_up + displacement
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsInteractableAreaButton"
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
# Connect area signals
|
||||
if area_entered.connect(_on_button_entered):
|
||||
push_error("Unable to connect button area signal")
|
||||
if area_exited.connect(_on_button_exited):
|
||||
push_error("Unable to connect button area signal")
|
||||
if body_entered.connect(_on_button_entered):
|
||||
push_error("Unable to connect button area signal")
|
||||
if body_exited.connect(_on_button_exited):
|
||||
push_error("Unable to connect button area signal")
|
||||
|
||||
|
||||
# Called when an area or body enters the button area
|
||||
func _on_button_entered(item: Node3D) -> void:
|
||||
# Add to the dictionary of trigger items
|
||||
_trigger_items[item] = item
|
||||
|
||||
# Detect transition to pressed
|
||||
if !pressed:
|
||||
# Update state to pressed
|
||||
pressed = true
|
||||
|
||||
# Kill the current tween
|
||||
if _tween:
|
||||
_tween.kill()
|
||||
|
||||
# Construct the button animation tween
|
||||
_tween = create_tween()
|
||||
_tween.set_trans(Tween.TRANS_LINEAR)
|
||||
_tween.set_ease(Tween.EASE_IN_OUT)
|
||||
_tween.tween_property(_button, "position", _button_down, duration)
|
||||
|
||||
# Emit the pressed signal
|
||||
button_pressed.emit(self)
|
||||
|
||||
|
||||
# Called when an area or body exits the button area
|
||||
func _on_button_exited(item: Node3D) -> void:
|
||||
# Remove from the dictionary of triggered items
|
||||
_trigger_items.erase(item)
|
||||
|
||||
# Detect transition to released
|
||||
if pressed and _trigger_items.is_empty():
|
||||
# Update state to released
|
||||
pressed = false
|
||||
|
||||
# Kill the current tween
|
||||
if _tween:
|
||||
_tween.kill()
|
||||
|
||||
# Construct the button animation tween
|
||||
_tween = create_tween()
|
||||
_tween.set_trans(Tween.TRANS_LINEAR)
|
||||
_tween.set_ease(Tween.EASE_IN_OUT)
|
||||
_tween.tween_property(_button, "position", _button_up, duration)
|
||||
|
||||
# Emit the released signal
|
||||
button_released.emit(self)
|
||||
|
||||
|
||||
# Check button configuration
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
|
||||
# Ensure a button has been specified
|
||||
if not get_node_or_null(button):
|
||||
warnings.append("Button node to animate must be specified")
|
||||
|
||||
# Ensure a valid duration
|
||||
if duration <= 0.0:
|
||||
warnings.append("Duration must be a positive number")
|
||||
|
||||
return warnings
|
||||
@@ -0,0 +1 @@
|
||||
uid://dq042kibpfryy
|
||||
@@ -0,0 +1,8 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://cme66uldrjl2i"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dq042kibpfryy" path="res://addons/godot-xr-tools/interactables/interactable_area_button.gd" id="1"]
|
||||
|
||||
[node name="InteractableAreaButton" type="Area3D"]
|
||||
collision_layer = 0
|
||||
collision_mask = 131072
|
||||
script = ExtResource("1")
|
||||
88
addons/godot-xr-tools/interactables/interactable_handle.gd
Normal file
88
addons/godot-xr-tools/interactables/interactable_handle.gd
Normal file
@@ -0,0 +1,88 @@
|
||||
@tool
|
||||
class_name XRToolsInteractableHandle
|
||||
extends XRToolsPickable
|
||||
|
||||
|
||||
## XR Tools Interactable Handle script
|
||||
##
|
||||
## The interactable handle is a (usually invisible) object extending from
|
||||
## [XRToolsPickable] that can be grabbed by the player and is used to
|
||||
## manipulate interactable objects.
|
||||
##
|
||||
## The interactible handle has an origin position of its parent. In order
|
||||
## to position interactible handles on the interactible object, the handle
|
||||
## should be placed under a parent handle-origin node, and the origin nodes
|
||||
## position set as desired.
|
||||
##
|
||||
## When the handle is released, it snaps back to its parent origin. If the
|
||||
## handle is pulled further than its snap distance, then the handle is
|
||||
## automatically released.
|
||||
|
||||
|
||||
## Distance from the handle origin to auto-snap the grab
|
||||
@export var snap_distance : float = 0.3
|
||||
|
||||
|
||||
# Handle origin spatial node
|
||||
@onready var handle_origin: Node3D = get_parent()
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsInteractableHandle" or super(xr_name)
|
||||
|
||||
|
||||
# Called when this handle is added to the scene
|
||||
func _ready() -> void:
|
||||
# In Godot 4 we must now manually call our super class ready function
|
||||
super()
|
||||
|
||||
# Ensure we start at our origin
|
||||
transform = Transform3D.IDENTITY
|
||||
|
||||
# Turn off processing - it will be turned on only when held
|
||||
set_process(false)
|
||||
|
||||
|
||||
# Called on every frame when the handle is held to check for snapping
|
||||
func _process(_delta: float) -> void:
|
||||
# Skip if not picked up
|
||||
if not is_picked_up():
|
||||
return
|
||||
|
||||
# If too far from the origin then drop the handle
|
||||
var origin_pos = handle_origin.global_transform.origin
|
||||
var handle_pos = global_transform.origin
|
||||
if handle_pos.distance_to(origin_pos) > snap_distance:
|
||||
drop()
|
||||
|
||||
|
||||
# Called when the handle is picked up
|
||||
func pick_up(by) -> void:
|
||||
# Call the base-class to perform the pickup
|
||||
super(by)
|
||||
|
||||
# Enable the process function while held
|
||||
set_process(true)
|
||||
|
||||
|
||||
# Called when the handle is dropped
|
||||
func let_go(by: Node3D, _p_linear_velocity: Vector3, _p_angular_velocity: Vector3) -> void:
|
||||
# Call the base-class to perform the drop, but with no velocity
|
||||
super(by, Vector3.ZERO, Vector3.ZERO)
|
||||
|
||||
# Disable the process function as no-longer held
|
||||
set_process(false)
|
||||
|
||||
# Snap the handle back to the origin
|
||||
transform = Transform3D.IDENTITY
|
||||
|
||||
|
||||
# Check handle configurationv
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
|
||||
if !transform.is_equal_approx(Transform3D.IDENTITY):
|
||||
warnings.append("Interactable handle must have no transform from its parent handle origin")
|
||||
|
||||
return warnings
|
||||
@@ -0,0 +1 @@
|
||||
uid://dlojnwxo25bky
|
||||
10
addons/godot-xr-tools/interactables/interactable_handle.tscn
Normal file
10
addons/godot-xr-tools/interactables/interactable_handle.tscn
Normal file
@@ -0,0 +1,10 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://bddj5m7ull6g0"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dlojnwxo25bky" path="res://addons/godot-xr-tools/interactables/interactable_handle.gd" id="1"]
|
||||
|
||||
[node name="InteractableHandle" type="RigidBody3D"]
|
||||
collision_layer = 262144
|
||||
collision_mask = 0
|
||||
gravity_scale = 0.0
|
||||
script = ExtResource("1")
|
||||
picked_up_layer = 0
|
||||
@@ -0,0 +1,81 @@
|
||||
@tool
|
||||
class_name XRToolsInteractableHandleDriven
|
||||
extends Node3D
|
||||
|
||||
|
||||
## XR Tools Interactable Handle Driven script
|
||||
##
|
||||
## This is the base class for interactables driven by handles. It subscribes
|
||||
## to all child handle picked_up and dropped signals, and maintains a list
|
||||
## of all grabbed handles.
|
||||
##
|
||||
## When one or more handles are grabbed, the _process function is enabled
|
||||
## to process the handle-driven movement.
|
||||
|
||||
|
||||
## Signal called when this interactable is grabbed
|
||||
signal grabbed(interactable)
|
||||
|
||||
## Signal called when this interactable is released
|
||||
signal released(interactable)
|
||||
|
||||
|
||||
# Array of handles currently grabbed
|
||||
var grabbed_handles := Array()
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsInteractableHandleDriven"
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
# Hook picked_up and dropped signals from all child handles
|
||||
_hook_child_handles(self)
|
||||
|
||||
# Turn off processing until a handle is grabbed
|
||||
set_process(false)
|
||||
|
||||
|
||||
# Called when a handle is picked up
|
||||
func _on_handle_picked_up(handle: XRToolsInteractableHandle) -> void:
|
||||
# Append to the list of grabbed handles
|
||||
grabbed_handles.append(handle)
|
||||
|
||||
# Enable processing
|
||||
if grabbed_handles.size() == 1:
|
||||
# Report grabbed
|
||||
emit_signal("grabbed", self)
|
||||
|
||||
# Enable physics processing
|
||||
set_process(true)
|
||||
|
||||
|
||||
# Called when a handle is dropped
|
||||
func _on_handle_dropped(handle: XRToolsInteractableHandle) -> void:
|
||||
# Remove from the list of grabbed handles
|
||||
grabbed_handles.erase(handle)
|
||||
|
||||
# Disable processing when we drop the last handle
|
||||
if grabbed_handles.is_empty():
|
||||
# Disable physics processing
|
||||
set_process(false)
|
||||
|
||||
# Report released
|
||||
emit_signal("released", self)
|
||||
|
||||
|
||||
# Recursive function to hook picked_up and dropped signals in all child handles
|
||||
func _hook_child_handles(node: Node) -> void:
|
||||
# If this node is a handle then hook its handle signals
|
||||
var handle := node as XRToolsInteractableHandle
|
||||
if handle:
|
||||
if handle.picked_up.connect(_on_handle_picked_up):
|
||||
push_error("Unable to connect handle signal")
|
||||
if handle.dropped.connect(_on_handle_dropped):
|
||||
push_error("Unable to connect handle signal")
|
||||
|
||||
# Recurse into all children
|
||||
for child in node.get_children():
|
||||
_hook_child_handles(child)
|
||||
@@ -0,0 +1 @@
|
||||
uid://jeeqtkcsy7ou
|
||||
155
addons/godot-xr-tools/interactables/interactable_hinge.gd
Normal file
155
addons/godot-xr-tools/interactables/interactable_hinge.gd
Normal file
@@ -0,0 +1,155 @@
|
||||
@tool
|
||||
class_name XRToolsInteractableHinge
|
||||
extends XRToolsInteractableHandleDriven
|
||||
|
||||
|
||||
## XR Tools Interactable Hinge script
|
||||
##
|
||||
## The interactable hinge is a hinge transform node controlled by the
|
||||
## player through one or more [XRToolsInteractableHandle] instances.
|
||||
##
|
||||
## The hinge rotates itelf around its local X axis, and so should be
|
||||
## placed as a child of a node to translate and rotate as appropriate.
|
||||
##
|
||||
## The interactable hinge is not a [RigidBody3D], and as such will not react
|
||||
## to any collisions.
|
||||
|
||||
|
||||
## Signal for hinge moved
|
||||
signal hinge_moved(angle)
|
||||
|
||||
|
||||
## Hinge minimum limit
|
||||
@export var hinge_limit_min : float = -45.0: set = _set_hinge_limit_min
|
||||
|
||||
## Hinge maximum limit
|
||||
@export var hinge_limit_max : float = 45.0: set = _set_hinge_limit_max
|
||||
|
||||
## Hinge step size (zero for no steps)
|
||||
@export var hinge_steps : float = 0.0: set = _set_hinge_steps
|
||||
|
||||
## Hinge position
|
||||
@export var hinge_position : float = 0.0: set = _set_hinge_position
|
||||
|
||||
## Default position
|
||||
@export var default_position : float = 0.0: set = _set_default_position
|
||||
|
||||
## If true, the hinge moves to the default position when releases
|
||||
@export var default_on_release : bool = false
|
||||
|
||||
|
||||
# Hinge values in radians
|
||||
@onready var _hinge_limit_min_rad : float = deg_to_rad(hinge_limit_min)
|
||||
@onready var _hinge_limit_max_rad : float = deg_to_rad(hinge_limit_max)
|
||||
@onready var _hinge_steps_rad : float = deg_to_rad(hinge_steps)
|
||||
@onready var _hinge_position_rad : float = deg_to_rad(hinge_position)
|
||||
@onready var _default_position_rad : float = deg_to_rad(default_position)
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsInteractableHinge" or super(xr_name)
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
# In Godot 4 we must now manually call our super class ready function
|
||||
super()
|
||||
|
||||
# Set the initial position to match the initial hinge position value
|
||||
transform = Transform3D(
|
||||
Basis.from_euler(Vector3(_hinge_position_rad, 0, 0)),
|
||||
Vector3.ZERO
|
||||
)
|
||||
|
||||
# Connect signals
|
||||
if released.connect(_on_hinge_released):
|
||||
push_error("Cannot connect hinge released signal")
|
||||
|
||||
|
||||
# Called every frame when one or more handles are held by the player
|
||||
func _process(_delta: float) -> void:
|
||||
# Get the total handle angular offsets
|
||||
var offset_sum := 0.0
|
||||
for item in grabbed_handles:
|
||||
var handle := item as XRToolsInteractableHandle
|
||||
var to_handle: Vector3 = handle.global_transform.origin * global_transform
|
||||
var to_handle_origin: Vector3 = handle.handle_origin.global_transform.origin * global_transform
|
||||
to_handle.x = 0.0
|
||||
to_handle_origin.x = 0.0
|
||||
offset_sum += to_handle_origin.signed_angle_to(to_handle, Vector3.RIGHT)
|
||||
|
||||
# Average the angular offsets
|
||||
var offset := offset_sum / grabbed_handles.size()
|
||||
|
||||
# Move the hinge by the requested offset
|
||||
move_hinge(_hinge_position_rad + offset)
|
||||
|
||||
|
||||
# Move the hinge to the specified position
|
||||
func move_hinge(pos: float) -> void:
|
||||
# Do the hinge move
|
||||
pos = _do_move_hinge(pos)
|
||||
if pos == _hinge_position_rad:
|
||||
return
|
||||
|
||||
# Update the current positon
|
||||
_hinge_position_rad = pos
|
||||
hinge_position = rad_to_deg(pos)
|
||||
|
||||
# Emit the moved signal
|
||||
emit_signal("hinge_moved", hinge_position)
|
||||
|
||||
|
||||
# Handle release of hinge
|
||||
func _on_hinge_released(_interactable: XRToolsInteractableHinge):
|
||||
if default_on_release:
|
||||
move_hinge(_default_position_rad)
|
||||
|
||||
|
||||
# Called when hinge_limit_min is set externally
|
||||
func _set_hinge_limit_min(value: float) -> void:
|
||||
hinge_limit_min = value
|
||||
_hinge_limit_min_rad = deg_to_rad(value)
|
||||
|
||||
|
||||
# Called when hinge_limit_max is set externally
|
||||
func _set_hinge_limit_max(value: float) -> void:
|
||||
hinge_limit_max = value
|
||||
_hinge_limit_max_rad = deg_to_rad(value)
|
||||
|
||||
|
||||
# Called when hinge_steps is set externally
|
||||
func _set_hinge_steps(value: float) -> void:
|
||||
hinge_steps = value
|
||||
_hinge_steps_rad = deg_to_rad(value)
|
||||
|
||||
|
||||
# Called when hinge_position is set externally
|
||||
func _set_hinge_position(value: float) -> void:
|
||||
var pos = _do_move_hinge(deg_to_rad(value))
|
||||
hinge_position = rad_to_deg(pos)
|
||||
_hinge_position_rad = pos
|
||||
|
||||
|
||||
# Called when default_position is set externally
|
||||
func _set_default_position(value: float) -> void:
|
||||
default_position = value
|
||||
_default_position_rad = deg_to_rad(value)
|
||||
|
||||
|
||||
# Do the hinge move
|
||||
func _do_move_hinge(pos: float) -> float:
|
||||
# Apply hinge step-quantization
|
||||
if _hinge_steps_rad:
|
||||
pos = round(pos / _hinge_steps_rad) * _hinge_steps_rad
|
||||
|
||||
# Apply hinge limits
|
||||
pos = clamp(pos, _hinge_limit_min_rad, _hinge_limit_max_rad)
|
||||
|
||||
# Move if necessary
|
||||
if pos != _hinge_position_rad:
|
||||
transform.basis = Basis.from_euler(Vector3(pos, 0.0, 0.0))
|
||||
|
||||
# Return the updated position
|
||||
return pos
|
||||
@@ -0,0 +1 @@
|
||||
uid://c8gx2wc7hsxf5
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://bnvxqb33lltmj"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c8gx2wc7hsxf5" path="res://addons/godot-xr-tools/interactables/interactable_hinge.gd" id="1"]
|
||||
|
||||
[node name="InteractableHinge" type="Spatial"]
|
||||
script = ExtResource("1")
|
||||
236
addons/godot-xr-tools/interactables/interactable_joystick.gd
Normal file
236
addons/godot-xr-tools/interactables/interactable_joystick.gd
Normal file
@@ -0,0 +1,236 @@
|
||||
@tool
|
||||
class_name XRToolsInteractableJoystick
|
||||
extends XRToolsInteractableHandleDriven
|
||||
|
||||
|
||||
## XR Tools Interactable Joystick script
|
||||
##
|
||||
## The interactable joystick is a joystick transform node controlled by the
|
||||
## player through [XRToolsInteractableHandle] instances.
|
||||
##
|
||||
## The joystick rotates itelf around its local X/Y axes, and so should be
|
||||
## placed as a child of a node to translate and rotate as appropriate.
|
||||
##
|
||||
## The interactable joystick is not a [RigidBody3D], and as such will not react
|
||||
## to any collisions.
|
||||
|
||||
|
||||
## Signal for hinge moved
|
||||
signal joystick_moved(x_angle, y_angle)
|
||||
|
||||
|
||||
## Constant for flattening a vector horizontally (X/Z only)
|
||||
const VECTOR_XZ := Vector3(1.0, 0.0, 1.0)
|
||||
|
||||
## Constant for flattening a vector vertically (Y/Z only)
|
||||
const VECTOR_YZ := Vector3(0.0, 1.0, 1.0)
|
||||
|
||||
|
||||
## Joystick X minimum limit
|
||||
@export var joystick_x_limit_min : float = -45.0: set = _set_joystick_x_limit_min
|
||||
|
||||
## Joystick X maximum limit
|
||||
@export var joystick_x_limit_max : float = 45.0: set = _set_joystick_x_limit_max
|
||||
|
||||
## Joystick Y minimum limit
|
||||
@export var joystick_y_limit_min : float = -45.0: set = _set_joystick_y_limit_min
|
||||
|
||||
## Joystick Y maximum limit
|
||||
@export var joystick_y_limit_max : float = 45.0: set = _set_joystick_y_limit_max
|
||||
|
||||
## Joystick X step size (zero for no steps)
|
||||
@export var joystick_x_steps : float = 0.0: set = _set_joystick_x_steps
|
||||
|
||||
## Joystick Y step size (zero for no steps)
|
||||
@export var joystick_y_steps : float = 0.0: set = _set_joystick_y_steps
|
||||
|
||||
## Joystick X position
|
||||
@export var joystick_x_position : float = 0.0: set = _set_joystick_x_position
|
||||
|
||||
## Joystick Y position
|
||||
@export var joystick_y_position : float = 0.0: set = _set_joystick_y_position
|
||||
|
||||
## Default X position
|
||||
@export var default_x_position : float = 0.0: set = _set_default_x_position
|
||||
|
||||
## Default Y position
|
||||
@export var default_y_position : float = 0.0: set = _set_default_y_position
|
||||
|
||||
## If true, the joystick moves to the default position when released
|
||||
@export var default_on_release : bool = false
|
||||
|
||||
|
||||
# Joystick values in radians
|
||||
@onready var _joystick_x_limit_min_rad : float = deg_to_rad(joystick_x_limit_min)
|
||||
@onready var _joystick_x_limit_max_rad : float = deg_to_rad(joystick_x_limit_max)
|
||||
@onready var _joystick_y_limit_min_rad : float = deg_to_rad(joystick_y_limit_min)
|
||||
@onready var _joystick_y_limit_max_rad : float = deg_to_rad(joystick_y_limit_max)
|
||||
@onready var _joystick_x_steps_rad : float = deg_to_rad(joystick_x_steps)
|
||||
@onready var _joystick_y_steps_rad : float = deg_to_rad(joystick_y_steps)
|
||||
@onready var _joystick_x_position_rad : float = deg_to_rad(joystick_x_position)
|
||||
@onready var _joystick_y_position_rad : float = deg_to_rad(joystick_y_position)
|
||||
@onready var _default_x_position_rad : float = deg_to_rad(default_x_position)
|
||||
@onready var _default_y_position_rad : float = deg_to_rad(default_y_position)
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsInteractableJoystick" or super(xr_name)
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
# In Godot 4 we must now manually call our super class ready function
|
||||
super()
|
||||
|
||||
# Set the initial position to match the initial joystick position value
|
||||
transform = Transform3D(
|
||||
Basis.from_euler(Vector3(_joystick_y_position_rad, _joystick_x_position_rad, 0)),
|
||||
Vector3.ZERO)
|
||||
|
||||
# Connect signals
|
||||
if released.connect(_on_joystick_released):
|
||||
push_error("Cannot connect joystick released signal")
|
||||
|
||||
|
||||
# Called every frame when one or more handles are held by the player
|
||||
func _process(_delta: float) -> void:
|
||||
# Do not process in the editor
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# Skip if no handles grabbed
|
||||
if grabbed_handles.is_empty():
|
||||
return
|
||||
|
||||
# Get the total handle angular offsets
|
||||
var offset_x_sum := 0.0
|
||||
var offset_y_sum := 0.0
|
||||
for item in grabbed_handles:
|
||||
var handle := item as XRToolsInteractableHandle
|
||||
var to_handle: Vector3 = handle.global_transform.origin * global_transform
|
||||
var to_handle_origin: Vector3 = handle.handle_origin.global_transform.origin * global_transform
|
||||
|
||||
var to_handle_x := to_handle * VECTOR_XZ
|
||||
var to_handle_origin_x := to_handle_origin * VECTOR_XZ
|
||||
offset_x_sum += to_handle_origin_x.signed_angle_to(to_handle_x, Vector3.UP)
|
||||
|
||||
var to_handle_y := to_handle * VECTOR_YZ
|
||||
var to_handle_origin_y := to_handle_origin * VECTOR_YZ
|
||||
offset_y_sum += to_handle_origin_y.signed_angle_to(to_handle_y, Vector3.RIGHT)
|
||||
|
||||
# Average the angular offsets
|
||||
var offset_x := offset_x_sum / grabbed_handles.size()
|
||||
var offset_y := offset_y_sum / grabbed_handles.size()
|
||||
|
||||
# Move the joystick by the requested offset
|
||||
move_joystick(
|
||||
_joystick_x_position_rad + offset_x,
|
||||
_joystick_y_position_rad + offset_y)
|
||||
|
||||
|
||||
# Move the joystick to the specified position
|
||||
func move_joystick(position_x: float, position_y: float) -> void:
|
||||
# Do the move
|
||||
var pos := _do_move_joystick(Vector2(position_x, position_y))
|
||||
if pos.x == _joystick_x_position_rad and pos.y == _joystick_y_position_rad:
|
||||
return
|
||||
|
||||
# Update the current positon
|
||||
_joystick_x_position_rad = pos.x
|
||||
_joystick_y_position_rad = pos.y
|
||||
joystick_x_position = rad_to_deg(pos.x)
|
||||
joystick_y_position = rad_to_deg(pos.y)
|
||||
|
||||
# Emit the joystick signal
|
||||
emit_signal("joystick_moved", joystick_x_position, joystick_y_position)
|
||||
|
||||
|
||||
# Handle release of joystick
|
||||
func _on_joystick_released(_interactable: XRToolsInteractableJoystick):
|
||||
if default_on_release:
|
||||
move_joystick(_default_x_position_rad, _default_y_position_rad)
|
||||
|
||||
|
||||
# Called when joystick_x_limit_min is set externally
|
||||
func _set_joystick_x_limit_min(value: float) -> void:
|
||||
joystick_x_limit_min = value
|
||||
_joystick_x_limit_min_rad = deg_to_rad(value)
|
||||
|
||||
|
||||
# Called when joystick_y_limit_min is set externally
|
||||
func _set_joystick_y_limit_min(value: float) -> void:
|
||||
joystick_y_limit_min = value
|
||||
_joystick_y_limit_min_rad = deg_to_rad(value)
|
||||
|
||||
|
||||
# Called when joystick_x_limit_max is set externally
|
||||
func _set_joystick_x_limit_max(value: float) -> void:
|
||||
joystick_x_limit_max = value
|
||||
_joystick_x_limit_max_rad = deg_to_rad(value)
|
||||
|
||||
|
||||
# Called when joystick_y_limit_max is set externally
|
||||
func _set_joystick_y_limit_max(value: float) -> void:
|
||||
joystick_y_limit_max = value
|
||||
_joystick_y_limit_max_rad = deg_to_rad(value)
|
||||
|
||||
|
||||
# Called when joystick_x_steps is set externally
|
||||
func _set_joystick_x_steps(value: float) -> void:
|
||||
joystick_x_steps = value
|
||||
_joystick_x_steps_rad = deg_to_rad(value)
|
||||
|
||||
|
||||
# Called when joystick_y_steps is set externally
|
||||
func _set_joystick_y_steps(value: float) -> void:
|
||||
joystick_y_steps = value
|
||||
_joystick_y_steps_rad = deg_to_rad(value)
|
||||
|
||||
|
||||
# Called when joystick_x_position is set externally
|
||||
func _set_joystick_x_position(value: float) -> void:
|
||||
var pos := Vector2(deg_to_rad(value), _joystick_y_position_rad)
|
||||
pos = _do_move_joystick(pos)
|
||||
joystick_x_position = rad_to_deg(pos.x)
|
||||
_joystick_x_position_rad = pos.x
|
||||
|
||||
|
||||
# Called when joystick_y_position is set externally
|
||||
func _set_joystick_y_position(value: float) -> void:
|
||||
var pos := Vector2(_joystick_x_position_rad, deg_to_rad(value))
|
||||
pos = _do_move_joystick(pos)
|
||||
joystick_y_position = rad_to_deg(pos.y)
|
||||
_joystick_y_position_rad = pos.y
|
||||
|
||||
|
||||
# Called when default_x_position is set externally
|
||||
func _set_default_x_position(value: float) -> void:
|
||||
default_x_position = value
|
||||
_default_x_position_rad = deg_to_rad(value)
|
||||
|
||||
|
||||
# Called when default_y_position is set externally
|
||||
func _set_default_y_position(value: float) -> void:
|
||||
default_y_position = value
|
||||
_default_y_position_rad = deg_to_rad(value)
|
||||
|
||||
|
||||
# Do the joystick move
|
||||
func _do_move_joystick(pos: Vector2) -> Vector2:
|
||||
# Apply joystick step-quantization
|
||||
if _joystick_x_steps_rad:
|
||||
pos.x = round(pos.x / _joystick_x_steps_rad) * _joystick_x_steps_rad
|
||||
if _joystick_y_steps_rad:
|
||||
pos.y = round(pos.y / _joystick_y_steps_rad) * _joystick_y_steps_rad
|
||||
|
||||
# Apply joystick limits
|
||||
pos.x = clamp(pos.x, _joystick_x_limit_min_rad, _joystick_x_limit_max_rad)
|
||||
pos.y = clamp(pos.y, _joystick_y_limit_min_rad, _joystick_y_limit_max_rad)
|
||||
|
||||
# Move if necessary
|
||||
if pos.x != _joystick_x_position_rad or pos.y != _joystick_y_position_rad:
|
||||
transform.basis = Basis.from_euler(Vector3(pos.y, pos.x, 0.0))
|
||||
|
||||
# Return the updated position
|
||||
return pos
|
||||
@@ -0,0 +1 @@
|
||||
uid://c0j857ar3agno
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://du32t1mb2kd7x"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c0j857ar3agno" path="res://addons/godot-xr-tools/interactables/interactable_joystick.gd" id="1"]
|
||||
|
||||
[node name="InteractableJoystick" type="Spatial"]
|
||||
script = ExtResource("1")
|
||||
120
addons/godot-xr-tools/interactables/interactable_slider.gd
Normal file
120
addons/godot-xr-tools/interactables/interactable_slider.gd
Normal file
@@ -0,0 +1,120 @@
|
||||
@tool
|
||||
class_name XRToolsInteractableSlider
|
||||
extends XRToolsInteractableHandleDriven
|
||||
|
||||
|
||||
## XR Tools Interactable Slider script
|
||||
##
|
||||
## The interactable slider is a slider transform node controlled by the
|
||||
## player through [XRToolsInteractableHandle] instances.
|
||||
##
|
||||
## The slider translates itelf along its local X axis, and so should be
|
||||
## placed as a child of a node to translate and rotate as appropriate.
|
||||
##
|
||||
## The interactable slider is not a [RigidBody3D], and as such will not react
|
||||
## to any collisions.
|
||||
|
||||
|
||||
## Signal for slider moved
|
||||
signal slider_moved(position)
|
||||
|
||||
|
||||
## Slider minimum limit
|
||||
@export var slider_limit_min : float = 0.0
|
||||
|
||||
## Slider maximum limit
|
||||
@export var slider_limit_max : float = 1.0
|
||||
|
||||
## Slider step size (zero for no steps)
|
||||
@export var slider_steps : float = 0.0
|
||||
|
||||
## Slider position
|
||||
@export var slider_position : float = 0.0: set = _set_slider_position
|
||||
|
||||
## Default position
|
||||
@export var default_position : float = 0.0
|
||||
|
||||
## If true, the slider moves to the default position when released
|
||||
@export var default_on_release : bool = false
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsInteractableSlider" or super(xr_name)
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready() -> void:
|
||||
# In Godot 4 we must now manually call our super class ready function
|
||||
super()
|
||||
|
||||
# Set the initial position to match the initial slider position value
|
||||
transform = Transform3D(
|
||||
Basis.IDENTITY,
|
||||
Vector3(slider_position, 0.0, 0.0)
|
||||
)
|
||||
|
||||
# Connect signals
|
||||
if released.connect(_on_slider_released):
|
||||
push_error("Cannot connect slider released signal")
|
||||
|
||||
|
||||
# Called every frame when one or more handles are held by the player
|
||||
func _process(_delta: float) -> void:
|
||||
# Get the total handle offsets
|
||||
var offset_sum := Vector3.ZERO
|
||||
for item in grabbed_handles:
|
||||
var handle := item as XRToolsInteractableHandle
|
||||
offset_sum += handle.global_transform.origin - handle.handle_origin.global_transform.origin
|
||||
|
||||
# Rotate the offset sum vector from global into local coordinate space
|
||||
offset_sum = offset_sum * global_transform.basis
|
||||
|
||||
# Get the average displacement in the X axis
|
||||
var offset := offset_sum.x / grabbed_handles.size()
|
||||
|
||||
# Move the slider by the requested offset
|
||||
move_slider(slider_position + offset)
|
||||
|
||||
|
||||
# Move the slider to the specified position
|
||||
func move_slider(pos: float) -> void:
|
||||
# Do the slider move
|
||||
pos = _do_move_slider(pos)
|
||||
if pos == slider_position:
|
||||
return
|
||||
|
||||
# Update the current position
|
||||
slider_position = pos
|
||||
|
||||
# Emit the moved signal
|
||||
emit_signal("slider_moved", pos)
|
||||
|
||||
|
||||
# Handle release of slider
|
||||
func _on_slider_released(_interactable: XRToolsInteractableSlider):
|
||||
if default_on_release:
|
||||
move_slider(default_position)
|
||||
|
||||
|
||||
# Called when the slider position is set externally
|
||||
func _set_slider_position(pos: float) -> void:
|
||||
pos = _do_move_slider(pos)
|
||||
slider_position = pos
|
||||
|
||||
|
||||
# Do the slider move
|
||||
func _do_move_slider(pos: float) -> float:
|
||||
# Apply slider step-quantization
|
||||
if slider_steps:
|
||||
pos = round(pos / slider_steps) * slider_steps
|
||||
|
||||
# Apply slider limits
|
||||
pos = clamp(pos, slider_limit_min, slider_limit_max)
|
||||
|
||||
# Move if necessary
|
||||
if pos != slider_position:
|
||||
transform.origin.x = pos
|
||||
|
||||
# Return the updated position
|
||||
return pos
|
||||
@@ -0,0 +1 @@
|
||||
uid://cloxfr8r3b00u
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://b0r6edl74ddo7"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cloxfr8r3b00u" path="res://addons/godot-xr-tools/interactables/interactable_slider.gd" id="1"]
|
||||
|
||||
[node name="InteractableSlider" type="Node3D"]
|
||||
script = ExtResource("1")
|
||||
Reference in New Issue
Block a user