init
This commit is contained in:
555
addons/godot-xr-tools/functions/function_gaze_pointer.gd
Normal file
555
addons/godot-xr-tools/functions/function_gaze_pointer.gd
Normal file
@@ -0,0 +1,555 @@
|
||||
@tool
|
||||
@icon("res://addons/godot-xr-tools/editor/icons/function.svg")
|
||||
class_name XRToolsFunctionGazePointer
|
||||
extends Node3D
|
||||
|
||||
|
||||
## XR Tools Function Gaze Pointer Script
|
||||
##
|
||||
## This script implements a pointer function for a players camera. Pointer
|
||||
## events (entered, exited, pressed, release, and movement) are delivered by
|
||||
## invoking signals on the target node.
|
||||
##
|
||||
## Pointer target nodes commonly extend from [XRToolsInteractableArea] or
|
||||
## [XRToolsInteractableBody].
|
||||
|
||||
|
||||
## Signal emitted when this object points at another object
|
||||
signal pointing_event(event)
|
||||
|
||||
|
||||
## Enumeration of laser show modes
|
||||
enum LaserShow {
|
||||
HIDE = 0, ## Hide laser
|
||||
SHOW = 1, ## Show laser
|
||||
COLLIDE = 2, ## Only show laser on collision
|
||||
}
|
||||
|
||||
## Enumeration of laser length modes
|
||||
enum LaserLength {
|
||||
FULL = 0, ## Full length
|
||||
COLLIDE = 1 ## Draw to collision
|
||||
}
|
||||
|
||||
|
||||
## Default pointer collision mask of 21:pointable and 23:ui-objects
|
||||
const DEFAULT_MASK := 0b0000_0000_0101_0000_0000_0000_0000_0000
|
||||
|
||||
## Default pointer collision mask of 23:ui-objects
|
||||
const SUPPRESS_MASK := 0b0000_0000_0100_0000_0000_0000_0000_0000
|
||||
|
||||
|
||||
@export_group("General")
|
||||
|
||||
## Pointer enabled
|
||||
@export var enabled : bool = true: set = set_enabled
|
||||
|
||||
## Y Offset for pointer
|
||||
@export var y_offset : float = -0.013: set = set_y_offset
|
||||
|
||||
## Pointer distance
|
||||
@export var distance : float = 10: set = set_distance
|
||||
|
||||
## Active button action
|
||||
@export var active_button_action : String = "trigger_click"
|
||||
|
||||
@export_group("Laser")
|
||||
|
||||
## Controls when the laser is visible
|
||||
@export var show_laser : LaserShow = LaserShow.SHOW: set = set_show_laser
|
||||
|
||||
## Controls the length of the laser
|
||||
@export var laser_length : LaserLength = LaserLength.FULL: set = set_laser_length
|
||||
|
||||
## Laser pointer material
|
||||
@export var laser_material : StandardMaterial3D = null : set = set_laser_material
|
||||
|
||||
## Laser pointer material when hitting target
|
||||
@export var laser_hit_material : StandardMaterial3D = null : set = set_laser_hit_material
|
||||
|
||||
@export_group("Target")
|
||||
|
||||
## If true, the pointer target is shown
|
||||
@export var show_target : bool = false: set = set_show_target
|
||||
|
||||
## Controls the target radius
|
||||
@export var target_radius : float = 0.05: set = set_target_radius
|
||||
|
||||
## Target material
|
||||
@export var target_material : StandardMaterial3D = null : set = set_target_material
|
||||
|
||||
@export_group("Collision")
|
||||
|
||||
## Pointer collision mask
|
||||
@export_flags_3d_physics var collision_mask : int = DEFAULT_MASK: set = set_collision_mask
|
||||
|
||||
## Enable pointer collision with bodies
|
||||
@export var collide_with_bodies : bool = true: set = set_collide_with_bodies
|
||||
|
||||
## Enable pointer collision with areas
|
||||
@export var collide_with_areas : bool = false: set = set_collide_with_areas
|
||||
|
||||
@export_group("Suppression")
|
||||
|
||||
## Suppress radius
|
||||
@export var suppress_radius : float = 0.2: set = set_suppress_radius
|
||||
|
||||
## Suppress mask
|
||||
@export_flags_3d_physics var suppress_mask : int = SUPPRESS_MASK: set = set_suppress_mask
|
||||
|
||||
@export_group("Gaze Pointer")
|
||||
|
||||
## send clicks on hold or just move the mouse
|
||||
@export var click_on_hold : bool = false
|
||||
|
||||
## time on hold to release a click
|
||||
@export var hold_time : float = 2.0
|
||||
|
||||
## Color our our visualisation
|
||||
@export var color : Color = Color(1.0, 1.0, 1.0, 1.0): set = set_color
|
||||
|
||||
## Size of the pointer end
|
||||
@export var size : Vector2 = Vector2(0.3, 0.3): set = set_size
|
||||
|
||||
## held time counter
|
||||
var time_held = 0.0
|
||||
|
||||
## hold to click cursor material
|
||||
var material : ShaderMaterial
|
||||
|
||||
## bool for release click
|
||||
var gaze_pressed = false
|
||||
|
||||
## Current target node
|
||||
var target : Node3D = null
|
||||
|
||||
## Last target node
|
||||
var last_target : Node3D = null
|
||||
|
||||
## Last collision point
|
||||
var last_collided_at : Vector3 = Vector3.ZERO
|
||||
|
||||
# World scale
|
||||
var _world_scale : float = 1.0
|
||||
|
||||
# The current camera
|
||||
var _camera_parent : XRCamera3D
|
||||
|
||||
## Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsFunctionGazePointer"
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
# Do not initialise if in the editor
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# Read the initial world-scale
|
||||
_world_scale = XRServer.world_scale
|
||||
|
||||
_camera_parent = get_parent() as XRCamera3D
|
||||
|
||||
material = $Visualise.get_surface_override_material(0)
|
||||
|
||||
if !Engine.is_editor_hint():
|
||||
_set_time_held(0.0)
|
||||
|
||||
_update_size()
|
||||
_update_color()
|
||||
|
||||
# init our state
|
||||
_update_y_offset()
|
||||
_update_distance()
|
||||
_update_pointer()
|
||||
_update_target_radius()
|
||||
_update_target_material()
|
||||
_update_collision_mask()
|
||||
_update_collide_with_bodies()
|
||||
_update_collide_with_areas()
|
||||
_update_suppress_radius()
|
||||
_update_suppress_mask()
|
||||
|
||||
|
||||
# Called on each frame to update the pickup
|
||||
func _process(delta):
|
||||
# Do not process if in the editor
|
||||
if Engine.is_editor_hint() or !is_inside_tree():
|
||||
return
|
||||
|
||||
# Handle world-scale changes
|
||||
var new_world_scale := XRServer.world_scale
|
||||
if (_world_scale != new_world_scale):
|
||||
_world_scale = new_world_scale
|
||||
_update_y_offset()
|
||||
|
||||
# Find the new pointer target
|
||||
var new_target : Node3D
|
||||
var new_at : Vector3
|
||||
var suppress_area := $SuppressArea
|
||||
if (enabled and
|
||||
not $SuppressArea.has_overlapping_bodies() and
|
||||
not $SuppressArea.has_overlapping_areas() and
|
||||
$RayCast.is_colliding()):
|
||||
new_at = $RayCast.get_collision_point()
|
||||
if target:
|
||||
# Locked to 'target' even if we're colliding with something else
|
||||
new_target = target
|
||||
else:
|
||||
# Target is whatever the raycast is colliding with
|
||||
new_target = $RayCast.get_collider()
|
||||
|
||||
# hide gaze pointer when pressed
|
||||
if gaze_pressed:
|
||||
show_target = false
|
||||
else:
|
||||
show_target = true
|
||||
|
||||
# If no current or previous collisions then skip
|
||||
if not new_target and not last_target:
|
||||
return
|
||||
|
||||
# Handle pointer changes
|
||||
if new_target and not last_target:
|
||||
# Pointer entered new_target
|
||||
XRToolsPointerEvent.entered(self, new_target, new_at)
|
||||
|
||||
# Pointer moved on new_target for the first time
|
||||
XRToolsPointerEvent.moved(self, new_target, new_at, new_at)
|
||||
|
||||
if click_on_hold and !gaze_pressed:
|
||||
_set_time_held(time_held + delta)
|
||||
if time_held > hold_time:
|
||||
_button_pressed()
|
||||
|
||||
# Update visible artifacts for hit
|
||||
_visible_hit(new_at)
|
||||
elif not new_target and last_target:
|
||||
# Pointer exited last_target
|
||||
XRToolsPointerEvent.exited(self, last_target, last_collided_at)
|
||||
|
||||
if click_on_hold:
|
||||
_set_time_held(0.0)
|
||||
gaze_pressed = false
|
||||
|
||||
# Update visible artifacts for miss
|
||||
_visible_miss()
|
||||
elif new_target != last_target:
|
||||
# Pointer exited last_target
|
||||
XRToolsPointerEvent.exited(self, last_target, last_collided_at)
|
||||
|
||||
if click_on_hold:
|
||||
_set_time_held(0.0)
|
||||
gaze_pressed = false
|
||||
|
||||
# Pointer entered new_target
|
||||
XRToolsPointerEvent.entered(self, new_target, new_at)
|
||||
|
||||
# Pointer moved on new_target
|
||||
XRToolsPointerEvent.moved(self, new_target, new_at, new_at)
|
||||
|
||||
if click_on_hold and !gaze_pressed:
|
||||
_set_time_held(time_held + delta)
|
||||
if time_held > hold_time:
|
||||
_button_pressed()
|
||||
|
||||
# Move visible artifacts
|
||||
_visible_move(new_at)
|
||||
elif new_at != last_collided_at:
|
||||
# Pointer moved on new_target
|
||||
XRToolsPointerEvent.moved(self, new_target, new_at, last_collided_at)
|
||||
|
||||
if click_on_hold and !gaze_pressed:
|
||||
_set_time_held(time_held + delta)
|
||||
if time_held > hold_time:
|
||||
_button_pressed()
|
||||
|
||||
# Move visible artifacts
|
||||
_visible_move(new_at)
|
||||
|
||||
# Update last values
|
||||
last_target = new_target
|
||||
last_collided_at = new_at
|
||||
|
||||
|
||||
# Set pointer enabled property
|
||||
func set_enabled(p_enabled : bool) -> void:
|
||||
enabled = p_enabled
|
||||
if is_inside_tree():
|
||||
_update_pointer()
|
||||
|
||||
|
||||
# Set pointer y_offset property
|
||||
func set_y_offset(p_offset : float) -> void:
|
||||
y_offset = p_offset
|
||||
if is_inside_tree():
|
||||
_update_y_offset()
|
||||
|
||||
|
||||
# Set pointer distance property
|
||||
func set_distance(p_new_value : float) -> void:
|
||||
distance = p_new_value
|
||||
if is_inside_tree():
|
||||
_update_distance()
|
||||
|
||||
|
||||
# Set pointer show_laser property
|
||||
func set_show_laser(p_show : LaserShow) -> void:
|
||||
show_laser = p_show
|
||||
if is_inside_tree():
|
||||
_update_pointer()
|
||||
|
||||
|
||||
# Set pointer laser_length property
|
||||
func set_laser_length(p_laser_length : LaserLength) -> void:
|
||||
laser_length = p_laser_length
|
||||
if is_inside_tree():
|
||||
_update_pointer()
|
||||
|
||||
|
||||
# Set pointer laser_material property
|
||||
func set_laser_material(p_laser_material : StandardMaterial3D) -> void:
|
||||
laser_material = p_laser_material
|
||||
if is_inside_tree():
|
||||
_update_pointer()
|
||||
|
||||
|
||||
# Set pointer laser_hit_material property
|
||||
func set_laser_hit_material(p_laser_hit_material : StandardMaterial3D) -> void:
|
||||
laser_hit_material = p_laser_hit_material
|
||||
if is_inside_tree():
|
||||
_update_pointer()
|
||||
|
||||
|
||||
# Set pointer show_target property
|
||||
func set_show_target(p_show_target : bool) -> void:
|
||||
show_target = p_show_target
|
||||
if is_inside_tree():
|
||||
$Target.visible = enabled and show_target and last_target
|
||||
|
||||
|
||||
# Set pointer target_radius property
|
||||
func set_target_radius(p_target_radius : float) -> void:
|
||||
target_radius = p_target_radius
|
||||
if is_inside_tree():
|
||||
_update_target_radius()
|
||||
|
||||
|
||||
# Set pointer target_material property
|
||||
func set_target_material(p_target_material : StandardMaterial3D) -> void:
|
||||
target_material = p_target_material
|
||||
if is_inside_tree():
|
||||
_update_target_material()
|
||||
|
||||
|
||||
# Set pointer collision_mask property
|
||||
func set_collision_mask(p_new_mask : int) -> void:
|
||||
collision_mask = p_new_mask
|
||||
if is_inside_tree():
|
||||
_update_collision_mask()
|
||||
|
||||
|
||||
# Set pointer collide_with_bodies property
|
||||
func set_collide_with_bodies(p_new_value : bool) -> void:
|
||||
collide_with_bodies = p_new_value
|
||||
if is_inside_tree():
|
||||
_update_collide_with_bodies()
|
||||
|
||||
|
||||
# Set pointer collide_with_areas property
|
||||
func set_collide_with_areas(p_new_value : bool) -> void:
|
||||
collide_with_areas = p_new_value
|
||||
if is_inside_tree():
|
||||
_update_collide_with_areas()
|
||||
|
||||
|
||||
# Set suppress radius property
|
||||
func set_suppress_radius(p_suppress_radius : float) -> void:
|
||||
suppress_radius = p_suppress_radius
|
||||
if is_inside_tree():
|
||||
_update_suppress_radius()
|
||||
|
||||
|
||||
func set_suppress_mask(p_suppress_mask : int) -> void:
|
||||
suppress_mask = p_suppress_mask
|
||||
if is_inside_tree():
|
||||
_update_suppress_mask()
|
||||
|
||||
|
||||
# Pointer Y offset update handler
|
||||
func _update_y_offset() -> void:
|
||||
$Laser.position.y = y_offset * _world_scale
|
||||
$RayCast.position.y = y_offset * _world_scale
|
||||
|
||||
|
||||
# Pointer distance update handler
|
||||
func _update_distance() -> void:
|
||||
$RayCast.target_position.z = -distance
|
||||
_update_pointer()
|
||||
|
||||
|
||||
# Pointer target radius update handler
|
||||
func _update_target_radius() -> void:
|
||||
$Target.mesh.radius = target_radius
|
||||
$Target.mesh.height = target_radius * 2
|
||||
|
||||
|
||||
# Pointer target_material update handler
|
||||
func _update_target_material() -> void:
|
||||
$Target.set_surface_override_material(0, target_material)
|
||||
|
||||
|
||||
# Pointer collision_mask update handler
|
||||
func _update_collision_mask() -> void:
|
||||
$RayCast.collision_mask = collision_mask
|
||||
|
||||
|
||||
# Pointer collide_with_bodies update handler
|
||||
func _update_collide_with_bodies() -> void:
|
||||
$RayCast.collide_with_bodies = collide_with_bodies
|
||||
|
||||
|
||||
# Pointer collide_with_areas update handler
|
||||
func _update_collide_with_areas() -> void:
|
||||
$RayCast.collide_with_areas = collide_with_areas
|
||||
|
||||
|
||||
# Pointer suppress_radius update handler
|
||||
func _update_suppress_radius() -> void:
|
||||
$SuppressArea/CollisionShape3D.shape.radius = suppress_radius
|
||||
|
||||
|
||||
# Pointer suppress_mask update handler
|
||||
func _update_suppress_mask() -> void:
|
||||
$SuppressArea.collision_mask = suppress_mask
|
||||
|
||||
|
||||
# Pointer visible artifacts update handler
|
||||
func _update_pointer() -> void:
|
||||
if enabled and last_target:
|
||||
_visible_hit(last_collided_at)
|
||||
else:
|
||||
_visible_miss()
|
||||
|
||||
|
||||
# Pointer-activation button pressed handler
|
||||
func _button_pressed() -> void:
|
||||
if $RayCast.is_colliding():
|
||||
# Report pressed
|
||||
target = $RayCast.get_collider()
|
||||
last_collided_at = $RayCast.get_collision_point()
|
||||
XRToolsPointerEvent.pressed(self, target, last_collided_at)
|
||||
if click_on_hold:
|
||||
_set_time_held(0.0)
|
||||
gaze_pressed = true
|
||||
XRToolsPointerEvent.released(self, last_target, last_collided_at)
|
||||
target = null
|
||||
_set_time_held(0.0)
|
||||
|
||||
# Pointer-activation button released handler
|
||||
func _button_released() -> void:
|
||||
if target:
|
||||
# Report release
|
||||
XRToolsPointerEvent.released(self, target, last_collided_at)
|
||||
target = null
|
||||
last_collided_at = Vector3(0, 0, 0)
|
||||
|
||||
|
||||
# Update the laser active material
|
||||
func _update_laser_active_material(hit : bool) -> void:
|
||||
if hit and laser_hit_material:
|
||||
$Laser.set_surface_override_material(0, laser_hit_material)
|
||||
else:
|
||||
$Laser.set_surface_override_material(0, laser_material)
|
||||
|
||||
|
||||
# Update the visible artifacts to show a hit
|
||||
func _visible_hit(at : Vector3) -> void:
|
||||
# Show target if enabled
|
||||
if show_target:
|
||||
$Target.global_transform.origin = at
|
||||
$Target.visible = true
|
||||
|
||||
# Control laser visibility
|
||||
if show_laser != LaserShow.HIDE:
|
||||
# Ensure the correct laser material is set
|
||||
_update_laser_active_material(true)
|
||||
|
||||
# Adjust laser length
|
||||
if laser_length == LaserLength.COLLIDE:
|
||||
var collide_len : float = at.distance_to(global_transform.origin)
|
||||
$Laser.mesh.size.z = collide_len
|
||||
$Laser.position.z = collide_len * -0.5
|
||||
else:
|
||||
$Laser.mesh.size.z = distance
|
||||
$Laser.position.z = distance * -0.5
|
||||
|
||||
# Show laser
|
||||
$Laser.visible = true
|
||||
else:
|
||||
# Ensure laser is hidden
|
||||
$Laser.visible = false
|
||||
|
||||
|
||||
# Move the visible pointer artifacts to the target
|
||||
func _visible_move(at : Vector3) -> void:
|
||||
# Move target if configured
|
||||
if show_target:
|
||||
$Target.global_transform.origin = at
|
||||
|
||||
# Adjust laser length if set to collide-length
|
||||
if laser_length == LaserLength.COLLIDE:
|
||||
var collide_len : float = at.distance_to(global_transform.origin)
|
||||
$Laser.mesh.size.z = collide_len
|
||||
$Laser.position.z = collide_len * -0.5
|
||||
|
||||
$Visualise.global_transform.origin = at
|
||||
|
||||
|
||||
# Update the visible artifacts to show a miss
|
||||
func _visible_miss() -> void:
|
||||
# Ensure target is hidden
|
||||
$Target.visible = false
|
||||
|
||||
# Ensure the correct laser material is set
|
||||
_update_laser_active_material(false)
|
||||
|
||||
# Hide laser if not set to show always
|
||||
$Laser.visible = show_laser == LaserShow.SHOW
|
||||
|
||||
# Restore laser length if set to collide-length
|
||||
$Laser.mesh.size.z = distance
|
||||
$Laser.position.z = distance * -0.5
|
||||
|
||||
#set gaze pointer visualization
|
||||
func _set_time_held(p_time_held):
|
||||
time_held = p_time_held
|
||||
if material:
|
||||
$Visualise.visible = time_held > 0.0
|
||||
material.set_shader_parameter("value", time_held/hold_time)
|
||||
|
||||
# set gaze pointer size
|
||||
func set_size(p_size: Vector2):
|
||||
size = p_size
|
||||
_update_size()
|
||||
|
||||
# update gaze pointer size
|
||||
func _update_size():
|
||||
if material: # Note, material won't be set until after we setup our scene
|
||||
var mesh : QuadMesh = $Visualise.mesh
|
||||
if mesh.size != size:
|
||||
mesh.size = size
|
||||
|
||||
# updating the size will unset our material, so reset it
|
||||
$Visualise.set_surface_override_material(0, material)
|
||||
|
||||
#set gaze_pointer_color
|
||||
func set_color(p_color: Color):
|
||||
color = p_color
|
||||
_update_color()
|
||||
|
||||
#update gaze pointer color
|
||||
func _update_color():
|
||||
if material:
|
||||
material.set_shader_parameter("albedo", color)
|
||||
@@ -0,0 +1 @@
|
||||
uid://f60byrk7xjak
|
||||
66
addons/godot-xr-tools/functions/function_gaze_pointer.tscn
Normal file
66
addons/godot-xr-tools/functions/function_gaze_pointer.tscn
Normal file
@@ -0,0 +1,66 @@
|
||||
[gd_scene load_steps=9 format=3 uid="uid://do1wif8rpqtwj"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://f60byrk7xjak" path="res://addons/godot-xr-tools/functions/function_gaze_pointer.gd" id="1_ipkdr"]
|
||||
[ext_resource type="Material" path="res://addons/godot-xr-tools/materials/pointer.tres" id="2_ndm62"]
|
||||
[ext_resource type="Shader" uid="uid://dncfip67nl2sf" path="res://addons/godot-xr-tools/misc/hold_button_gaze_pointer_visualshader.tres" id="3_1p5pd"]
|
||||
|
||||
[sub_resource type="BoxMesh" id="1"]
|
||||
resource_local_to_scene = true
|
||||
material = ExtResource("2_ndm62")
|
||||
size = Vector3(0.002, 0.002, 10)
|
||||
subdivide_depth = 20
|
||||
|
||||
[sub_resource type="SphereMesh" id="2"]
|
||||
material = ExtResource("2_ndm62")
|
||||
radius = 0.05
|
||||
height = 0.1
|
||||
radial_segments = 16
|
||||
rings = 8
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_k3gfm"]
|
||||
radius = 0.2
|
||||
|
||||
[sub_resource type="QuadMesh" id="QuadMesh_lulcv"]
|
||||
resource_local_to_scene = true
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_ico2c"]
|
||||
render_priority = -100
|
||||
shader = ExtResource("3_1p5pd")
|
||||
shader_parameter/albedo = Color(1, 1, 1, 1)
|
||||
shader_parameter/value = 0.2
|
||||
shader_parameter/fade = 0.05
|
||||
shader_parameter/radius = 0.8
|
||||
shader_parameter/width = 0.2
|
||||
|
||||
[node name="FunctionGazePointer" type="Node3D"]
|
||||
script = ExtResource("1_ipkdr")
|
||||
show_laser = 0
|
||||
show_target = true
|
||||
|
||||
[node name="RayCast" type="RayCast3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.013, 0)
|
||||
target_position = Vector3(0, 0, -10)
|
||||
collision_mask = 5242880
|
||||
|
||||
[node name="Laser" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.013, -5)
|
||||
visible = false
|
||||
cast_shadow = 0
|
||||
mesh = SubResource("1")
|
||||
|
||||
[node name="Target" type="MeshInstance3D" parent="."]
|
||||
visible = false
|
||||
mesh = SubResource("2")
|
||||
|
||||
[node name="SuppressArea" type="Area3D" parent="."]
|
||||
collision_layer = 0
|
||||
collision_mask = 4194304
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="SuppressArea"]
|
||||
shape = SubResource("SphereShape3D_k3gfm")
|
||||
|
||||
[node name="Visualise" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -1)
|
||||
cast_shadow = 0
|
||||
mesh = SubResource("QuadMesh_lulcv")
|
||||
surface_material_override/0 = SubResource("ShaderMaterial_ico2c")
|
||||
521
addons/godot-xr-tools/functions/function_pickup.gd
Normal file
521
addons/godot-xr-tools/functions/function_pickup.gd
Normal file
@@ -0,0 +1,521 @@
|
||||
@tool
|
||||
@icon("res://addons/godot-xr-tools/editor/icons/function.svg")
|
||||
class_name XRToolsFunctionPickup
|
||||
extends XRToolsHandPalmOffset
|
||||
|
||||
|
||||
## XR Tools Function Pickup Script
|
||||
##
|
||||
## This script implements picking up of objects. Most pickable
|
||||
## objects are instances of the [XRToolsPickable] class.
|
||||
##
|
||||
## Additionally this script can work in conjunction with the
|
||||
## [XRToolsMovementProvider] class support climbing. Most climbable objects are
|
||||
## instances of the [XRToolsClimbable] class.
|
||||
|
||||
|
||||
## Signal emitted when the pickup picks something up
|
||||
signal has_picked_up(what)
|
||||
|
||||
## Signal emitted when the pickup drops something
|
||||
signal has_dropped
|
||||
|
||||
|
||||
# Default pickup collision mask of 3:pickable and 19:handle
|
||||
const DEFAULT_GRAB_MASK := 0b0000_0000_0000_0100_0000_0000_0000_0100
|
||||
|
||||
# Default pickup collision mask of 3:pickable
|
||||
const DEFAULT_RANGE_MASK := 0b0000_0000_0000_0000_0000_0000_0000_0100
|
||||
|
||||
# Constant for worst-case grab distance
|
||||
const MAX_GRAB_DISTANCE2: float = 1000000.0
|
||||
|
||||
# Class for storing copied collision data
|
||||
class CopiedCollision extends RefCounted:
|
||||
var collision_shape : CollisionShape3D
|
||||
var org_transform : Transform3D
|
||||
|
||||
## Pickup enabled property
|
||||
@export var enabled : bool = true
|
||||
|
||||
## Grip controller axis
|
||||
@export var pickup_axis_action : String = "grip"
|
||||
|
||||
## Action controller button
|
||||
@export var action_button_action : String = "trigger_click"
|
||||
|
||||
## Grab distance
|
||||
@export var grab_distance : float = 0.3: set = _set_grab_distance
|
||||
|
||||
## Grab collision mask
|
||||
@export_flags_3d_physics \
|
||||
var grab_collision_mask : int = DEFAULT_GRAB_MASK: set = _set_grab_collision_mask
|
||||
|
||||
## If true, ranged-grabbing is enabled
|
||||
@export var ranged_enable : bool = true
|
||||
|
||||
## Ranged-grab distance
|
||||
@export var ranged_distance : float = 5.0: set = _set_ranged_distance
|
||||
|
||||
## Ranged-grab angle
|
||||
@export_range(0.0, 45.0) var ranged_angle : float = 5.0: set = _set_ranged_angle
|
||||
|
||||
## Ranged-grab collision mask
|
||||
@export_flags_3d_physics \
|
||||
var ranged_collision_mask : int = DEFAULT_RANGE_MASK: set = _set_ranged_collision_mask
|
||||
|
||||
## Throw impulse factor
|
||||
@export var impulse_factor : float = 1.0
|
||||
|
||||
## Throw velocity averaging
|
||||
@export var velocity_samples: int = 5
|
||||
|
||||
|
||||
# Public fields
|
||||
var closest_object : Node3D = null
|
||||
var picked_up_object : Node3D = null
|
||||
var picked_up_ranged : bool = false
|
||||
var grip_pressed : bool = false
|
||||
|
||||
# Private fields
|
||||
var _object_in_grab_area := Array()
|
||||
var _object_in_ranged_area := Array()
|
||||
var _velocity_averager := XRToolsVelocityAverager.new(velocity_samples)
|
||||
var _grab_area : Area3D
|
||||
var _grab_collision : CollisionShape3D
|
||||
var _ranged_area : Area3D
|
||||
var _ranged_collision : CollisionShape3D
|
||||
var _active_copied_collisions : Array[CopiedCollision]
|
||||
|
||||
## Collision hand (if applicable)
|
||||
@onready var _collision_hand : XRToolsCollisionHand
|
||||
|
||||
## Grip threshold (from configuration)
|
||||
@onready var _grip_threshold : float = XRTools.get_grip_threshold()
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsFunctionPickup"
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
# Skip creating grab-helpers if in the editor
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# Create the grab collision shape
|
||||
_grab_collision = CollisionShape3D.new()
|
||||
_grab_collision.set_name("GrabCollisionShape")
|
||||
_grab_collision.shape = SphereShape3D.new()
|
||||
_grab_collision.shape.radius = grab_distance
|
||||
|
||||
# Create the grab area
|
||||
_grab_area = Area3D.new()
|
||||
_grab_area.set_name("GrabArea")
|
||||
_grab_area.collision_layer = 0
|
||||
_grab_area.collision_mask = grab_collision_mask
|
||||
_grab_area.add_child(_grab_collision)
|
||||
_grab_area.area_entered.connect(_on_grab_entered)
|
||||
_grab_area.body_entered.connect(_on_grab_entered)
|
||||
_grab_area.area_exited.connect(_on_grab_exited)
|
||||
_grab_area.body_exited.connect(_on_grab_exited)
|
||||
add_child(_grab_area)
|
||||
|
||||
# Create the ranged collision shape
|
||||
_ranged_collision = CollisionShape3D.new()
|
||||
_ranged_collision.set_name("RangedCollisionShape")
|
||||
_ranged_collision.shape = CylinderShape3D.new()
|
||||
_ranged_collision.transform.basis = Basis(Vector3.RIGHT, PI/2)
|
||||
|
||||
# Create the ranged area
|
||||
_ranged_area = Area3D.new()
|
||||
_ranged_area.set_name("RangedArea")
|
||||
_ranged_area.collision_layer = 0
|
||||
_ranged_area.collision_mask = ranged_collision_mask
|
||||
_ranged_area.add_child(_ranged_collision)
|
||||
_ranged_area.area_entered.connect(_on_ranged_entered)
|
||||
_ranged_area.body_entered.connect(_on_ranged_entered)
|
||||
_ranged_area.area_exited.connect(_on_ranged_exited)
|
||||
_ranged_area.body_exited.connect(_on_ranged_exited)
|
||||
add_child(_ranged_area)
|
||||
|
||||
# Update the colliders
|
||||
_update_colliders()
|
||||
|
||||
|
||||
# Called when we're added to the tree
|
||||
func _enter_tree():
|
||||
super._enter_tree()
|
||||
|
||||
_collision_hand = XRToolsCollisionHand.find_ancestor(self)
|
||||
|
||||
# Monitor Grab Button
|
||||
if _controller:
|
||||
_controller.button_pressed.connect(_on_button_pressed)
|
||||
_controller.button_released.connect(_on_button_released)
|
||||
|
||||
# Called when we exit the tree
|
||||
func _exit_tree():
|
||||
if _controller:
|
||||
_controller.button_pressed.disconnect(_on_button_pressed)
|
||||
_controller.button_released.disconnect(_on_button_released)
|
||||
|
||||
if _collision_hand:
|
||||
_remove_copied_collisions()
|
||||
_collision_hand = null
|
||||
|
||||
super._exit_tree()
|
||||
|
||||
|
||||
# Called on each frame to update the pickup
|
||||
func _process(delta):
|
||||
super._process(delta)
|
||||
|
||||
# Do not process if in the editor
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# Skip if disabled, or the controller isn't active
|
||||
if !enabled or !_controller.get_is_active():
|
||||
return
|
||||
|
||||
# Handle our grip
|
||||
var grip_value = _controller.get_float(pickup_axis_action)
|
||||
if (grip_pressed and grip_value < (_grip_threshold - 0.1)):
|
||||
grip_pressed = false
|
||||
_on_grip_release()
|
||||
elif (!grip_pressed and grip_value > (_grip_threshold + 0.1)):
|
||||
grip_pressed = true
|
||||
_on_grip_pressed()
|
||||
|
||||
# Calculate average velocity
|
||||
if is_instance_valid(picked_up_object) and picked_up_object.is_picked_up():
|
||||
# Average velocity of picked up object
|
||||
_velocity_averager.add_transform(delta, picked_up_object.global_transform)
|
||||
else:
|
||||
# Average velocity of this pickup
|
||||
_velocity_averager.add_transform(delta, global_transform)
|
||||
|
||||
_update_copied_collisions()
|
||||
_update_closest_object()
|
||||
|
||||
|
||||
## Find an [XRToolsFunctionPickup] node.
|
||||
##
|
||||
## This function searches from the specified node for an [XRToolsFunctionPickup]
|
||||
## assuming the node is a sibling of the pickup under an [XRController3D].
|
||||
static func find_instance(node : Node) -> XRToolsFunctionPickup:
|
||||
return XRTools.find_xr_child(
|
||||
XRHelpers.get_xr_controller(node),
|
||||
"*",
|
||||
"XRToolsFunctionPickup") as XRToolsFunctionPickup
|
||||
|
||||
|
||||
## Find the left [XRToolsFunctionPickup] node.
|
||||
##
|
||||
## This function searches from the specified node for the left controller
|
||||
## [XRToolsFunctionPickup] assuming the node is a sibling of the [XOrigin3D].
|
||||
static func find_left(node : Node) -> XRToolsFunctionPickup:
|
||||
return XRTools.find_xr_child(
|
||||
XRHelpers.get_left_controller(node),
|
||||
"*",
|
||||
"XRToolsFunctionPickup") as XRToolsFunctionPickup
|
||||
|
||||
|
||||
## Find the right [XRToolsFunctionPickup] node.
|
||||
##
|
||||
## This function searches from the specified node for the right controller
|
||||
## [XRToolsFunctionPickup] assuming the node is a sibling of the [XROrigin3D].
|
||||
static func find_right(node : Node) -> XRToolsFunctionPickup:
|
||||
return XRTools.find_xr_child(
|
||||
XRHelpers.get_right_controller(node),
|
||||
"*",
|
||||
"XRToolsFunctionPickup") as XRToolsFunctionPickup
|
||||
|
||||
|
||||
## Get the [XRController3D] driving this pickup.
|
||||
func get_controller() -> XRController3D:
|
||||
return _controller
|
||||
|
||||
|
||||
# Called when the grab distance has been modified
|
||||
func _set_grab_distance(new_value: float) -> void:
|
||||
grab_distance = new_value
|
||||
if is_inside_tree():
|
||||
_update_colliders()
|
||||
|
||||
|
||||
# Called when the grab collision mask has been modified
|
||||
func _set_grab_collision_mask(new_value: int) -> void:
|
||||
grab_collision_mask = new_value
|
||||
if is_inside_tree() and _grab_area:
|
||||
_grab_area.collision_mask = new_value
|
||||
|
||||
|
||||
# Called when the ranged-grab distance has been modified
|
||||
func _set_ranged_distance(new_value: float) -> void:
|
||||
ranged_distance = new_value
|
||||
if is_inside_tree():
|
||||
_update_colliders()
|
||||
|
||||
|
||||
# Called when the ranged-grab angle has been modified
|
||||
func _set_ranged_angle(new_value: float) -> void:
|
||||
ranged_angle = new_value
|
||||
if is_inside_tree():
|
||||
_update_colliders()
|
||||
|
||||
|
||||
# Called when the ranged-grab collision mask has been modified
|
||||
func _set_ranged_collision_mask(new_value: int) -> void:
|
||||
ranged_collision_mask = new_value
|
||||
if is_inside_tree() and _ranged_area:
|
||||
_ranged_area.collision_mask = new_value
|
||||
|
||||
|
||||
# Update the colliders geometry
|
||||
func _update_colliders() -> void:
|
||||
# Update the grab sphere
|
||||
if _grab_collision:
|
||||
_grab_collision.shape.radius = grab_distance
|
||||
|
||||
# Update the ranged-grab cylinder
|
||||
if _ranged_collision:
|
||||
_ranged_collision.shape.radius = tan(deg_to_rad(ranged_angle)) * ranged_distance
|
||||
_ranged_collision.shape.height = ranged_distance
|
||||
_ranged_collision.transform.origin.z = -ranged_distance * 0.5
|
||||
|
||||
|
||||
# Called when an object enters the grab sphere
|
||||
func _on_grab_entered(target: Node3D) -> void:
|
||||
# reject objects which don't support picking up
|
||||
if not target.has_method('pick_up'):
|
||||
return
|
||||
|
||||
# ignore objects already known
|
||||
if _object_in_grab_area.find(target) >= 0:
|
||||
return
|
||||
|
||||
# Add to the list of objects in grab area
|
||||
_object_in_grab_area.push_back(target)
|
||||
|
||||
|
||||
# Called when an object enters the ranged-grab cylinder
|
||||
func _on_ranged_entered(target: Node3D) -> void:
|
||||
# reject objects which don't support picking up rangedly
|
||||
if not 'can_ranged_grab' in target or not target.can_ranged_grab:
|
||||
return
|
||||
|
||||
# ignore objects already known
|
||||
if _object_in_ranged_area.find(target) >= 0:
|
||||
return
|
||||
|
||||
# Add to the list of objects in grab area
|
||||
_object_in_ranged_area.push_back(target)
|
||||
|
||||
|
||||
# Called when an object exits the grab sphere
|
||||
func _on_grab_exited(target: Node3D) -> void:
|
||||
_object_in_grab_area.erase(target)
|
||||
|
||||
|
||||
# Called when an object exits the ranged-grab cylinder
|
||||
func _on_ranged_exited(target: Node3D) -> void:
|
||||
_object_in_ranged_area.erase(target)
|
||||
|
||||
|
||||
# Update the closest object field with the best choice of grab
|
||||
func _update_closest_object() -> void:
|
||||
# Find the closest object we can pickup
|
||||
var new_closest_obj: Node3D = null
|
||||
if not picked_up_object:
|
||||
# Find the closest in grab area
|
||||
new_closest_obj = _get_closest_grab()
|
||||
if not new_closest_obj and ranged_enable:
|
||||
# Find closest in ranged area
|
||||
new_closest_obj = _get_closest_ranged()
|
||||
|
||||
# Skip if no change
|
||||
if closest_object == new_closest_obj:
|
||||
return
|
||||
|
||||
# remove highlight on old object
|
||||
if is_instance_valid(closest_object):
|
||||
closest_object.request_highlight(self, false)
|
||||
|
||||
# add highlight to new object
|
||||
closest_object = new_closest_obj
|
||||
if is_instance_valid(closest_object):
|
||||
closest_object.request_highlight(self, true)
|
||||
|
||||
|
||||
# Find the pickable object closest to our hand's grab location
|
||||
func _get_closest_grab() -> Node3D:
|
||||
var new_closest_obj: Node3D = null
|
||||
var new_closest_distance := MAX_GRAB_DISTANCE2
|
||||
for o in _object_in_grab_area:
|
||||
# skip objects that can not be picked up
|
||||
if not o.can_pick_up(self):
|
||||
continue
|
||||
|
||||
# Save if this object is closer than the current best
|
||||
var distance_squared := global_transform.origin.distance_squared_to(
|
||||
o.global_transform.origin)
|
||||
if distance_squared < new_closest_distance:
|
||||
new_closest_obj = o
|
||||
new_closest_distance = distance_squared
|
||||
|
||||
# Return best object
|
||||
return new_closest_obj
|
||||
|
||||
|
||||
# Find the rangedly-pickable object closest to our hand's pointing direction
|
||||
func _get_closest_ranged() -> Node3D:
|
||||
var new_closest_obj: Node3D = null
|
||||
var new_closest_angle_dp := cos(deg_to_rad(ranged_angle))
|
||||
var hand_forwards := -global_transform.basis.z
|
||||
for o in _object_in_ranged_area:
|
||||
# skip objects that can not be picked up
|
||||
if not o.can_pick_up(self):
|
||||
continue
|
||||
|
||||
# Save if this object is closer than the current best
|
||||
var object_direction: Vector3 = o.global_transform.origin - global_transform.origin
|
||||
object_direction = object_direction.normalized()
|
||||
var angle_dp := hand_forwards.dot(object_direction)
|
||||
if angle_dp > new_closest_angle_dp:
|
||||
new_closest_obj = o
|
||||
new_closest_angle_dp = angle_dp
|
||||
|
||||
# Return best object
|
||||
return new_closest_obj
|
||||
|
||||
|
||||
## Drop the currently held object
|
||||
func drop_object() -> void:
|
||||
if not is_instance_valid(picked_up_object):
|
||||
return
|
||||
|
||||
# Remove any copied collision objects
|
||||
_remove_copied_collisions()
|
||||
|
||||
# let go of this object
|
||||
picked_up_object.let_go(
|
||||
self,
|
||||
_velocity_averager.linear_velocity() * impulse_factor,
|
||||
_velocity_averager.angular_velocity())
|
||||
picked_up_object = null
|
||||
|
||||
if _collision_hand:
|
||||
# Reset the held weight
|
||||
_collision_hand.set_held_weight(0.0)
|
||||
|
||||
emit_signal("has_dropped")
|
||||
|
||||
|
||||
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
|
||||
|
||||
# Handle snap-zone
|
||||
var snap := target as XRToolsSnapZone
|
||||
if snap:
|
||||
target = snap.picked_up_object
|
||||
snap.drop_object()
|
||||
|
||||
# Pick up our target. Note, target may do instant drop_and_free
|
||||
picked_up_ranged = not _object_in_grab_area.has(target)
|
||||
picked_up_object = target
|
||||
target.pick_up(self)
|
||||
|
||||
# If object picked up then emit signal
|
||||
if is_instance_valid(picked_up_object):
|
||||
_copy_collisions()
|
||||
|
||||
picked_up_object.request_highlight(self, false)
|
||||
emit_signal("has_picked_up", picked_up_object)
|
||||
|
||||
|
||||
# Copy collision shapes on the held object to our collision hand (if applicable).
|
||||
# If we're two handing an object, both collision hands will get copies.
|
||||
func _copy_collisions():
|
||||
if not is_instance_valid(_collision_hand):
|
||||
return
|
||||
|
||||
if not is_instance_valid(picked_up_object) or not picked_up_object is RigidBody3D:
|
||||
return
|
||||
|
||||
for child in picked_up_object.get_children():
|
||||
if child is CollisionShape3D and not child.disabled:
|
||||
|
||||
var copied_collision : CopiedCollision = CopiedCollision.new()
|
||||
copied_collision.collision_shape = CollisionShape3D.new()
|
||||
copied_collision.collision_shape.shape = child.shape
|
||||
copied_collision.org_transform = child.transform
|
||||
|
||||
_collision_hand.add_child(copied_collision.collision_shape, false, Node.INTERNAL_MODE_BACK)
|
||||
copied_collision.collision_shape.global_transform = picked_up_object.global_transform * \
|
||||
copied_collision.org_transform
|
||||
|
||||
_active_copied_collisions.push_back(copied_collision)
|
||||
|
||||
|
||||
# Adjust positions of our collisions to match actual location of object
|
||||
func _update_copied_collisions():
|
||||
if is_instance_valid(_collision_hand) and is_instance_valid(picked_up_object):
|
||||
for copied_collision : CopiedCollision in _active_copied_collisions:
|
||||
if is_instance_valid(copied_collision.collision_shape):
|
||||
copied_collision.collision_shape.global_transform = picked_up_object.global_transform * \
|
||||
copied_collision.org_transform
|
||||
|
||||
|
||||
# Remove copied collision shapes
|
||||
func _remove_copied_collisions():
|
||||
if is_instance_valid(_collision_hand):
|
||||
for copied_collision : CopiedCollision in _active_copied_collisions:
|
||||
if is_instance_valid(copied_collision.collision_shape):
|
||||
_collision_hand.remove_child(copied_collision.collision_shape)
|
||||
copied_collision.collision_shape.queue_free()
|
||||
|
||||
_active_copied_collisions.clear()
|
||||
|
||||
|
||||
func _on_button_pressed(p_button) -> void:
|
||||
if p_button == action_button_action and is_instance_valid(picked_up_object):
|
||||
if picked_up_object.has_method("action"):
|
||||
picked_up_object.action()
|
||||
|
||||
if picked_up_object.has_method("controller_action"):
|
||||
picked_up_object.controller_action(_controller)
|
||||
|
||||
|
||||
func _on_button_released(p_button) -> void:
|
||||
if p_button == action_button_action and is_instance_valid(picked_up_object):
|
||||
if picked_up_object.has_method("action_release"):
|
||||
picked_up_object.action_release()
|
||||
|
||||
if picked_up_object.has_method("controller_action_release"):
|
||||
picked_up_object.controller_action_release(_controller)
|
||||
|
||||
|
||||
func _on_grip_pressed() -> void:
|
||||
if is_instance_valid(picked_up_object) and !picked_up_object.press_to_hold:
|
||||
drop_object()
|
||||
elif is_instance_valid(closest_object):
|
||||
_pick_up_object(closest_object)
|
||||
|
||||
|
||||
func _on_grip_release() -> void:
|
||||
if is_instance_valid(picked_up_object) and picked_up_object.press_to_hold:
|
||||
drop_object()
|
||||
1
addons/godot-xr-tools/functions/function_pickup.gd.uid
Normal file
1
addons/godot-xr-tools/functions/function_pickup.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b18kkstg643yw
|
||||
7
addons/godot-xr-tools/functions/function_pickup.tscn
Normal file
7
addons/godot-xr-tools/functions/function_pickup.tscn
Normal file
@@ -0,0 +1,7 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://b4ysuy43poobf"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b18kkstg643yw" path="res://addons/godot-xr-tools/functions/function_pickup.gd" id="1"]
|
||||
|
||||
[node name="FunctionPickup" type="Node3D"]
|
||||
script = ExtResource("1")
|
||||
grab_collision_mask = 327684
|
||||
542
addons/godot-xr-tools/functions/function_pointer.gd
Normal file
542
addons/godot-xr-tools/functions/function_pointer.gd
Normal file
@@ -0,0 +1,542 @@
|
||||
@tool
|
||||
@icon("res://addons/godot-xr-tools/editor/icons/function.svg")
|
||||
class_name XRToolsFunctionPointer
|
||||
extends XRToolsHandAimOffset
|
||||
|
||||
|
||||
## XR Tools Function Pointer Script
|
||||
##
|
||||
## This script implements a pointer function for a players controller. Pointer
|
||||
## events (entered, exited, pressed, release, and movement) are delivered by
|
||||
## invoking signals on the target node.
|
||||
##
|
||||
## Pointer target nodes commonly extend from [XRToolsInteractableArea] or
|
||||
## [XRToolsInteractableBody].
|
||||
|
||||
|
||||
## Signal emitted when this object points at another object
|
||||
signal pointing_event(event)
|
||||
|
||||
|
||||
## Enumeration of laser show modes
|
||||
enum LaserShow {
|
||||
HIDE = 0, ## Hide laser
|
||||
SHOW = 1, ## Show laser
|
||||
COLLIDE = 2, ## Only show laser on collision
|
||||
}
|
||||
|
||||
## Enumeration of laser length modes
|
||||
enum LaserLength {
|
||||
FULL = 0, ## Full length
|
||||
COLLIDE = 1 ## Draw to collision
|
||||
}
|
||||
|
||||
|
||||
## Default pointer collision mask of 21:pointable and 23:ui-objects
|
||||
const DEFAULT_MASK := 0b0000_0000_0101_0000_0000_0000_0000_0000
|
||||
|
||||
## Default pointer collision mask of 23:ui-objects
|
||||
const SUPPRESS_MASK := 0b0000_0000_0100_0000_0000_0000_0000_0000
|
||||
|
||||
|
||||
@export_group("General")
|
||||
|
||||
## Pointer enabled
|
||||
@export var enabled : bool = true: set = set_enabled
|
||||
|
||||
## Y Offset for pointer
|
||||
@export var y_offset : float = -0.013: set = set_y_offset
|
||||
|
||||
## Pointer distance
|
||||
@export var distance : float = 10: set = set_distance
|
||||
|
||||
## Active button action
|
||||
@export var active_button_action : String = "trigger_click"
|
||||
|
||||
@export_group("Laser")
|
||||
|
||||
## Controls when the laser is visible
|
||||
@export var show_laser : LaserShow = LaserShow.SHOW: set = set_show_laser
|
||||
|
||||
## Controls the length of the laser
|
||||
@export var laser_length : LaserLength = LaserLength.FULL: set = set_laser_length
|
||||
|
||||
## Laser pointer material
|
||||
@export var laser_material : StandardMaterial3D = null : set = set_laser_material
|
||||
|
||||
## Laser pointer material when hitting target
|
||||
@export var laser_hit_material : StandardMaterial3D = null : set = set_laser_hit_material
|
||||
|
||||
@export_group("Target")
|
||||
|
||||
## If true, the pointer target is shown
|
||||
@export var show_target : bool = false: set = set_show_target
|
||||
|
||||
## Controls the target radius
|
||||
@export var target_radius : float = 0.05: set = set_target_radius
|
||||
|
||||
## Target material
|
||||
@export var target_material : StandardMaterial3D = null : set = set_target_material
|
||||
|
||||
@export_group("Collision")
|
||||
|
||||
## Pointer collision mask
|
||||
@export_flags_3d_physics var collision_mask : int = DEFAULT_MASK: set = set_collision_mask
|
||||
|
||||
## Enable pointer collision with bodies
|
||||
@export var collide_with_bodies : bool = true: set = set_collide_with_bodies
|
||||
|
||||
## Enable pointer collision with areas
|
||||
@export var collide_with_areas : bool = false: set = set_collide_with_areas
|
||||
|
||||
@export_group("Suppression")
|
||||
|
||||
## Suppress radius
|
||||
@export var suppress_radius : float = 0.2: set = set_suppress_radius
|
||||
|
||||
## Suppress mask
|
||||
@export_flags_3d_physics var suppress_mask : int = SUPPRESS_MASK: set = set_suppress_mask
|
||||
|
||||
|
||||
## Current target node
|
||||
var target : Node3D = null
|
||||
|
||||
## Last target node
|
||||
var last_target : Node3D = null
|
||||
|
||||
## Last collision point
|
||||
var last_collided_at : Vector3 = Vector3.ZERO
|
||||
|
||||
# World scale
|
||||
var _world_scale : float = 1.0
|
||||
|
||||
# Left controller node
|
||||
var _controller_left_node : XRController3D
|
||||
|
||||
# Right controller node
|
||||
var _controller_right_node : XRController3D
|
||||
|
||||
# The currently active controller
|
||||
var _active_controller : XRController3D
|
||||
|
||||
|
||||
## Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsFunctionPointer"
|
||||
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _enter_tree():
|
||||
super._enter_tree()
|
||||
|
||||
# Do not initialise if in the editor
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# Read the initial world-scale
|
||||
_world_scale = XRServer.world_scale
|
||||
|
||||
# Check for a parent controller
|
||||
if _controller:
|
||||
# Set as active on the parent controller
|
||||
_active_controller = _controller
|
||||
|
||||
# Get button press feedback from our parent controller
|
||||
_controller.button_pressed.connect(_on_button_pressed.bind(_controller))
|
||||
_controller.button_released.connect(_on_button_released.bind(_controller))
|
||||
else:
|
||||
# Disable this if we don't have a controller
|
||||
hand_offset_mode = 4
|
||||
|
||||
# Get the left and right controllers
|
||||
_controller_left_node = XRHelpers.get_left_controller(self)
|
||||
_controller_right_node = XRHelpers.get_right_controller(self)
|
||||
|
||||
# Start out right hand controller
|
||||
_active_controller = _controller_right_node
|
||||
|
||||
# Get button press feedback from both left and right controllers
|
||||
_controller_left_node.button_pressed.connect(
|
||||
_on_button_pressed.bind(_controller_left_node))
|
||||
_controller_left_node.button_released.connect(
|
||||
_on_button_released.bind(_controller_left_node))
|
||||
_controller_right_node.button_pressed.connect(
|
||||
_on_button_pressed.bind(_controller_right_node))
|
||||
_controller_right_node.button_released.connect(
|
||||
_on_button_released.bind(_controller_right_node))
|
||||
|
||||
# init our state
|
||||
_update_y_offset()
|
||||
_update_distance()
|
||||
_update_pointer()
|
||||
_update_target_radius()
|
||||
_update_target_material()
|
||||
_update_collision_mask()
|
||||
_update_collide_with_bodies()
|
||||
_update_collide_with_areas()
|
||||
_update_suppress_radius()
|
||||
_update_suppress_mask()
|
||||
|
||||
func _exit_tree():
|
||||
_active_controller = null
|
||||
|
||||
if _controller and not Engine.is_editor_hint():
|
||||
_controller.button_pressed.disconnect(_on_button_pressed.bind(_controller))
|
||||
_controller.button_released.disconnect(_on_button_released.bind(_controller))
|
||||
|
||||
# This will be unset in our superclass method
|
||||
|
||||
if _controller_left_node:
|
||||
if not Engine.is_editor_hint():
|
||||
_controller_left_node.button_pressed.disconnect(
|
||||
_on_button_pressed.bind(_controller_left_node))
|
||||
_controller_left_node.button_released.disconnect(
|
||||
_on_button_released.bind(_controller_left_node))
|
||||
|
||||
_controller_left_node = null
|
||||
|
||||
if _controller_right_node:
|
||||
if not Engine.is_editor_hint():
|
||||
_controller_right_node.button_pressed.disconnect(
|
||||
_on_button_pressed.bind(_controller_right_node))
|
||||
_controller_right_node.button_released.disconnect(
|
||||
_on_button_released.bind(_controller_right_node))
|
||||
|
||||
_controller_right_node = null
|
||||
|
||||
super._exit_tree()
|
||||
|
||||
# Called on each frame to update the pickup
|
||||
func _process(delta):
|
||||
super._process(delta)
|
||||
|
||||
# Do not process if in the editor
|
||||
if Engine.is_editor_hint() or !is_inside_tree():
|
||||
return
|
||||
|
||||
# Track the active controller (if this pointer is not childed to a controller)
|
||||
if _controller == null and _active_controller != null:
|
||||
transform = _active_controller.transform
|
||||
|
||||
# Handle world-scale changes
|
||||
var new_world_scale := XRServer.world_scale
|
||||
if (_world_scale != new_world_scale):
|
||||
_world_scale = new_world_scale
|
||||
_update_y_offset()
|
||||
|
||||
# Find the new pointer target
|
||||
var new_target : Node3D
|
||||
var new_at : Vector3
|
||||
var suppress_area := $SuppressArea
|
||||
if (enabled and
|
||||
not $SuppressArea.has_overlapping_bodies() and
|
||||
not $SuppressArea.has_overlapping_areas() and
|
||||
$RayCast.is_colliding()):
|
||||
new_at = $RayCast.get_collision_point()
|
||||
if target:
|
||||
# Locked to 'target' even if we're colliding with something else
|
||||
new_target = target
|
||||
else:
|
||||
# Target is whatever the raycast is colliding with
|
||||
new_target = $RayCast.get_collider()
|
||||
|
||||
# If no current or previous collisions then skip
|
||||
if not new_target and not last_target:
|
||||
return
|
||||
|
||||
# Handle pointer changes
|
||||
if new_target and not last_target:
|
||||
# Pointer entered new_target
|
||||
XRToolsPointerEvent.entered(self, new_target, new_at)
|
||||
|
||||
# Pointer moved on new_target for the first time
|
||||
XRToolsPointerEvent.moved(self, new_target, new_at, new_at)
|
||||
|
||||
# Update visible artifacts for hit
|
||||
_visible_hit(new_at)
|
||||
elif not new_target and last_target:
|
||||
# Pointer exited last_target
|
||||
XRToolsPointerEvent.exited(self, last_target, last_collided_at)
|
||||
|
||||
# Update visible artifacts for miss
|
||||
_visible_miss()
|
||||
elif new_target != last_target:
|
||||
# Pointer exited last_target
|
||||
XRToolsPointerEvent.exited(self, last_target, last_collided_at)
|
||||
|
||||
# Pointer entered new_target
|
||||
XRToolsPointerEvent.entered(self, new_target, new_at)
|
||||
|
||||
# Pointer moved on new_target
|
||||
XRToolsPointerEvent.moved(self, new_target, new_at, new_at)
|
||||
|
||||
# Move visible artifacts
|
||||
_visible_move(new_at)
|
||||
elif new_at != last_collided_at:
|
||||
# Pointer moved on new_target
|
||||
XRToolsPointerEvent.moved(self, new_target, new_at, last_collided_at)
|
||||
|
||||
# Move visible artifacts
|
||||
_visible_move(new_at)
|
||||
|
||||
# Update last values
|
||||
last_target = new_target
|
||||
last_collided_at = new_at
|
||||
|
||||
|
||||
# Set pointer enabled property
|
||||
func set_enabled(p_enabled : bool) -> void:
|
||||
enabled = p_enabled
|
||||
if is_inside_tree():
|
||||
_update_pointer()
|
||||
|
||||
|
||||
# Set pointer y_offset property
|
||||
func set_y_offset(p_offset : float) -> void:
|
||||
y_offset = p_offset
|
||||
if is_inside_tree():
|
||||
_update_y_offset()
|
||||
|
||||
|
||||
# Set pointer distance property
|
||||
func set_distance(p_new_value : float) -> void:
|
||||
distance = p_new_value
|
||||
if is_inside_tree():
|
||||
_update_distance()
|
||||
|
||||
|
||||
# Set pointer show_laser property
|
||||
func set_show_laser(p_show : LaserShow) -> void:
|
||||
show_laser = p_show
|
||||
if is_inside_tree():
|
||||
_update_pointer()
|
||||
|
||||
|
||||
# Set pointer laser_length property
|
||||
func set_laser_length(p_laser_length : LaserLength) -> void:
|
||||
laser_length = p_laser_length
|
||||
if is_inside_tree():
|
||||
_update_pointer()
|
||||
|
||||
|
||||
# Set pointer laser_material property
|
||||
func set_laser_material(p_laser_material : StandardMaterial3D) -> void:
|
||||
laser_material = p_laser_material
|
||||
if is_inside_tree():
|
||||
_update_pointer()
|
||||
|
||||
|
||||
# Set pointer laser_hit_material property
|
||||
func set_laser_hit_material(p_laser_hit_material : StandardMaterial3D) -> void:
|
||||
laser_hit_material = p_laser_hit_material
|
||||
if is_inside_tree():
|
||||
_update_pointer()
|
||||
|
||||
|
||||
# Set pointer show_target property
|
||||
func set_show_target(p_show_target : bool) -> void:
|
||||
show_target = p_show_target
|
||||
if is_inside_tree():
|
||||
$Target.visible = enabled and show_target and last_target
|
||||
|
||||
|
||||
# Set pointer target_radius property
|
||||
func set_target_radius(p_target_radius : float) -> void:
|
||||
target_radius = p_target_radius
|
||||
if is_inside_tree():
|
||||
_update_target_radius()
|
||||
|
||||
|
||||
# Set pointer target_material property
|
||||
func set_target_material(p_target_material : StandardMaterial3D) -> void:
|
||||
target_material = p_target_material
|
||||
if is_inside_tree():
|
||||
_update_target_material()
|
||||
|
||||
|
||||
# Set pointer collision_mask property
|
||||
func set_collision_mask(p_new_mask : int) -> void:
|
||||
collision_mask = p_new_mask
|
||||
if is_inside_tree():
|
||||
_update_collision_mask()
|
||||
|
||||
|
||||
# Set pointer collide_with_bodies property
|
||||
func set_collide_with_bodies(p_new_value : bool) -> void:
|
||||
collide_with_bodies = p_new_value
|
||||
if is_inside_tree():
|
||||
_update_collide_with_bodies()
|
||||
|
||||
|
||||
# Set pointer collide_with_areas property
|
||||
func set_collide_with_areas(p_new_value : bool) -> void:
|
||||
collide_with_areas = p_new_value
|
||||
if is_inside_tree():
|
||||
_update_collide_with_areas()
|
||||
|
||||
|
||||
# Set suppress radius property
|
||||
func set_suppress_radius(p_suppress_radius : float) -> void:
|
||||
suppress_radius = p_suppress_radius
|
||||
if is_inside_tree():
|
||||
_update_suppress_radius()
|
||||
|
||||
|
||||
func set_suppress_mask(p_suppress_mask : int) -> void:
|
||||
suppress_mask = p_suppress_mask
|
||||
if is_inside_tree():
|
||||
_update_suppress_mask()
|
||||
|
||||
|
||||
# Pointer Y offset update handler
|
||||
func _update_y_offset() -> void:
|
||||
$Laser.position.y = y_offset * _world_scale
|
||||
$RayCast.position.y = y_offset * _world_scale
|
||||
|
||||
|
||||
# Pointer distance update handler
|
||||
func _update_distance() -> void:
|
||||
$RayCast.target_position.z = -distance
|
||||
_update_pointer()
|
||||
|
||||
|
||||
# Pointer target radius update handler
|
||||
func _update_target_radius() -> void:
|
||||
$Target.mesh.radius = target_radius
|
||||
$Target.mesh.height = target_radius * 2
|
||||
|
||||
|
||||
# Pointer target_material update handler
|
||||
func _update_target_material() -> void:
|
||||
$Target.set_surface_override_material(0, target_material)
|
||||
|
||||
|
||||
# Pointer collision_mask update handler
|
||||
func _update_collision_mask() -> void:
|
||||
$RayCast.collision_mask = collision_mask
|
||||
|
||||
|
||||
# Pointer collide_with_bodies update handler
|
||||
func _update_collide_with_bodies() -> void:
|
||||
$RayCast.collide_with_bodies = collide_with_bodies
|
||||
|
||||
|
||||
# Pointer collide_with_areas update handler
|
||||
func _update_collide_with_areas() -> void:
|
||||
$RayCast.collide_with_areas = collide_with_areas
|
||||
|
||||
|
||||
# Pointer suppress_radius update handler
|
||||
func _update_suppress_radius() -> void:
|
||||
$SuppressArea/CollisionShape3D.shape.radius = suppress_radius
|
||||
|
||||
|
||||
# Pointer suppress_mask update handler
|
||||
func _update_suppress_mask() -> void:
|
||||
$SuppressArea.collision_mask = suppress_mask
|
||||
|
||||
|
||||
# Pointer visible artifacts update handler
|
||||
func _update_pointer() -> void:
|
||||
if enabled and last_target:
|
||||
_visible_hit(last_collided_at)
|
||||
else:
|
||||
_visible_miss()
|
||||
|
||||
|
||||
# Pointer-activation button pressed handler
|
||||
func _button_pressed() -> void:
|
||||
if $RayCast.is_colliding():
|
||||
# Report pressed
|
||||
target = $RayCast.get_collider()
|
||||
last_collided_at = $RayCast.get_collision_point()
|
||||
XRToolsPointerEvent.pressed(self, target, last_collided_at)
|
||||
|
||||
|
||||
# Pointer-activation button released handler
|
||||
func _button_released() -> void:
|
||||
if target:
|
||||
# Report release
|
||||
XRToolsPointerEvent.released(self, target, last_collided_at)
|
||||
target = null
|
||||
last_collided_at = Vector3(0, 0, 0)
|
||||
|
||||
|
||||
# Button pressed handler
|
||||
func _on_button_pressed(p_button : String, controller : XRController3D) -> void:
|
||||
if p_button == active_button_action and enabled:
|
||||
if controller == _active_controller:
|
||||
_button_pressed()
|
||||
else:
|
||||
_active_controller = controller
|
||||
|
||||
|
||||
# Button released handler
|
||||
func _on_button_released(p_button : String, _controller : XRController3D) -> void:
|
||||
if p_button == active_button_action and target:
|
||||
_button_released()
|
||||
|
||||
|
||||
# Update the laser active material
|
||||
func _update_laser_active_material(hit : bool) -> void:
|
||||
if hit and laser_hit_material:
|
||||
$Laser.set_surface_override_material(0, laser_hit_material)
|
||||
else:
|
||||
$Laser.set_surface_override_material(0, laser_material)
|
||||
|
||||
|
||||
# Update the visible artifacts to show a hit
|
||||
func _visible_hit(at : Vector3) -> void:
|
||||
# Show target if enabled
|
||||
if show_target:
|
||||
$Target.global_transform.origin = at
|
||||
$Target.visible = true
|
||||
|
||||
# Control laser visibility
|
||||
if show_laser != LaserShow.HIDE:
|
||||
# Ensure the correct laser material is set
|
||||
_update_laser_active_material(true)
|
||||
|
||||
# Adjust laser length
|
||||
if laser_length == LaserLength.COLLIDE:
|
||||
var collide_len : float = at.distance_to(global_transform.origin)
|
||||
$Laser.mesh.size.z = collide_len
|
||||
$Laser.position.z = collide_len * -0.5
|
||||
else:
|
||||
$Laser.mesh.size.z = distance
|
||||
$Laser.position.z = distance * -0.5
|
||||
|
||||
# Show laser
|
||||
$Laser.visible = true
|
||||
else:
|
||||
# Ensure laser is hidden
|
||||
$Laser.visible = false
|
||||
|
||||
|
||||
# Move the visible pointer artifacts to the target
|
||||
func _visible_move(at : Vector3) -> void:
|
||||
# Move target if configured
|
||||
if show_target:
|
||||
$Target.global_transform.origin = at
|
||||
|
||||
# Adjust laser length if set to collide-length
|
||||
if laser_length == LaserLength.COLLIDE:
|
||||
var collide_len : float = at.distance_to(global_transform.origin)
|
||||
$Laser.mesh.size.z = collide_len
|
||||
$Laser.position.z = collide_len * -0.5
|
||||
|
||||
|
||||
# Update the visible artifacts to show a miss
|
||||
func _visible_miss() -> void:
|
||||
# Ensure target is hidden
|
||||
$Target.visible = false
|
||||
|
||||
# Ensure the correct laser material is set
|
||||
_update_laser_active_material(false)
|
||||
|
||||
# Hide laser if not set to show always
|
||||
$Laser.visible = show_laser == LaserShow.SHOW
|
||||
|
||||
# Restore laser length if set to collide-length
|
||||
$Laser.mesh.size.z = distance
|
||||
$Laser.position.z = distance * -0.5
|
||||
1
addons/godot-xr-tools/functions/function_pointer.gd.uid
Normal file
1
addons/godot-xr-tools/functions/function_pointer.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cvnagt08r6ul4
|
||||
44
addons/godot-xr-tools/functions/function_pointer.tscn
Normal file
44
addons/godot-xr-tools/functions/function_pointer.tscn
Normal file
@@ -0,0 +1,44 @@
|
||||
[gd_scene load_steps=6 format=3 uid="uid://cqhw276realc"]
|
||||
|
||||
[ext_resource type="Material" path="res://addons/godot-xr-tools/materials/pointer.tres" id="1"]
|
||||
[ext_resource type="Script" uid="uid://cvnagt08r6ul4" path="res://addons/godot-xr-tools/functions/function_pointer.gd" id="2"]
|
||||
|
||||
[sub_resource type="BoxMesh" id="1"]
|
||||
resource_local_to_scene = true
|
||||
material = ExtResource("1")
|
||||
size = Vector3(0.002, 0.002, 10)
|
||||
subdivide_depth = 20
|
||||
|
||||
[sub_resource type="SphereMesh" id="2"]
|
||||
material = ExtResource("1")
|
||||
radius = 0.05
|
||||
height = 0.1
|
||||
radial_segments = 16
|
||||
rings = 8
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_k3gfm"]
|
||||
radius = 0.2
|
||||
|
||||
[node name="FunctionPointer" type="Node3D"]
|
||||
script = ExtResource("2")
|
||||
|
||||
[node name="RayCast" type="RayCast3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.013, 0)
|
||||
target_position = Vector3(0, 0, -10)
|
||||
collision_mask = 5242880
|
||||
|
||||
[node name="Laser" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.013, -5)
|
||||
cast_shadow = 0
|
||||
mesh = SubResource("1")
|
||||
|
||||
[node name="Target" type="MeshInstance3D" parent="."]
|
||||
visible = false
|
||||
mesh = SubResource("2")
|
||||
|
||||
[node name="SuppressArea" type="Area3D" parent="."]
|
||||
collision_layer = 0
|
||||
collision_mask = 4194304
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="SuppressArea"]
|
||||
shape = SubResource("SphereShape3D_k3gfm")
|
||||
117
addons/godot-xr-tools/functions/function_pose_detector.gd
Normal file
117
addons/godot-xr-tools/functions/function_pose_detector.gd
Normal file
@@ -0,0 +1,117 @@
|
||||
@tool
|
||||
@icon("res://addons/godot-xr-tools/editor/icons/hand.svg")
|
||||
class_name XRToolsFunctionPoseDetector
|
||||
extends XRToolsHandPalmOffset
|
||||
|
||||
|
||||
## XR Tools Function Pose Area
|
||||
##
|
||||
## This area works with the XRToolsHandPoseArea to control the pose
|
||||
## of the VR hands.
|
||||
|
||||
|
||||
# Default pose detector collision mask of 22:pose-area
|
||||
const DEFAULT_MASK := 0b0000_0000_0010_0000_0000_0000_0000_0000
|
||||
|
||||
|
||||
## Collision mask to detect hand pose areas
|
||||
@export_flags_3d_physics var collision_mask : int = DEFAULT_MASK: set = set_collision_mask
|
||||
|
||||
|
||||
## Hand to control
|
||||
var _hand : XRToolsHand
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsFunctionPoseDetector"
|
||||
|
||||
|
||||
# Called when we enter our tree
|
||||
func _enter_tree():
|
||||
super._enter_tree()
|
||||
|
||||
_hand = XRToolsHand.find_instance(self)
|
||||
|
||||
# Connect signals (if controller and hand are valid)
|
||||
if _controller and _hand:
|
||||
if $SenseArea.area_entered.connect(_on_area_entered):
|
||||
push_error("Unable to connect area_entered signal")
|
||||
if $SenseArea.area_exited.connect(_on_area_exited):
|
||||
push_error("Unable to connect area_exited signal")
|
||||
|
||||
# Update collision mask
|
||||
_update_collision_mask()
|
||||
|
||||
|
||||
func _exit_tree():
|
||||
# Disconnect signals (if controller and hand are valid)
|
||||
if _controller and _hand:
|
||||
$SenseArea.area_entered.disconnect(_on_area_entered)
|
||||
$SenseArea.area_exited.disconnect(_on_area_exited)
|
||||
|
||||
_hand = null
|
||||
super._exit_tree()
|
||||
|
||||
|
||||
# This method verifies the pose area has a valid configuration.
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings : PackedStringArray = super._get_configuration_warnings()
|
||||
|
||||
# Verify hand can be found
|
||||
if !XRToolsHand.find_instance(self):
|
||||
warnings.append("Node must be a within a branch of an XRController node with a hand")
|
||||
|
||||
# Pass basic validation
|
||||
return warnings
|
||||
|
||||
|
||||
func set_collision_mask(mask : int) -> void:
|
||||
collision_mask = mask
|
||||
if is_inside_tree():
|
||||
_update_collision_mask()
|
||||
|
||||
|
||||
func _update_collision_mask() -> void:
|
||||
$SenseArea.collision_mask = collision_mask
|
||||
|
||||
|
||||
## Signal handler called when this XRToolsFunctionPoseArea enters an area
|
||||
func _on_area_entered(area : Area3D) -> void:
|
||||
# Igjnore if the area is not a hand-pose area
|
||||
var pose_area := area as XRToolsHandPoseArea
|
||||
if !pose_area:
|
||||
return
|
||||
|
||||
# Get the positional tracker
|
||||
var tracker := XRServer.get_tracker(_controller.tracker) as XRPositionalTracker
|
||||
|
||||
# Set the appropriate poses
|
||||
if tracker.hand == XRPositionalTracker.TRACKER_HAND_LEFT and pose_area.left_pose:
|
||||
_hand.add_pose_override(
|
||||
pose_area,
|
||||
pose_area.pose_priority,
|
||||
pose_area.left_pose)
|
||||
# Disable grabpoints in this pose_area
|
||||
pose_area.disable_grab_points()
|
||||
elif tracker.hand == XRPositionalTracker.TRACKER_HAND_RIGHT and pose_area.right_pose:
|
||||
_hand.add_pose_override(
|
||||
pose_area,
|
||||
pose_area.pose_priority,
|
||||
pose_area.right_pose)
|
||||
# Disable grabpoints in this pose_area
|
||||
pose_area.disable_grab_points()
|
||||
|
||||
|
||||
## Signal handler called when this XRToolsFunctionPoseArea leaves an area
|
||||
func _on_area_exited(area : Area3D) -> void:
|
||||
# Ignore if the area is not a hand-pose area
|
||||
var pose_area := area as XRToolsHandPoseArea
|
||||
if !pose_area:
|
||||
return
|
||||
|
||||
# Remove any overrides set from this hand-pose area
|
||||
_hand.remove_pose_override(pose_area)
|
||||
|
||||
# Enable previously disabled grabpoints
|
||||
pose_area.enable_grab_points()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cupmjxb4tpq3p
|
||||
19
addons/godot-xr-tools/functions/function_pose_detector.tscn
Normal file
19
addons/godot-xr-tools/functions/function_pose_detector.tscn
Normal file
@@ -0,0 +1,19 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://bft3xyxs31ci3"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cupmjxb4tpq3p" path="res://addons/godot-xr-tools/functions/function_pose_detector.gd" id="1"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="1"]
|
||||
radius = 0.08
|
||||
height = 0.24
|
||||
|
||||
[node name="FunctionPoseDetector" type="Node3D"]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="SenseArea" type="Area3D" parent="."]
|
||||
collision_layer = 0
|
||||
collision_mask = 2097152
|
||||
monitorable = false
|
||||
|
||||
[node name="CollisionShape" type="CollisionShape3D" parent="SenseArea"]
|
||||
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0, 0, -0.01)
|
||||
shape = SubResource("1")
|
||||
495
addons/godot-xr-tools/functions/function_teleport.gd
Normal file
495
addons/godot-xr-tools/functions/function_teleport.gd
Normal file
@@ -0,0 +1,495 @@
|
||||
@tool
|
||||
@icon("res://addons/godot-xr-tools/editor/icons/function.svg")
|
||||
class_name XRToolsFunctionTeleport
|
||||
extends XRToolsHandPalmOffset
|
||||
|
||||
|
||||
## XR Tools Function Teleport Script
|
||||
##
|
||||
## This script provides teleport functionality.
|
||||
##
|
||||
## Add this scene as a sub scene of your [XRController3D] node to implement
|
||||
## a teleport function on that controller.
|
||||
|
||||
|
||||
# Default teleport collision mask of all
|
||||
const DEFAULT_MASK := 0b1111_1111_1111_1111_1111_1111_1111_1111
|
||||
|
||||
# Default material
|
||||
# gdlint:ignore = load-constant-name
|
||||
const _DefaultMaterial := preload("res://addons/godot-xr-tools/materials/capsule.tres")
|
||||
|
||||
|
||||
## If true, teleporting is enabled
|
||||
@export var enabled : bool = true: set = set_enabled
|
||||
|
||||
## Teleport button action
|
||||
@export var teleport_button_action : String = "trigger_click"
|
||||
|
||||
## Teleport rotation action
|
||||
@export var rotation_action : String = "primary"
|
||||
|
||||
# Teleport Path Group
|
||||
@export_group("Visuals")
|
||||
|
||||
## Teleport allowed color property
|
||||
@export var can_teleport_color : Color = Color(0.0, 1.0, 0.0, 1.0)
|
||||
|
||||
## Teleport denied color property
|
||||
@export var cant_teleport_color : Color = Color(1.0, 0.0, 0.0, 1.0)
|
||||
|
||||
## Teleport no-collision color property
|
||||
@export var no_collision_color : Color = Color(45.0 / 255.0, 80.0 / 255.0, 220.0 / 255.0, 1.0)
|
||||
|
||||
## Teleport-arc strength
|
||||
@export var strength : float = 5.0
|
||||
|
||||
## Teleport texture
|
||||
@export var arc_texture : Texture2D \
|
||||
= preload("res://addons/godot-xr-tools/images/teleport_arrow.png") \
|
||||
: set = set_arc_texture
|
||||
|
||||
## Target texture
|
||||
@export var target_texture : Texture2D \
|
||||
= preload("res://addons/godot-xr-tools/images/teleport_target.png") \
|
||||
: set = set_target_texture
|
||||
|
||||
# Player Group
|
||||
@export_group("Player")
|
||||
|
||||
## Player height property
|
||||
@export var player_height : float = 1.8: set = set_player_height
|
||||
|
||||
## Player radius property
|
||||
@export var player_radius : float = 0.4: set = set_player_radius
|
||||
|
||||
## Player scene
|
||||
@export var player_scene : PackedScene: set = set_player_scene
|
||||
|
||||
# Target Group
|
||||
@export_group("Collision")
|
||||
|
||||
## Maximum floor slope
|
||||
@export var max_slope : float = 20.0
|
||||
|
||||
## Collision mask
|
||||
@export_flags_3d_physics var collision_mask : int = 1023
|
||||
|
||||
## Valid teleport layer mask
|
||||
@export_flags_3d_physics var valid_teleport_mask : int = DEFAULT_MASK
|
||||
|
||||
|
||||
## Player capsule material (ignored for custom player scenes)
|
||||
var player_material : StandardMaterial3D = _DefaultMaterial : set = set_player_material
|
||||
|
||||
|
||||
var is_on_floor : bool = true
|
||||
var is_teleporting : bool = false
|
||||
var can_teleport : bool = true
|
||||
var teleport_rotation : float = 0.0
|
||||
var floor_normal : Vector3 = Vector3.UP
|
||||
var last_target_transform : Transform3D = Transform3D()
|
||||
var collision_shape : Shape3D
|
||||
var step_size : float = 0.5
|
||||
|
||||
|
||||
# Custom player scene
|
||||
var player : Node3D
|
||||
|
||||
|
||||
# World scale
|
||||
@onready var ws : float = XRServer.world_scale
|
||||
|
||||
## Capsule shown when not using a custom player mesh
|
||||
@onready var capsule : MeshInstance3D = $Target/Player_figure/Capsule
|
||||
|
||||
## [XRToolsPlayerBody] node.
|
||||
@onready var player_body := XRToolsPlayerBody.find_instance(self)
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsFunctionTeleport"
|
||||
|
||||
|
||||
func _enter_tree():
|
||||
var bt:= Transform3D()
|
||||
bt.origin = Vector3(0.0, 0.0, -0.1)
|
||||
set_base_transform(bt)
|
||||
|
||||
super._enter_tree()
|
||||
|
||||
# Called when the node enters the scene tree for the first time.
|
||||
func _ready():
|
||||
# Do not initialise if in the editor
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# It's inactive when we start
|
||||
$Teleport.visible = false
|
||||
$Target.visible = false
|
||||
|
||||
# Scale to our world scale
|
||||
$Teleport.mesh.size = Vector2(0.05 * ws, 1.0)
|
||||
$Target.mesh.size = Vector2(ws, ws)
|
||||
$Target/Player_figure.scale = Vector3(ws, ws, ws)
|
||||
|
||||
# get our capsule shape
|
||||
collision_shape = CapsuleShape3D.new()
|
||||
|
||||
# Apply properties
|
||||
_update_arc_texture()
|
||||
_update_target_texture()
|
||||
_update_player_scene()
|
||||
_update_player_height()
|
||||
_update_player_radius()
|
||||
_update_player_material()
|
||||
|
||||
|
||||
func _physics_process(delta):
|
||||
# Do not process physics if in the editor
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# Skip if required nodes are missing
|
||||
if !player_body or !_controller:
|
||||
return
|
||||
|
||||
# if we're not enabled no point in doing mode
|
||||
if !enabled:
|
||||
# reset these
|
||||
is_teleporting = false
|
||||
$Teleport.visible = false
|
||||
$Target.visible = false
|
||||
|
||||
# and stop this from running until we enable again
|
||||
set_physics_process(false)
|
||||
return
|
||||
|
||||
# check if our world scale has changed..
|
||||
var new_ws := XRServer.world_scale
|
||||
if ws != new_ws:
|
||||
ws = new_ws
|
||||
$Teleport.mesh.size = Vector2(0.05 * ws, 1.0)
|
||||
$Target.mesh.size = Vector2(ws, ws)
|
||||
$Target/Player_figure.scale = Vector3(ws, ws, ws)
|
||||
|
||||
if _controller and _controller.get_is_active() and \
|
||||
_controller.is_button_pressed(teleport_button_action):
|
||||
if !is_teleporting:
|
||||
is_teleporting = true
|
||||
$Teleport.visible = true
|
||||
$Target.visible = true
|
||||
teleport_rotation = 0.0
|
||||
|
||||
# get our physics engine state
|
||||
var state := get_world_3d().direct_space_state
|
||||
var query := PhysicsShapeQueryParameters3D.new()
|
||||
|
||||
# init stuff about our query that doesn't change
|
||||
query.collision_mask = collision_mask
|
||||
query.margin = collision_shape.margin
|
||||
query.shape_rid = collision_shape.get_rid()
|
||||
|
||||
# make a transform for offsetting our shape, it's always
|
||||
# lying on its side by default...
|
||||
var shape_transform := Transform3D(
|
||||
Basis(),
|
||||
Vector3(0.0, player_height / 2.0, 0.0))
|
||||
|
||||
# update location
|
||||
var teleport_global_transform : Transform3D = $Teleport.global_transform
|
||||
var target_global_origin := teleport_global_transform.origin
|
||||
var up := player_body.up_player
|
||||
var down := -up.normalized() / ws
|
||||
|
||||
############################################################
|
||||
# New teleport logic
|
||||
# We're going to use test move in steps to find out where we hit something...
|
||||
# This can be optimised loads by determining the lenght based on the angle
|
||||
# between sections extending the length when we're in a flat part of the arch
|
||||
# Where we do get a collission we may want to fine tune the collision
|
||||
var cast_length := 0.0
|
||||
var fine_tune := 1.0
|
||||
var hit_something := false
|
||||
var max_slope_cos := cos(deg_to_rad(max_slope))
|
||||
for i in range(1,26):
|
||||
var new_cast_length := cast_length + (step_size / fine_tune)
|
||||
var global_target := Vector3(0.0, 0.0, -new_cast_length)
|
||||
|
||||
# our quadratic values
|
||||
var t := global_target.z / strength
|
||||
var t2 := t * t
|
||||
|
||||
# target to world space
|
||||
global_target = teleport_global_transform * global_target
|
||||
|
||||
# adjust for gravity
|
||||
global_target += down * t2
|
||||
|
||||
# test our new location for collisions
|
||||
query.transform = Transform3D(
|
||||
player_body.global_transform.basis,
|
||||
global_target) * shape_transform
|
||||
var cast_result := state.collide_shape(query, 10)
|
||||
if cast_result.is_empty():
|
||||
# we didn't collide with anything so check our next section...
|
||||
cast_length = new_cast_length
|
||||
target_global_origin = global_target
|
||||
elif (fine_tune <= 16.0):
|
||||
# try again with a small step size
|
||||
fine_tune *= 2.0
|
||||
else:
|
||||
# if we don't collide make sure we keep using our current origin point
|
||||
var collided_at := target_global_origin
|
||||
|
||||
# check for collision
|
||||
var step_delta := global_target - target_global_origin
|
||||
if up.dot(step_delta) > 0:
|
||||
# if we're moving up, we hit the ceiling of something, we
|
||||
# don't really care what
|
||||
is_on_floor = false
|
||||
else:
|
||||
# now we cast a ray downwards to see if we're on a surface
|
||||
var ray_query := PhysicsRayQueryParameters3D.new()
|
||||
ray_query.from = target_global_origin + (up * 0.5 * player_height)
|
||||
ray_query.to = target_global_origin - (up * 1.1 * player_height)
|
||||
ray_query.collision_mask = collision_mask
|
||||
|
||||
var intersects := state.intersect_ray(ray_query)
|
||||
if intersects.is_empty():
|
||||
is_on_floor = false
|
||||
else:
|
||||
# did we collide with a floor or a wall?
|
||||
floor_normal = intersects["normal"]
|
||||
var dot := up.dot(floor_normal)
|
||||
|
||||
if dot > max_slope_cos:
|
||||
is_on_floor = true
|
||||
else:
|
||||
is_on_floor = false
|
||||
|
||||
# Update our collision point if it's moved enough, this
|
||||
# solves a little bit of jittering
|
||||
var diff : Vector3 = collided_at - intersects["position"]
|
||||
|
||||
if diff.length() > 0.1:
|
||||
collided_at = intersects["position"]
|
||||
|
||||
# Fail if the hit target isn't in our valid mask
|
||||
var collider_mask : int = intersects["collider"].collision_layer
|
||||
if not valid_teleport_mask & collider_mask:
|
||||
is_on_floor = false
|
||||
|
||||
# we are colliding, find our if we're colliding on a wall or
|
||||
# floor, one we can do, the other nope...
|
||||
cast_length += (collided_at - target_global_origin).length()
|
||||
target_global_origin = collided_at
|
||||
hit_something = true
|
||||
break
|
||||
|
||||
# and just update our shader
|
||||
$Teleport.get_surface_override_material(0).set_shader_parameter("scale_t", 1.0 / strength)
|
||||
$Teleport.get_surface_override_material(0).set_shader_parameter("down", down)
|
||||
$Teleport.get_surface_override_material(0).set_shader_parameter("length", cast_length)
|
||||
if hit_something:
|
||||
var color := can_teleport_color
|
||||
var normal := up
|
||||
if is_on_floor:
|
||||
# if we're on the floor we'll reorientate our target to match.
|
||||
normal = floor_normal
|
||||
can_teleport = true
|
||||
else:
|
||||
can_teleport = false
|
||||
color = cant_teleport_color
|
||||
|
||||
# check our axis to see if we need to rotate
|
||||
teleport_rotation += (delta * _controller.get_vector2(rotation_action).x * -4.0)
|
||||
|
||||
# update target and colour
|
||||
var target_basis := Basis()
|
||||
target_basis.y = normal
|
||||
target_basis.x = teleport_global_transform.basis.x.slide(normal).normalized()
|
||||
target_basis.z = target_basis.x.cross(target_basis.y)
|
||||
|
||||
target_basis = target_basis.rotated(normal, teleport_rotation)
|
||||
last_target_transform.basis = target_basis
|
||||
last_target_transform.origin = target_global_origin + up * 0.001
|
||||
$Target.global_transform = last_target_transform
|
||||
|
||||
$Teleport.get_surface_override_material(0).set_shader_parameter("mix_color", color)
|
||||
$Target.get_surface_override_material(0).albedo_color = color
|
||||
$Target.visible = can_teleport
|
||||
else:
|
||||
can_teleport = false
|
||||
$Target.visible = false
|
||||
$Teleport.get_surface_override_material(0).set_shader_parameter("mix_color", no_collision_color)
|
||||
elif is_teleporting:
|
||||
if can_teleport:
|
||||
|
||||
# Make our target using the players up vector
|
||||
var new_transform := last_target_transform
|
||||
new_transform.basis.y = player_body.up_player
|
||||
new_transform.basis.x = new_transform.basis.y.cross(new_transform.basis.z).normalized()
|
||||
new_transform.basis.z = new_transform.basis.x.cross(new_transform.basis.y).normalized()
|
||||
|
||||
# Teleport the player
|
||||
player_body.teleport(new_transform)
|
||||
|
||||
# and disable
|
||||
is_teleporting = false
|
||||
$Teleport.visible = false
|
||||
$Target.visible = false
|
||||
|
||||
|
||||
# This method verifies the teleport has a valid configuration.
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings : PackedStringArray = super._get_configuration_warnings()
|
||||
|
||||
# Verify we can find the XRToolsPlayerBody
|
||||
if !XRToolsPlayerBody.find_instance(self):
|
||||
warnings.append("This node must be within a branch of an XRToolsPlayerBody node")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
|
||||
|
||||
# Provide custom property information
|
||||
func _get_property_list() -> Array[Dictionary]:
|
||||
return [
|
||||
{
|
||||
"name" : "Player",
|
||||
"type" : TYPE_NIL,
|
||||
"usage" : PROPERTY_USAGE_GROUP
|
||||
},
|
||||
{
|
||||
"name" : "player_material",
|
||||
"class_name" : "StandardMaterial3D",
|
||||
"type" : TYPE_OBJECT,
|
||||
"usage" : PROPERTY_USAGE_NO_EDITOR if player_scene else PROPERTY_USAGE_DEFAULT,
|
||||
"hint" : PROPERTY_HINT_RESOURCE_TYPE,
|
||||
"hint_string" : "StandardMaterial3D"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# Allow revert of custom properties
|
||||
func _property_can_revert(property : StringName) -> bool:
|
||||
return property == "player_material"
|
||||
|
||||
|
||||
# Provide revert values for custom properties
|
||||
func _property_get_revert(property : StringName): # Variant
|
||||
if property == "player_material":
|
||||
return _DefaultMaterial
|
||||
|
||||
|
||||
# Set enabled property
|
||||
func set_enabled(new_value : bool) -> void:
|
||||
enabled = new_value
|
||||
if enabled:
|
||||
# make sure our physics process is on
|
||||
set_physics_process(true)
|
||||
else:
|
||||
# we turn this off in physics process just in case we want to do some cleanup
|
||||
pass
|
||||
|
||||
|
||||
# Set the arc texture
|
||||
func set_arc_texture(p_arc_texture : Texture2D) -> void:
|
||||
arc_texture = p_arc_texture
|
||||
if is_inside_tree():
|
||||
_update_arc_texture()
|
||||
|
||||
|
||||
# Set the target texture
|
||||
func set_target_texture(p_target_texture : Texture2D) -> void:
|
||||
target_texture = p_target_texture
|
||||
if is_inside_tree():
|
||||
_update_target_texture()
|
||||
|
||||
|
||||
# Set player height property
|
||||
func set_player_height(p_height : float) -> void:
|
||||
player_height = p_height
|
||||
if is_inside_tree():
|
||||
_update_player_height()
|
||||
|
||||
|
||||
# Set player radius property
|
||||
func set_player_radius(p_radius : float) -> void:
|
||||
player_radius = p_radius
|
||||
if is_inside_tree():
|
||||
_update_player_radius()
|
||||
|
||||
|
||||
# Set the player scene
|
||||
func set_player_scene(p_player_scene : PackedScene) -> void:
|
||||
player_scene = p_player_scene
|
||||
notify_property_list_changed()
|
||||
if is_inside_tree():
|
||||
_update_player_scene()
|
||||
|
||||
|
||||
# Set the player material
|
||||
func set_player_material(p_player_material : StandardMaterial3D) -> void:
|
||||
player_material = p_player_material
|
||||
if is_inside_tree():
|
||||
_update_player_material()
|
||||
|
||||
|
||||
# Update arc texture
|
||||
func _update_arc_texture():
|
||||
var material : ShaderMaterial = $Teleport.get_surface_override_material(0)
|
||||
if material and arc_texture:
|
||||
material.set_shader_parameter("arrow_texture", arc_texture)
|
||||
|
||||
|
||||
# Update target texture
|
||||
func _update_target_texture():
|
||||
var material : StandardMaterial3D = $Target.get_surface_override_material(0)
|
||||
if material and target_texture:
|
||||
material.albedo_texture = target_texture
|
||||
|
||||
|
||||
# Player height update handler
|
||||
func _update_player_height() -> void:
|
||||
if collision_shape:
|
||||
collision_shape.height = player_height - (2.0 * player_radius)
|
||||
|
||||
if capsule:
|
||||
capsule.mesh.height = player_height
|
||||
capsule.position = Vector3(0.0, player_height/2.0, 0.0)
|
||||
|
||||
|
||||
# Player radius update handler
|
||||
func _update_player_radius():
|
||||
if collision_shape:
|
||||
collision_shape.height = player_height
|
||||
collision_shape.radius = player_radius
|
||||
|
||||
if capsule:
|
||||
capsule.mesh.height = player_height
|
||||
capsule.mesh.radius = player_radius
|
||||
|
||||
|
||||
# Update the player scene
|
||||
func _update_player_scene() -> void:
|
||||
# Free the current player
|
||||
if player:
|
||||
player.queue_free()
|
||||
player = null
|
||||
|
||||
# If specified, instantiate a new player
|
||||
if player_scene:
|
||||
player = player_scene.instantiate()
|
||||
$Target/Player_figure.add_child(player)
|
||||
|
||||
# Show the capsule mesh only if we have no player
|
||||
capsule.visible = player == null
|
||||
|
||||
|
||||
# Update player material
|
||||
func _update_player_material():
|
||||
if player_material:
|
||||
capsule.set_surface_override_material(0, player_material)
|
||||
1
addons/godot-xr-tools/functions/function_teleport.gd.uid
Normal file
1
addons/godot-xr-tools/functions/function_teleport.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://blaa027erhjlp
|
||||
37
addons/godot-xr-tools/functions/function_teleport.tscn
Normal file
37
addons/godot-xr-tools/functions/function_teleport.tscn
Normal file
@@ -0,0 +1,37 @@
|
||||
[gd_scene load_steps=8 format=3 uid="uid://fiul51tsyoop"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://blaa027erhjlp" path="res://addons/godot-xr-tools/functions/function_teleport.gd" id="1"]
|
||||
[ext_resource type="Material" uid="uid://bk72wfw25ff0v" path="res://addons/godot-xr-tools/materials/teleport.tres" id="2"]
|
||||
[ext_resource type="Material" path="res://addons/godot-xr-tools/materials/target.tres" id="3"]
|
||||
[ext_resource type="Material" path="res://addons/godot-xr-tools/materials/capsule.tres" id="4"]
|
||||
|
||||
[sub_resource type="PlaneMesh" id="1"]
|
||||
size = Vector2(0.05, 1)
|
||||
subdivide_depth = 40
|
||||
|
||||
[sub_resource type="PlaneMesh" id="2"]
|
||||
size = Vector2(1, 1)
|
||||
|
||||
[sub_resource type="CapsuleMesh" id="3"]
|
||||
radius = 0.4
|
||||
height = 1.8
|
||||
|
||||
[node name="FunctionTeleport" type="Node3D"]
|
||||
script = ExtResource("1")
|
||||
player_material = ExtResource("4")
|
||||
|
||||
[node name="Teleport" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("1")
|
||||
surface_material_override/0 = ExtResource("2")
|
||||
|
||||
[node name="Target" type="MeshInstance3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, -4.92359)
|
||||
mesh = SubResource("2")
|
||||
surface_material_override/0 = ExtResource("3")
|
||||
|
||||
[node name="Player_figure" type="Marker3D" parent="Target"]
|
||||
|
||||
[node name="Capsule" type="MeshInstance3D" parent="Target/Player_figure"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9, 0)
|
||||
mesh = SubResource("3")
|
||||
surface_material_override/0 = ExtResource("4")
|
||||
283
addons/godot-xr-tools/functions/movement_climb.gd
Normal file
283
addons/godot-xr-tools/functions/movement_climb.gd
Normal file
@@ -0,0 +1,283 @@
|
||||
@tool
|
||||
class_name XRToolsMovementClimb
|
||||
extends XRToolsMovementProvider
|
||||
|
||||
|
||||
## XR Tools Movement Provider for Climbing
|
||||
##
|
||||
## This script provides climbing movement for the player. To add climbing
|
||||
## support, the player must also have [XRToolsFunctionPickup] nodes attached
|
||||
## to the left and right controllers, and an [XRToolsPlayerBody] under the
|
||||
## [XROrigin3D].
|
||||
##
|
||||
## Climbable objects can inherit from the climbable scene, or be [StaticBody]
|
||||
## objects with the [XRToolsClimbable] script attached to them.
|
||||
##
|
||||
## When climbing, the global velocity of the [XRToolsPlayerBody] is averaged,
|
||||
## and upon release the velocity is applied to the [XRToolsPlayerBody] with an
|
||||
## optional fling multiplier, so the player can fling themselves up walls if
|
||||
## desired.
|
||||
|
||||
|
||||
## Signal invoked when the player starts climing
|
||||
signal player_climb_start
|
||||
|
||||
## Signal invoked when the player ends climbing
|
||||
signal player_climb_end
|
||||
|
||||
|
||||
## Distance at which grabs snap
|
||||
const SNAP_DISTANCE : float = 1.0
|
||||
|
||||
|
||||
## Movement provider order
|
||||
@export var order : int = 15
|
||||
|
||||
## Push forward when flinging
|
||||
@export var forward_push : float = 1.0
|
||||
|
||||
## Velocity multiplier when flinging up walls
|
||||
@export var fling_multiplier : float = 1.0
|
||||
|
||||
## Averages for velocity measurement
|
||||
@export var velocity_averages : int = 5
|
||||
|
||||
|
||||
# Left climbing handle
|
||||
var _left_handle : Node3D
|
||||
|
||||
# Right climbing handle
|
||||
var _right_handle : Node3D
|
||||
|
||||
# Dominant handle (moving the player)
|
||||
var _dominant : Node3D
|
||||
|
||||
|
||||
# Velocity averager
|
||||
@onready var _averager := XRToolsVelocityAveragerLinear.new(velocity_averages)
|
||||
|
||||
# Left pickup node
|
||||
@onready var _left_pickup_node := XRToolsFunctionPickup.find_left(self)
|
||||
|
||||
# Right pickup node
|
||||
@onready var _right_pickup_node := XRToolsFunctionPickup.find_right(self)
|
||||
|
||||
# Left controller
|
||||
@onready var _left_controller := XRHelpers.get_left_controller(self)
|
||||
|
||||
# Right controller
|
||||
@onready var _right_controller := XRHelpers.get_right_controller(self)
|
||||
|
||||
# Left collision hand
|
||||
@onready var _left_hand := XRToolsHand.find_left(self)
|
||||
|
||||
# Right collision hand
|
||||
@onready var _right_hand := XRToolsHand.find_right(self)
|
||||
|
||||
# Left collision hand
|
||||
@onready var _left_collision_hand := XRToolsCollisionHand.find_left(self)
|
||||
|
||||
# Right collision hand
|
||||
@onready var _right_collision_hand := XRToolsCollisionHand.find_right(self)
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsMovementClimb" 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()
|
||||
|
||||
# Do not initialise if in the editor
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# Connect pickup funcitons
|
||||
if _left_pickup_node.connect("has_picked_up", _on_left_picked_up):
|
||||
push_error("Unable to connect left picked up signal")
|
||||
if _right_pickup_node.connect("has_picked_up", _on_right_picked_up):
|
||||
push_error("Unable to connect right picked up signal")
|
||||
if _left_pickup_node.connect("has_dropped", _on_left_dropped):
|
||||
push_error("Unable to connect left dropped signal")
|
||||
if _right_pickup_node.connect("has_dropped", _on_right_dropped):
|
||||
push_error("Unable to connect right dropped signal")
|
||||
|
||||
|
||||
## Perform player physics movement
|
||||
func physics_movement(delta: float, player_body: XRToolsPlayerBody, disabled: bool):
|
||||
# Disable climbing if requested
|
||||
if disabled or !enabled:
|
||||
_set_climbing(false, player_body)
|
||||
return
|
||||
|
||||
# Check for climbing handles being deleted while held
|
||||
if not is_instance_valid(_left_handle):
|
||||
_left_handle = null
|
||||
if not is_instance_valid(_right_handle):
|
||||
_right_handle = null
|
||||
if not is_instance_valid(_dominant):
|
||||
_dominant = null
|
||||
|
||||
# Snap grabs if too far
|
||||
if _left_handle:
|
||||
var left_pickup_pos := _left_controller.global_position
|
||||
var left_grab_pos = _left_handle.global_position
|
||||
if left_pickup_pos.distance_to(left_grab_pos) > SNAP_DISTANCE:
|
||||
_left_pickup_node.drop_object()
|
||||
if _right_handle:
|
||||
var right_pickup_pos := _right_controller.global_position
|
||||
var right_grab_pos := _right_handle.global_position
|
||||
if right_pickup_pos.distance_to(right_grab_pos) > SNAP_DISTANCE:
|
||||
_right_pickup_node.drop_object()
|
||||
|
||||
# Update climbing
|
||||
_set_climbing(_dominant != null, player_body)
|
||||
|
||||
# Skip if not actively climbing
|
||||
if !is_active:
|
||||
return
|
||||
|
||||
# Calculate how much the player has moved
|
||||
var offset := Vector3.ZERO
|
||||
if _dominant == _left_handle:
|
||||
var left_pickup_pos := _left_controller.global_position
|
||||
var left_grab_pos := _left_handle.global_position
|
||||
offset = left_pickup_pos - left_grab_pos
|
||||
elif _dominant == _right_handle:
|
||||
var right_pickup_pos := _right_controller.global_position
|
||||
var right_grab_pos := _right_handle.global_position
|
||||
offset = right_pickup_pos - right_grab_pos
|
||||
|
||||
# Move the player by the offset
|
||||
var old_position := player_body.global_position
|
||||
player_body.move_and_collide(-offset)
|
||||
player_body.velocity = Vector3.ZERO
|
||||
|
||||
# Update the players average-velocity data
|
||||
var distance := player_body.global_position - old_position
|
||||
_averager.add_distance(delta, distance)
|
||||
|
||||
# Report exclusive motion performed (to bypass gravity)
|
||||
return true
|
||||
|
||||
|
||||
## Start or stop climbing
|
||||
func _set_climbing(active: bool, player_body: XRToolsPlayerBody) -> void:
|
||||
# Skip if no change
|
||||
if active == is_active:
|
||||
return
|
||||
|
||||
# Update state
|
||||
is_active = active
|
||||
|
||||
# Handle state change
|
||||
if is_active:
|
||||
_averager.clear()
|
||||
player_body.override_player_height(self, 0.0)
|
||||
emit_signal("player_climb_start")
|
||||
else:
|
||||
# Calculate the forward direction (based on camera-forward)
|
||||
var dir_forward = -player_body.camera_node.global_transform.basis.z \
|
||||
.slide(player_body.up_player) \
|
||||
.normalized()
|
||||
|
||||
# Set player velocity based on averaged velocity, fling multiplier,
|
||||
# and a forward push
|
||||
var velocity := _averager.velocity()
|
||||
player_body.velocity = (velocity * fling_multiplier) + (dir_forward * forward_push)
|
||||
|
||||
player_body.override_player_height(self)
|
||||
emit_signal("player_climb_end")
|
||||
|
||||
|
||||
## Handler for left controller picked up
|
||||
func _on_left_picked_up(what : Node3D) -> void:
|
||||
# Get the climbable
|
||||
var climbable = what as XRToolsClimbable
|
||||
if not climbable:
|
||||
return
|
||||
|
||||
# Get the handle
|
||||
_left_handle = climbable.get_grab_handle(_left_pickup_node)
|
||||
if not _left_handle:
|
||||
return
|
||||
|
||||
# Switch dominance to the left handle
|
||||
_dominant = _left_handle
|
||||
|
||||
# If collision hands present then target the handle
|
||||
if _left_collision_hand:
|
||||
_left_collision_hand.add_target_override(_left_handle, 0)
|
||||
elif _left_hand:
|
||||
_left_hand.add_target_override(_left_handle, 0)
|
||||
|
||||
|
||||
## Handler for right controller picked up
|
||||
func _on_right_picked_up(what : Node3D) -> void:
|
||||
# Get the climbable
|
||||
var climbable = what as XRToolsClimbable
|
||||
if not climbable:
|
||||
return
|
||||
|
||||
# Get the handle
|
||||
_right_handle = climbable.get_grab_handle(_right_pickup_node)
|
||||
if not _right_handle:
|
||||
return
|
||||
|
||||
# Switch dominance to the right handle
|
||||
_dominant = _right_handle
|
||||
|
||||
# If collision hands present then target the handle
|
||||
if _right_collision_hand:
|
||||
_right_collision_hand.add_target_override(_right_handle, 0)
|
||||
elif _right_hand:
|
||||
_right_hand.add_target_override(_right_handle, 0)
|
||||
|
||||
|
||||
## Handler for left controller dropped
|
||||
func _on_left_dropped() -> void:
|
||||
# If collision hands present then clear handle target
|
||||
if _left_collision_hand:
|
||||
_left_collision_hand.remove_target_override(_left_handle)
|
||||
if _left_hand:
|
||||
_left_hand.remove_target_override(_left_handle)
|
||||
|
||||
# Release handle and transfer dominance
|
||||
_left_handle = null
|
||||
_dominant = _right_handle
|
||||
|
||||
|
||||
## Handler for righ controller dropped
|
||||
func _on_right_dropped() -> void:
|
||||
# If collision hands present then clear handle target
|
||||
if _right_collision_hand:
|
||||
_right_collision_hand.remove_target_override(_right_handle)
|
||||
if _right_hand:
|
||||
_right_hand.remove_target_override(_right_handle)
|
||||
|
||||
# Release handle and transfer dominance
|
||||
_right_handle = null
|
||||
_dominant = _left_handle
|
||||
|
||||
|
||||
# This method verifies the movement provider has a valid configuration.
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := super()
|
||||
|
||||
# Verify the left controller pickup
|
||||
if !XRToolsFunctionPickup.find_left(self):
|
||||
warnings.append("Unable to find left XRToolsFunctionPickup node")
|
||||
|
||||
# Verify the right controller pickup
|
||||
if !XRToolsFunctionPickup.find_right(self):
|
||||
warnings.append("Unable to find right XRToolsFunctionPickup node")
|
||||
|
||||
# Verify velocity averages
|
||||
if velocity_averages < 2:
|
||||
warnings.append("Minimum of 2 velocity averages needed")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
1
addons/godot-xr-tools/functions/movement_climb.gd.uid
Normal file
1
addons/godot-xr-tools/functions/movement_climb.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dtl3uo5dnibof
|
||||
6
addons/godot-xr-tools/functions/movement_climb.tscn
Normal file
6
addons/godot-xr-tools/functions/movement_climb.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://bxm1ply47vaan"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dtl3uo5dnibof" path="res://addons/godot-xr-tools/functions/movement_climb.gd" id="1"]
|
||||
|
||||
[node name="MovementClimb" type="Node3D" groups=["movement_providers"]]
|
||||
script = ExtResource("1")
|
||||
102
addons/godot-xr-tools/functions/movement_crouch.gd
Normal file
102
addons/godot-xr-tools/functions/movement_crouch.gd
Normal file
@@ -0,0 +1,102 @@
|
||||
@tool
|
||||
class_name XRToolsMovementCrouch
|
||||
extends XRToolsMovementProvider
|
||||
|
||||
|
||||
## XR Tools Movement Provider for Crouching
|
||||
##
|
||||
## This script works with the [XRToolsPlayerBody] attached to the players
|
||||
## [XROrigin3D].
|
||||
##
|
||||
## While the player presses the crounch button, the height is overridden to
|
||||
## the specified crouch height.
|
||||
|
||||
|
||||
## Enumeration of crouching modes
|
||||
enum CrouchType {
|
||||
HOLD_TO_CROUCH, ## Hold button to crouch
|
||||
TOGGLE_CROUCH, ## Toggle crouching on button press
|
||||
}
|
||||
|
||||
|
||||
## Movement provider order
|
||||
@export var order : int = 10
|
||||
|
||||
## Crouch height
|
||||
@export var crouch_height : float = 1.0
|
||||
|
||||
## Crouch button
|
||||
@export var crouch_button_action : String = "primary_click"
|
||||
|
||||
## Type of crouching
|
||||
@export var crouch_type : CrouchType = CrouchType.HOLD_TO_CROUCH
|
||||
|
||||
|
||||
## Crouching flag
|
||||
var _crouching : bool = false
|
||||
|
||||
## Crouch button down state
|
||||
var _crouch_button_down : bool = false
|
||||
|
||||
|
||||
# Controller node
|
||||
var _controller : XRController3D
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsMovementCrouch" or super(xr_name)
|
||||
|
||||
|
||||
# Called when our node is added to our scene tree
|
||||
func _enter_tree():
|
||||
_controller = XRHelpers.get_xr_controller(self)
|
||||
|
||||
|
||||
# Called when our node is removed from our scene tree
|
||||
func _exit_tree():
|
||||
_controller = null
|
||||
|
||||
|
||||
# Perform jump movement
|
||||
func physics_movement(_delta: float, player_body: XRToolsPlayerBody, _disabled: bool):
|
||||
# Skip if the controller isn't active
|
||||
if not _controller or not _controller.get_is_active():
|
||||
return
|
||||
|
||||
# Detect crouch button down and pressed states
|
||||
var crouch_button_down := _controller.is_button_pressed(crouch_button_action)
|
||||
var crouch_button_pressed := crouch_button_down and !_crouch_button_down
|
||||
_crouch_button_down = crouch_button_down
|
||||
|
||||
# Calculate new crouching state
|
||||
var crouching := _crouching
|
||||
match crouch_type:
|
||||
CrouchType.HOLD_TO_CROUCH:
|
||||
# Crouch when button down
|
||||
crouching = crouch_button_down
|
||||
|
||||
CrouchType.TOGGLE_CROUCH:
|
||||
# Toggle when button pressed
|
||||
if crouch_button_pressed:
|
||||
crouching = !crouching
|
||||
|
||||
# Update crouching state
|
||||
if crouching != _crouching:
|
||||
_crouching = crouching
|
||||
if crouching:
|
||||
player_body.override_player_height(self, crouch_height)
|
||||
else:
|
||||
player_body.override_player_height(self)
|
||||
|
||||
|
||||
# This method verifies the movement provider has a valid configuration.
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := super()
|
||||
|
||||
# Check the controller node
|
||||
if not XRHelpers.get_xr_controller(self):
|
||||
warnings.append("This node must be within a branch of an XRController3D node")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
1
addons/godot-xr-tools/functions/movement_crouch.gd.uid
Normal file
1
addons/godot-xr-tools/functions/movement_crouch.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://vuhhcx5m67gt
|
||||
6
addons/godot-xr-tools/functions/movement_crouch.tscn
Normal file
6
addons/godot-xr-tools/functions/movement_crouch.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://clt88d5d1dje4"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://vuhhcx5m67gt" path="res://addons/godot-xr-tools/functions/movement_crouch.gd" id="1"]
|
||||
|
||||
[node name="MovementCrouch" type="Node3D" groups=["movement_providers"]]
|
||||
script = ExtResource("1")
|
||||
97
addons/godot-xr-tools/functions/movement_direct.gd
Normal file
97
addons/godot-xr-tools/functions/movement_direct.gd
Normal file
@@ -0,0 +1,97 @@
|
||||
@tool
|
||||
class_name XRToolsMovementDirect
|
||||
extends XRToolsMovementProvider
|
||||
|
||||
|
||||
## XR Tools Movement Provider for Direct Movement
|
||||
##
|
||||
## This script provides direct movement for the player. This script works
|
||||
## with the [XRToolsPlayerBody] attached to the players [XROrigin3D].
|
||||
##
|
||||
## The player may have multiple [XRToolsMovementDirect] nodes attached to
|
||||
## different controllers to provide different types of direct movement.
|
||||
|
||||
|
||||
## Movement provider order
|
||||
@export var order : int = 10
|
||||
|
||||
## Movement speed
|
||||
@export var max_speed : float = 3.0
|
||||
|
||||
## If true, the player can strafe
|
||||
@export var strafe : bool = false
|
||||
|
||||
## Input action for movement direction
|
||||
@export var input_action : String = "primary"
|
||||
|
||||
|
||||
# Controller node
|
||||
var _controller : XRController3D
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsMovementDirect" or super(xr_name)
|
||||
|
||||
|
||||
# Called when our node is added to our scene tree
|
||||
func _enter_tree():
|
||||
_controller = XRHelpers.get_xr_controller(self)
|
||||
|
||||
|
||||
# Called when our node is removed from our scene tree
|
||||
func _exit_tree():
|
||||
_controller = null
|
||||
|
||||
|
||||
# Perform jump movement
|
||||
func physics_movement(_delta: float, player_body: XRToolsPlayerBody, _disabled: bool):
|
||||
# Skip if the controller isn't active
|
||||
if not _controller or not _controller.get_is_active():
|
||||
return
|
||||
|
||||
## get input action with deadzone correction applied
|
||||
var dz_input_action = XRToolsUserSettings.get_adjusted_vector2(_controller, input_action)
|
||||
|
||||
player_body.ground_control_velocity.y += dz_input_action.y * max_speed
|
||||
if strafe:
|
||||
player_body.ground_control_velocity.x += dz_input_action.x * max_speed
|
||||
|
||||
# Clamp ground control
|
||||
var length := player_body.ground_control_velocity.length()
|
||||
if length > max_speed:
|
||||
player_body.ground_control_velocity *= max_speed / length
|
||||
|
||||
|
||||
# This method verifies the movement provider has a valid configuration.
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := super()
|
||||
|
||||
# Check the controller node
|
||||
if not XRHelpers.get_xr_controller(self):
|
||||
warnings.append("This node must be within a branch of an XRController3D node")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
|
||||
|
||||
## Find the left [XRToolsMovementDirect] node.
|
||||
##
|
||||
## This function searches from the specified node for the left controller
|
||||
## [XRToolsMovementDirect] assuming the node is a sibling of the [XROrigin3D].
|
||||
static func find_left(node : Node) -> XRToolsMovementDirect:
|
||||
return XRTools.find_xr_child(
|
||||
XRHelpers.get_left_controller(node),
|
||||
"*",
|
||||
"XRToolsMovementDirect") as XRToolsMovementDirect
|
||||
|
||||
|
||||
## Find the right [XRToolsMovementDirect] node.
|
||||
##
|
||||
## This function searches from the specified node for the right controller
|
||||
## [XRToolsMovementDirect] assuming the node is a sibling of the [XROrigin3D].
|
||||
static func find_right(node : Node) -> XRToolsMovementDirect:
|
||||
return XRTools.find_xr_child(
|
||||
XRHelpers.get_right_controller(node),
|
||||
"*",
|
||||
"XRToolsMovementDirect") as XRToolsMovementDirect
|
||||
1
addons/godot-xr-tools/functions/movement_direct.gd.uid
Normal file
1
addons/godot-xr-tools/functions/movement_direct.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dc4hg8kq5ia5y
|
||||
6
addons/godot-xr-tools/functions/movement_direct.tscn
Normal file
6
addons/godot-xr-tools/functions/movement_direct.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://bl2nuu3qhlb5k"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dc4hg8kq5ia5y" path="res://addons/godot-xr-tools/functions/movement_direct.gd" id="1"]
|
||||
|
||||
[node name="MovementDirect" type="Node3D" groups=["movement_providers"]]
|
||||
script = ExtResource("1")
|
||||
229
addons/godot-xr-tools/functions/movement_flight.gd
Normal file
229
addons/godot-xr-tools/functions/movement_flight.gd
Normal file
@@ -0,0 +1,229 @@
|
||||
@tool
|
||||
class_name XRToolsMovementFlight
|
||||
extends XRToolsMovementProvider
|
||||
|
||||
|
||||
## XR Tools Movement Provider for Flying
|
||||
##
|
||||
## This script provides flying movement for the player. The control parameters
|
||||
## are intended to support a wide variety of flight mechanics.
|
||||
##
|
||||
## Pitch and Bearing input devices are selected which produce a "forwards"
|
||||
## reference frame. The player controls (forwards/backwards and
|
||||
## left/right) are applied in relation to this reference frame.
|
||||
##
|
||||
## The Speed Scale and Traction parameters allow primitive flight where
|
||||
## the player is in direct control of their speed (in the reference frame).
|
||||
## This produces an effect described as the "Mary Poppins Flying Umbrella".
|
||||
##
|
||||
## The Acceleration, Drag, and Guidance parameters allow for slightly more
|
||||
## realisitic flying where the player can accelerate in their reference
|
||||
## frame. The drag is applied against the global reference and can be used
|
||||
## to construct a terminal velocity.
|
||||
##
|
||||
## The Guidance property attempts to lerp the players velocity into flight
|
||||
## forwards direction as if the player had guide-fins or wings.
|
||||
##
|
||||
## The Exclusive property specifies whether flight is exclusive (no further
|
||||
## physics effects after flying) or whether additional effects such as
|
||||
## the default player gravity are applied.
|
||||
|
||||
|
||||
## Signal emitted when flight starts
|
||||
signal flight_started()
|
||||
|
||||
## Signal emitted when flight finishes
|
||||
signal flight_finished()
|
||||
|
||||
|
||||
## Enumeration of controller to use for flight
|
||||
enum FlightController {
|
||||
LEFT, ## Use left controller
|
||||
RIGHT, ## Use right controller
|
||||
}
|
||||
|
||||
## Enumeration of pitch control input
|
||||
enum FlightPitch {
|
||||
HEAD, ## Head controls pitch
|
||||
CONTROLLER, ## Controller controls pitch
|
||||
}
|
||||
|
||||
## Enumeration of bearing control input
|
||||
enum FlightBearing {
|
||||
HEAD, ## Head controls bearing
|
||||
CONTROLLER, ## Controller controls bearing
|
||||
BODY, ## Body controls bearing
|
||||
}
|
||||
|
||||
|
||||
## Movement provider order
|
||||
@export var order : int = 30
|
||||
|
||||
## Flight controller
|
||||
@export var controller : FlightController = FlightController.LEFT
|
||||
|
||||
## Flight toggle button
|
||||
@export var flight_button : String = "by_button"
|
||||
|
||||
## Flight pitch control
|
||||
@export var pitch : FlightPitch = FlightPitch.CONTROLLER
|
||||
|
||||
## Flight bearing control
|
||||
@export var bearing : FlightBearing = FlightBearing.CONTROLLER
|
||||
|
||||
## Flight speed from control
|
||||
@export var speed_scale : float = 5.0
|
||||
|
||||
## Flight traction pulling flight velocity towards the controlled speed
|
||||
@export var speed_traction : float = 3.0
|
||||
|
||||
## Flight acceleration from control
|
||||
@export var acceleration_scale : float = 0.0
|
||||
|
||||
## Flight drag
|
||||
@export var drag : float = 0.1
|
||||
|
||||
## Guidance effect (virtual fins/wings)
|
||||
@export var guidance : float = 0.0
|
||||
|
||||
## If true, flight movement is exclusive preventing further movement functions
|
||||
@export var exclusive : bool = true
|
||||
|
||||
|
||||
## Flight button state
|
||||
var _flight_button : bool = false
|
||||
|
||||
## Flight controller
|
||||
var _controller : XRController3D
|
||||
|
||||
|
||||
# Node references
|
||||
@onready var _camera := XRHelpers.get_xr_camera(self)
|
||||
@onready var _left_controller := XRHelpers.get_left_controller(self)
|
||||
@onready var _right_controller := XRHelpers.get_right_controller(self)
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsMovementFlight" or super(xr_name)
|
||||
|
||||
|
||||
func _ready():
|
||||
# In Godot 4 we must now manually call our super class ready function
|
||||
super()
|
||||
|
||||
# Get the flight controller
|
||||
if controller == FlightController.LEFT:
|
||||
_controller = _left_controller
|
||||
else:
|
||||
_controller = _right_controller
|
||||
|
||||
|
||||
# Process physics movement for flight
|
||||
func physics_movement(delta: float, player_body: XRToolsPlayerBody, disabled: bool):
|
||||
# Disable flying if requested, or if no controller
|
||||
if disabled or !enabled or !_controller.get_is_active():
|
||||
set_flying(false)
|
||||
return
|
||||
|
||||
# Detect press of flight button
|
||||
var old_flight_button = _flight_button
|
||||
_flight_button = _controller.is_button_pressed(flight_button)
|
||||
if _flight_button and !old_flight_button:
|
||||
set_flying(!is_active)
|
||||
|
||||
# Skip if not flying
|
||||
if !is_active:
|
||||
return
|
||||
|
||||
# Select the pitch vector
|
||||
var pitch_vector: Vector3
|
||||
if pitch == FlightPitch.HEAD:
|
||||
# Use the vertical part of the 'head' forwards vector
|
||||
pitch_vector = -_camera.transform.basis.z.y * player_body.up_player
|
||||
else:
|
||||
# Use the vertical part of the 'controller' forwards vector
|
||||
pitch_vector = -_controller.transform.basis.z.y * player_body.up_player
|
||||
|
||||
# Select the bearing vector
|
||||
var bearing_vector: Vector3
|
||||
if bearing == FlightBearing.HEAD:
|
||||
# Use the horizontal part of the 'head' forwards vector
|
||||
bearing_vector = -_camera.global_transform.basis.z \
|
||||
.slide(player_body.up_player)
|
||||
elif bearing == FlightBearing.CONTROLLER:
|
||||
# Use the horizontal part of the 'controller' forwards vector
|
||||
bearing_vector = -_controller.global_transform.basis.z \
|
||||
.slide(player_body.up_player)
|
||||
else:
|
||||
# Use the horizontal part of the 'body' forwards vector
|
||||
var left := _left_controller.global_transform.origin
|
||||
var right := _right_controller.global_transform.origin
|
||||
var left_to_right := right - left
|
||||
bearing_vector = left_to_right \
|
||||
.rotated(player_body.up_player, PI/2) \
|
||||
.slide(player_body.up_player)
|
||||
|
||||
# Construct the flight bearing
|
||||
var forwards := (bearing_vector.normalized() + pitch_vector).normalized()
|
||||
var side := forwards.cross(player_body.up_player)
|
||||
|
||||
# Construct the target velocity
|
||||
var joy_forwards := _controller.get_vector2("primary").y
|
||||
var joy_side := _controller.get_vector2("primary").x
|
||||
var heading := forwards * joy_forwards + side * joy_side
|
||||
|
||||
# Calculate the flight velocity
|
||||
var flight_velocity := player_body.velocity
|
||||
flight_velocity *= 1.0 - drag * delta
|
||||
flight_velocity = flight_velocity.lerp(heading * speed_scale, speed_traction * delta)
|
||||
flight_velocity += heading * acceleration_scale * delta
|
||||
|
||||
# Apply virtual guidance effect
|
||||
if guidance > 0.0:
|
||||
var velocity_forwards := forwards * flight_velocity.length()
|
||||
flight_velocity = flight_velocity.lerp(velocity_forwards, guidance * delta)
|
||||
|
||||
# If exclusive then perform the exclusive move-and-slide
|
||||
if exclusive:
|
||||
player_body.velocity = player_body.move_player(flight_velocity)
|
||||
return true
|
||||
|
||||
# Update velocity and return for additional effects
|
||||
player_body.velocity = flight_velocity
|
||||
return
|
||||
|
||||
|
||||
func set_flying(active: bool) -> void:
|
||||
# Skip if no change
|
||||
if active == is_active:
|
||||
return
|
||||
|
||||
# Update state
|
||||
is_active = active
|
||||
|
||||
# Handle state change
|
||||
if is_active:
|
||||
emit_signal("flight_started")
|
||||
else:
|
||||
emit_signal("flight_finished")
|
||||
|
||||
|
||||
# This method verifies the movement provider has a valid configuration.
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := super()
|
||||
|
||||
# Verify the camera
|
||||
if !XRHelpers.get_xr_camera(self):
|
||||
warnings.append("Unable to find XRCamera3D")
|
||||
|
||||
# Verify the left controller
|
||||
if !XRHelpers.get_left_controller(self):
|
||||
warnings.append("Unable to find left XRController3D node")
|
||||
|
||||
# Verify the right controller
|
||||
if !XRHelpers.get_right_controller(self):
|
||||
warnings.append("Unable to find left XRController3D node")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
1
addons/godot-xr-tools/functions/movement_flight.gd.uid
Normal file
1
addons/godot-xr-tools/functions/movement_flight.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://vyag4h2e7n0p
|
||||
6
addons/godot-xr-tools/functions/movement_flight.tscn
Normal file
6
addons/godot-xr-tools/functions/movement_flight.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://kyhaogt0a4q8"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://vyag4h2e7n0p" path="res://addons/godot-xr-tools/functions/movement_flight.gd" id="1"]
|
||||
|
||||
[node name="MovementFlight" type="Node3D" groups=["movement_providers"]]
|
||||
script = ExtResource("1")
|
||||
245
addons/godot-xr-tools/functions/movement_footstep.gd
Normal file
245
addons/godot-xr-tools/functions/movement_footstep.gd
Normal file
@@ -0,0 +1,245 @@
|
||||
@tool
|
||||
class_name XRToolsMovementFootstep
|
||||
extends XRToolsMovementProvider
|
||||
|
||||
|
||||
## XR Tools Movement Provider for Footsteps
|
||||
##
|
||||
## This movement provider detects walking on different surfaces.
|
||||
## It plays audio sounds associated with the surface the player is
|
||||
## currently walking on.
|
||||
|
||||
|
||||
## Signal emitted when a footstep is generated
|
||||
signal footstep(name)
|
||||
|
||||
|
||||
# Number of audio players to pool
|
||||
const AUDIO_POOL_SIZE := 3
|
||||
|
||||
|
||||
## Movement provider order
|
||||
@export var order : int = 1001
|
||||
|
||||
## Default XRToolsSurfaceAudioType when not overridden
|
||||
@export var default_surface_audio_type : XRToolsSurfaceAudioType
|
||||
|
||||
## Speed at which the player is considered walking
|
||||
@export var walk_speed := 0.4
|
||||
|
||||
## Step per meter by time
|
||||
@export var steps_per_meter = 1.0
|
||||
|
||||
|
||||
# step time
|
||||
var step_time = 0.0
|
||||
|
||||
# Last on_ground state of the player
|
||||
var _old_on_ground := true
|
||||
|
||||
# Node representing the location of the players foot
|
||||
var _foot_spatial : Node3D
|
||||
|
||||
# Pool of idle AudioStreamPlayer3D nodes
|
||||
var _audio_pool_idle : Array[AudioStreamPlayer3D]
|
||||
|
||||
# Last ground node
|
||||
var _ground_node : Node
|
||||
|
||||
# Surface audio type associated with last ground node
|
||||
var _ground_node_audio_type : XRToolsSurfaceAudioType
|
||||
|
||||
|
||||
## PlayerBody - Player Physics Body Script
|
||||
@onready var player_body := XRToolsPlayerBody.find_instance(self)
|
||||
|
||||
|
||||
# Add support for is_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsMovementFootstep" or super(xr_name)
|
||||
|
||||
|
||||
func _ready():
|
||||
# In Godot 4 we must now manually call our super class ready function
|
||||
super()
|
||||
|
||||
# Construct the foot spatial - we will move it around as the player moves.
|
||||
_foot_spatial = Node3D.new()
|
||||
_foot_spatial.name = "FootSpatial"
|
||||
add_child(_foot_spatial)
|
||||
|
||||
# Make the array of players in _audio_pool_idle
|
||||
for i in AUDIO_POOL_SIZE:
|
||||
var player = $PlayerSettings.duplicate()
|
||||
player.name = "PlayerCopy%d" % (i + 1)
|
||||
_foot_spatial.add_child(player)
|
||||
_audio_pool_idle.append(player)
|
||||
player.finished.connect(_on_player_finished.bind(player))
|
||||
|
||||
# Set as always active
|
||||
is_active = true
|
||||
|
||||
# Listen for the player jumping
|
||||
player_body.player_jumped.connect(_on_player_jumped)
|
||||
|
||||
|
||||
# This method checks for configuration issues.
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := super()
|
||||
|
||||
# Verify player settings node exists
|
||||
if not $PlayerSettings:
|
||||
warnings.append("Missing player settings node")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
|
||||
|
||||
func physics_movement(_delta: float, player_body: XRToolsPlayerBody, _disabled: bool):
|
||||
# Update the spatial location of the foot
|
||||
_update_foot_spatial()
|
||||
|
||||
# Update the ground audio information
|
||||
_update_ground_audio()
|
||||
|
||||
# Skip if footsteps have been disabled
|
||||
if not enabled:
|
||||
step_time = 0
|
||||
return
|
||||
|
||||
# Detect landing on ground
|
||||
if not _old_on_ground and player_body.on_ground:
|
||||
# Play the ground hit sound
|
||||
_play_ground_hit()
|
||||
|
||||
# Update the old on_ground state
|
||||
_old_on_ground = player_body.on_ground
|
||||
if not player_body.on_ground:
|
||||
step_time = 0 # Reset when not on ground
|
||||
return
|
||||
|
||||
# Handle slow/stopped
|
||||
if player_body.ground_control_velocity.length() < walk_speed:
|
||||
step_time = 0 # Reset when slow/stopped
|
||||
return
|
||||
|
||||
# Count up the step timer, and skip if not take a step yet
|
||||
step_time += _delta * player_body.ground_control_velocity.length()
|
||||
if step_time > steps_per_meter:
|
||||
_play_step_sound()
|
||||
step_time = 0
|
||||
|
||||
|
||||
# Update the foot spatial to be where the players foot is
|
||||
func _update_foot_spatial() -> void:
|
||||
# Project the players camera down to the XZ plane (real-world space)
|
||||
var local_foot := player_body.camera_node.position.slide(Vector3.UP)
|
||||
|
||||
# Move the foot_spatial to the local foot in the global origin space
|
||||
_foot_spatial.global_position = player_body.origin_node.global_transform * local_foot
|
||||
|
||||
|
||||
# Update the ground audio information
|
||||
func _update_ground_audio() -> void:
|
||||
# Skip if no change
|
||||
if player_body.ground_node == _ground_node:
|
||||
return
|
||||
|
||||
# Save the new ground node
|
||||
_ground_node = player_body.ground_node
|
||||
|
||||
# Handle no ground
|
||||
if not _ground_node:
|
||||
_ground_node_audio_type = null
|
||||
return
|
||||
|
||||
# Find the surface audio for the ground (if any)
|
||||
var ground_audio : XRToolsSurfaceAudio = XRTools.find_xr_child(
|
||||
_ground_node, "*", "XRToolsSurfaceAudio")
|
||||
if ground_audio:
|
||||
_ground_node_audio_type = ground_audio.surface_audio_type
|
||||
else:
|
||||
_ground_node_audio_type = default_surface_audio_type
|
||||
|
||||
|
||||
# Called when the player jumps
|
||||
func _on_player_jumped() -> void:
|
||||
# Skip if no jump sound
|
||||
if not _ground_node_audio_type:
|
||||
return
|
||||
|
||||
# Play the jump sound
|
||||
_play_sound(
|
||||
_ground_node_audio_type.name,
|
||||
_ground_node_audio_type.jump_sound)
|
||||
|
||||
|
||||
# Play the hit sound made when the player lands on the ground
|
||||
func _play_ground_hit() -> void:
|
||||
# Skip if no hit sound
|
||||
if not _ground_node_audio_type:
|
||||
return
|
||||
|
||||
# Play the hit sound
|
||||
_play_sound(
|
||||
_ground_node_audio_type.name,
|
||||
_ground_node_audio_type.hit_sound)
|
||||
|
||||
|
||||
# Play a step sound for the current ground
|
||||
func _play_step_sound() -> void:
|
||||
# Skip if no walk audio
|
||||
if not _ground_node_audio_type or _ground_node_audio_type.walk_sounds.size() == 0:
|
||||
return
|
||||
|
||||
# Pick the sound index
|
||||
var idx := randi() % _ground_node_audio_type.walk_sounds.size()
|
||||
|
||||
# Pick the playback pitck
|
||||
var pitch := randf_range(
|
||||
_ground_node_audio_type.walk_pitch_minimum,
|
||||
_ground_node_audio_type.walk_pitch_maximum)
|
||||
|
||||
# Play the walk sound
|
||||
_play_sound(
|
||||
_ground_node_audio_type.name,
|
||||
_ground_node_audio_type.walk_sounds[idx],
|
||||
pitch)
|
||||
|
||||
|
||||
# Play the specified audio stream at the requested pitch using an
|
||||
# AudioStreamPlayer3D in the idle pool of players.
|
||||
func _play_sound(name : String, stream : AudioStream, pitch : float = 1.0) -> void:
|
||||
# Skip if no stream provided
|
||||
if not stream:
|
||||
return
|
||||
|
||||
# Emit the footstep signal
|
||||
footstep.emit(name)
|
||||
|
||||
# Verify we have an audio player
|
||||
if _audio_pool_idle.size() == 0:
|
||||
push_warning("XRToolsMovementFootstep idle audio pool empty")
|
||||
return
|
||||
|
||||
# Play the sound
|
||||
var player : AudioStreamPlayer3D = _audio_pool_idle.pop_front()
|
||||
player.stream = stream
|
||||
player.pitch_scale = pitch
|
||||
player.play()
|
||||
|
||||
|
||||
# Called when an AudioStreamPlayer3D in our pool finishes playing its sound
|
||||
func _on_player_finished(player : AudioStreamPlayer3D) -> void:
|
||||
_audio_pool_idle.append(player)
|
||||
|
||||
|
||||
## Find an [XRToolsMovementFootstep] node.
|
||||
##
|
||||
## This function searches from the specified node for an [XRToolsMovementFootstep]
|
||||
## assuming the node is a sibling of the body under an [ARVROrigin].
|
||||
static func find_instance(node: Node) -> XRToolsMovementFootstep:
|
||||
return XRTools.find_xr_child(
|
||||
XRHelpers.get_xr_origin(node),
|
||||
"*",
|
||||
"XRToolsMovementFootstep") as XRToolsMovementFootstep
|
||||
1
addons/godot-xr-tools/functions/movement_footstep.gd.uid
Normal file
1
addons/godot-xr-tools/functions/movement_footstep.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://clpt3lske02eh
|
||||
8
addons/godot-xr-tools/functions/movement_footstep.tscn
Normal file
8
addons/godot-xr-tools/functions/movement_footstep.tscn
Normal file
@@ -0,0 +1,8 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://0xlsitpu17r1"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://clpt3lske02eh" path="res://addons/godot-xr-tools/functions/movement_footstep.gd" id="1"]
|
||||
|
||||
[node name="MovementFootstep" type="Node3D" groups=["movement_providers"]]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="PlayerSettings" type="AudioStreamPlayer3D" parent="."]
|
||||
236
addons/godot-xr-tools/functions/movement_glide.gd
Normal file
236
addons/godot-xr-tools/functions/movement_glide.gd
Normal file
@@ -0,0 +1,236 @@
|
||||
@tool
|
||||
class_name XRToolsMovementGlide
|
||||
extends XRToolsMovementProvider
|
||||
|
||||
|
||||
## XR Tools Movement Provider for Gliding
|
||||
##
|
||||
## This script provides glide mechanics for the player. This script works
|
||||
## with the [XRToolsPlayerBody] attached to the players [XROrigin3D].
|
||||
##
|
||||
## The player enables flying by moving the controllers apart further than
|
||||
## 'glide_detect_distance'.
|
||||
##
|
||||
## When gliding, the players fall speed will slew to 'glide_fall_speed' and
|
||||
## the velocity will slew to 'glide_forward_speed' in the direction the
|
||||
## player is facing.
|
||||
##
|
||||
## Gliding is an exclusive motion operation, and so gliding should be ordered
|
||||
## after any Direct movement providers responsible for turning.
|
||||
|
||||
|
||||
## Signal invoked when the player starts gliding
|
||||
signal player_glide_start
|
||||
|
||||
## Signal invoked when the player ends gliding
|
||||
signal player_glide_end
|
||||
|
||||
## Signal invoked when the player flaps
|
||||
signal player_flapped
|
||||
|
||||
## Movement provider order
|
||||
@export var order : int = 35
|
||||
|
||||
## Controller separation distance to register as glide
|
||||
@export var glide_detect_distance : float = 1.0
|
||||
|
||||
## Minimum falling speed to be considered gliding
|
||||
@export var glide_min_fall_speed : float = -1.5
|
||||
|
||||
## Glide falling speed
|
||||
@export var glide_fall_speed : float = -2.0
|
||||
|
||||
## Glide forward speed
|
||||
@export var glide_forward_speed : float = 12.0
|
||||
|
||||
## Slew rate to transition to gliding
|
||||
@export var horizontal_slew_rate : float = 1.0
|
||||
|
||||
## Slew rate to transition to gliding
|
||||
@export var vertical_slew_rate : float = 2.0
|
||||
|
||||
## glide rotate with roll angle
|
||||
@export var turn_with_roll : bool = false
|
||||
|
||||
## Smooth turn speed in radians per second
|
||||
@export var roll_turn_speed : float = 1
|
||||
|
||||
## Add vertical impulse by flapping controllers
|
||||
@export var wings_impulse : bool = false
|
||||
|
||||
## Minimum velocity for flapping
|
||||
@export var flap_min_speed : float = 0.3
|
||||
|
||||
## Flapping force multiplier
|
||||
@export var wings_force : float = 1.0
|
||||
|
||||
## Minimum distance from controllers to ARVRCamera to rearm flaps.
|
||||
## if set to 0, you need to reach head level with hands to rearm flaps
|
||||
@export var rearm_distance_offset : float = 0.2
|
||||
|
||||
|
||||
## Flap activated (when both controllers are near the ARVRCamera height)
|
||||
var flap_armed : bool = false
|
||||
|
||||
## Last controllers position to calculate flapping velocity
|
||||
var last_local_left_position : Vector3
|
||||
var last_local_right_position : Vector3
|
||||
|
||||
# True if the controller positions are valid
|
||||
var _has_controller_positions : bool = false
|
||||
|
||||
|
||||
# Left controller
|
||||
@onready var _left_controller := XRHelpers.get_left_controller(self)
|
||||
|
||||
# Right controller
|
||||
@onready var _right_controller := XRHelpers.get_right_controller(self)
|
||||
|
||||
# ARVRCamera
|
||||
@onready var _camera_node := XRHelpers.get_xr_camera(self)
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsMovementGlide" or super(xr_name)
|
||||
|
||||
|
||||
func physics_movement(delta: float, player_body: XRToolsPlayerBody, disabled: bool):
|
||||
# Skip if disabled or either controller is off
|
||||
if disabled or !enabled or \
|
||||
!_left_controller.get_is_active() or \
|
||||
!_right_controller.get_is_active():
|
||||
_set_gliding(false)
|
||||
return
|
||||
|
||||
# If on the ground, then not gliding
|
||||
if player_body.on_ground:
|
||||
_set_gliding(false)
|
||||
return
|
||||
|
||||
# Get the controller left and right global horizontal positions
|
||||
var left_position := _left_controller.global_transform.origin
|
||||
var right_position := _right_controller.global_transform.origin
|
||||
|
||||
# Set default wings impulse to zero
|
||||
var wings_impulse_velocity := 0.0
|
||||
|
||||
# If wings impulse is active, calculate flapping impulse
|
||||
if wings_impulse:
|
||||
# Check controllers position relative to head
|
||||
var cam_local_y := _camera_node.position.y
|
||||
var left_hand_over_head = cam_local_y < _left_controller.position.y + rearm_distance_offset
|
||||
var right_hand_over_head = cam_local_y < _right_controller.position.y + rearm_distance_offset
|
||||
if left_hand_over_head && right_hand_over_head:
|
||||
flap_armed = true
|
||||
|
||||
if flap_armed:
|
||||
# Get controller local positions
|
||||
var local_left_position := _left_controller.position
|
||||
var local_right_position := _right_controller.position
|
||||
|
||||
# Store last frame controller positions for the first step
|
||||
if not _has_controller_positions:
|
||||
_has_controller_positions = true
|
||||
last_local_left_position = local_left_position
|
||||
last_local_right_position = local_right_position
|
||||
|
||||
# Calculate controllers velocity only when flapping downwards
|
||||
var left_wing_velocity = 0.0
|
||||
var right_wing_velocity = 0.0
|
||||
if local_left_position.y < last_local_left_position.y:
|
||||
left_wing_velocity = local_left_position.distance_to(last_local_left_position) / delta
|
||||
if local_right_position.y < last_local_right_position.y:
|
||||
right_wing_velocity = local_right_position.distance_to(last_local_right_position) / delta
|
||||
|
||||
# Calculate wings impulse
|
||||
if left_wing_velocity > flap_min_speed && right_wing_velocity > flap_min_speed:
|
||||
wings_impulse_velocity = (left_wing_velocity + right_wing_velocity) / 2
|
||||
wings_impulse_velocity = wings_impulse_velocity * wings_force * delta * 50
|
||||
emit_signal("player_flapped")
|
||||
flap_armed = false
|
||||
|
||||
# Store controller position for next frame
|
||||
last_local_left_position = local_left_position
|
||||
last_local_right_position = local_right_position
|
||||
|
||||
# Calculate global left to right controller vector
|
||||
var left_to_right := right_position - left_position
|
||||
|
||||
if turn_with_roll:
|
||||
var angle = -left_to_right.dot(player_body.up_player)
|
||||
player_body.rotate_player(roll_turn_speed * delta * angle)
|
||||
|
||||
# If not falling, then not gliding
|
||||
var vertical_velocity := player_body.velocity.dot(player_body.up_gravity)
|
||||
vertical_velocity += wings_impulse_velocity
|
||||
if vertical_velocity >= glide_min_fall_speed && wings_impulse_velocity == 0.0:
|
||||
_set_gliding(false)
|
||||
return
|
||||
|
||||
# Set gliding based on hand separation
|
||||
var separation := left_to_right.length() / XRServer.world_scale
|
||||
_set_gliding(separation >= glide_detect_distance)
|
||||
|
||||
# Skip if not gliding
|
||||
if !is_active:
|
||||
return
|
||||
|
||||
# Lerp the vertical velocity to glide_fall_speed
|
||||
vertical_velocity = lerp(vertical_velocity, glide_fall_speed, vertical_slew_rate * delta)
|
||||
|
||||
# Lerp the horizontal velocity towards forward_speed
|
||||
var horizontal_velocity := player_body.velocity.slide(player_body.up_gravity)
|
||||
var dir_forward := left_to_right \
|
||||
.rotated(player_body.up_gravity, PI/2) \
|
||||
.slide(player_body.up_gravity) \
|
||||
.normalized()
|
||||
var forward_velocity := dir_forward * glide_forward_speed
|
||||
horizontal_velocity = horizontal_velocity.lerp(forward_velocity, horizontal_slew_rate * delta)
|
||||
|
||||
# Perform the glide
|
||||
var glide_velocity := horizontal_velocity + vertical_velocity * player_body.up_gravity
|
||||
player_body.velocity = player_body.move_player(glide_velocity)
|
||||
|
||||
# Report exclusive motion performed (to bypass gravity)
|
||||
return true
|
||||
|
||||
|
||||
# Set the gliding state and fire any signals
|
||||
func _set_gliding(active: bool) -> void:
|
||||
# Skip if no change
|
||||
if active == is_active:
|
||||
return
|
||||
|
||||
# Update the is_gliding flag
|
||||
is_active = active
|
||||
|
||||
# Report transition
|
||||
if is_active:
|
||||
emit_signal("player_glide_start")
|
||||
else:
|
||||
emit_signal("player_glide_end")
|
||||
|
||||
|
||||
# This method verifies the movement provider has a valid configuration.
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := super()
|
||||
|
||||
# Verify the left controller
|
||||
if !XRHelpers.get_left_controller(self):
|
||||
warnings.append("Unable to find left XRController3D node")
|
||||
|
||||
# Verify the right controller
|
||||
if !XRHelpers.get_right_controller(self):
|
||||
warnings.append("Unable to find right XRController3D node")
|
||||
|
||||
# Check glide parameters
|
||||
if glide_min_fall_speed > 0:
|
||||
warnings.append("Glide minimum fall speed must be zero or less")
|
||||
if glide_fall_speed > 0:
|
||||
warnings.append("Glide fall speed must be zero or less")
|
||||
if glide_min_fall_speed < glide_fall_speed:
|
||||
warnings.append("Glide fall speed must be faster than minimum fall speed")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
1
addons/godot-xr-tools/functions/movement_glide.gd.uid
Normal file
1
addons/godot-xr-tools/functions/movement_glide.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://5pticrbivdx6
|
||||
6
addons/godot-xr-tools/functions/movement_glide.tscn
Normal file
6
addons/godot-xr-tools/functions/movement_glide.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://cvokcudrffkgc"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://5pticrbivdx6" path="res://addons/godot-xr-tools/functions/movement_glide.gd" id="1"]
|
||||
|
||||
[node name="MovementGlide" type="Node3D" groups=["movement_providers"]]
|
||||
script = ExtResource("1")
|
||||
288
addons/godot-xr-tools/functions/movement_grapple.gd
Normal file
288
addons/godot-xr-tools/functions/movement_grapple.gd
Normal file
@@ -0,0 +1,288 @@
|
||||
@tool
|
||||
class_name XRToolsMovementGrapple
|
||||
extends XRToolsMovementProvider
|
||||
|
||||
|
||||
## XR Tools Movement Provider for Grapple Movement
|
||||
##
|
||||
## This script provide simple grapple based movement - "bat hook" style
|
||||
## where the player flings a rope to the target and swings on it.
|
||||
## This script works with the [XRToolsPlayerBody] attached to the players
|
||||
## [XROrigin3D].
|
||||
|
||||
|
||||
## Signal emitted when grapple starts
|
||||
signal grapple_started()
|
||||
|
||||
## Signal emitted when grapple finishes
|
||||
signal grapple_finished()
|
||||
|
||||
|
||||
## Grapple state
|
||||
enum GrappleState {
|
||||
IDLE, ## Grapple is idle
|
||||
FIRED, ## Grapple is fired
|
||||
WINCHING, ## Grapple is winching
|
||||
}
|
||||
|
||||
|
||||
# Default grapple collision mask of 1-5 (world)
|
||||
const DEFAULT_COLLISION_MASK := 0b0000_0000_0000_0000_0000_0000_0001_1111
|
||||
|
||||
# Default grapple enable mask of 5:grapple-target
|
||||
const DEFAULT_ENABLE_MASK := 0b0000_0000_0000_0000_0000_0000_0001_0000
|
||||
|
||||
|
||||
## Movement provider order
|
||||
@export var order : int = 20
|
||||
|
||||
## Grapple length - use to adjust maximum distance for possible grapple hooking.
|
||||
@export var grapple_length : float = 15.0
|
||||
|
||||
## Grapple collision mask
|
||||
@export_flags_3d_physics var grapple_collision_mask : int = DEFAULT_COLLISION_MASK:
|
||||
set = _set_grapple_collision_mask
|
||||
|
||||
## Grapple enable mask
|
||||
@export_flags_3d_physics var grapple_enable_mask : int = DEFAULT_ENABLE_MASK
|
||||
|
||||
## Impulse speed applied to the player on first grapple
|
||||
@export var impulse_speed : float = 10.0
|
||||
|
||||
## Winch speed applied to the player while the grapple is held
|
||||
@export var winch_speed : float = 2.0
|
||||
|
||||
## Probably need to add export variables for line size, maybe line material at
|
||||
## some point so dev does not need to make children editable to do this.
|
||||
## For now, right click on grapple node and make children editable to edit these
|
||||
## facets.
|
||||
@export var rope_width : float = 0.02
|
||||
|
||||
## Air friction while grappling
|
||||
@export var friction : float = 0.1
|
||||
|
||||
## Grapple button (triggers grappling movement). Be sure this button does not
|
||||
## conflict with other functions.
|
||||
@export var grapple_button_action : String = "trigger_click"
|
||||
|
||||
## Hand offset to apply based on our controller pose
|
||||
## You can use auto if you're using the default aim_pose or grip_pose poses.
|
||||
@export_enum("auto", "aim", "grip", "palm", "disable") var hand_offset_mode : int = 0:
|
||||
set(value):
|
||||
hand_offset_mode = value
|
||||
notify_property_list_changed()
|
||||
if is_inside_tree():
|
||||
_update_transform()
|
||||
|
||||
|
||||
# Hook related variables
|
||||
var hook_object : Node3D = null
|
||||
var hook_local := Vector3(0,0,0)
|
||||
var hook_point := Vector3(0,0,0)
|
||||
|
||||
# Grapple button state
|
||||
var _grapple_button := false
|
||||
|
||||
# Get Controller node - consider way to universalize this if user wanted to
|
||||
# attach this to a gun instead of player's hand. Could consider variable to
|
||||
# select controller instead.
|
||||
var _controller : XRController3D
|
||||
|
||||
# Keep track of our tracker and pose
|
||||
var _controller_tracker_and_pose : String = ""
|
||||
|
||||
# Get line creation nodes
|
||||
@onready var _line_helper : Node3D = $LineHelper
|
||||
@onready var _line : CSGCylinder3D = $LineHelper/Line
|
||||
|
||||
# Get Raycast node
|
||||
@onready var _grapple_raycast : RayCast3D = $Grapple_RayCast
|
||||
|
||||
# Get Grapple Target Node
|
||||
@onready var _grapple_target : Node3D = $Grapple_Target
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsMovementGrapple" or super(xr_name)
|
||||
|
||||
|
||||
# Function run when node is added to scene
|
||||
func _ready():
|
||||
# In Godot 4 we must now manually call our super class ready function
|
||||
super()
|
||||
|
||||
# Skip if running in the editor
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# Ensure grapple length is valid
|
||||
var min_hook_length := 1.5 * XRServer.world_scale
|
||||
if grapple_length < min_hook_length:
|
||||
grapple_length = min_hook_length
|
||||
|
||||
# Set ray-cast
|
||||
_grapple_raycast.target_position = Vector3(0, 0, -grapple_length) * XRServer.world_scale
|
||||
_grapple_raycast.collision_mask = grapple_collision_mask
|
||||
|
||||
# Deal with line
|
||||
_line.radius = rope_width
|
||||
_line.hide()
|
||||
|
||||
|
||||
func _enter_tree():
|
||||
_controller = XRHelpers.get_xr_controller(self)
|
||||
|
||||
_update_transform()
|
||||
|
||||
|
||||
func _exit_tree():
|
||||
_controller = null
|
||||
|
||||
|
||||
# Check property config
|
||||
func _validate_property(property):
|
||||
if hand_offset_mode != 4 and (property.name == "position" or property.name == "rotation" or property.name == "scale" or property.name == "rotation_edit_mode" or property.name == "rotation_order"):
|
||||
# We control these, don't let the user set them.
|
||||
property.usage = PROPERTY_USAGE_NONE
|
||||
|
||||
|
||||
# Update our transform so we are positioned on our palm
|
||||
func _update_transform() -> void:
|
||||
if hand_offset_mode != 4:
|
||||
transform = XRTools.get_palm_offset(hand_offset_mode, _controller)
|
||||
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(_delta):
|
||||
# If we have a controller, make sure our hand transform is updated when needed.
|
||||
if _controller:
|
||||
var tracker_and_pose = _controller.tracker + "." + _controller.pose
|
||||
if _controller_tracker_and_pose != tracker_and_pose:
|
||||
_controller_tracker_and_pose = tracker_and_pose
|
||||
if hand_offset_mode == 0:
|
||||
_update_transform()
|
||||
|
||||
|
||||
# Update the grappling line and target
|
||||
func _physics_process(_delta : float):
|
||||
# Skip if running in the editor
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# If pointing grappler at target then show the target
|
||||
if enabled and not is_active and _is_raycast_valid():
|
||||
_grapple_target.global_transform.origin = _grapple_raycast.get_collision_point()
|
||||
_grapple_target.global_transform = _grapple_target.global_transform.orthonormalized()
|
||||
_grapple_target.visible = true
|
||||
else:
|
||||
_grapple_target.visible = false
|
||||
|
||||
# If actively grappling then update and show the grappling line
|
||||
if is_active:
|
||||
var line_length := (hook_point - _controller.global_transform.origin).length()
|
||||
_line_helper.look_at(hook_point, Vector3.UP)
|
||||
_line.height = line_length
|
||||
_line.position.z = line_length / -2
|
||||
_line.visible = true
|
||||
else:
|
||||
_line.visible = false
|
||||
|
||||
|
||||
# Perform grapple movement
|
||||
func physics_movement(delta: float, player_body: XRToolsPlayerBody, disabled: bool):
|
||||
# Disable if requested
|
||||
if disabled or !enabled or !_controller.get_is_active():
|
||||
_set_grappling(false)
|
||||
return
|
||||
|
||||
# Update grapple button
|
||||
var old_grapple_button := _grapple_button
|
||||
_grapple_button = _controller.is_button_pressed(grapple_button_action)
|
||||
|
||||
# Enable/disable grappling
|
||||
var do_impulse := false
|
||||
if is_active and !_grapple_button:
|
||||
_set_grappling(false)
|
||||
elif _grapple_button and !old_grapple_button and _is_raycast_valid():
|
||||
hook_object = _grapple_raycast.get_collider()
|
||||
hook_point = _grapple_raycast.get_collision_point()
|
||||
hook_local = hook_point * hook_object.global_transform
|
||||
do_impulse = true
|
||||
_set_grappling(true)
|
||||
|
||||
# Skip if not grappling
|
||||
if !is_active:
|
||||
return
|
||||
|
||||
# Get hook direction
|
||||
hook_point = hook_object.global_transform * hook_local
|
||||
var hook_vector := hook_point - _controller.global_transform.origin
|
||||
var hook_length := hook_vector.length()
|
||||
var hook_direction := hook_vector / hook_length
|
||||
|
||||
# Apply gravity
|
||||
player_body.velocity += player_body.gravity * delta
|
||||
|
||||
# Select the grapple speed
|
||||
var speed := impulse_speed if do_impulse else winch_speed
|
||||
if hook_length < 1.0:
|
||||
speed = 0.0
|
||||
|
||||
# Ensure velocity is at least winch_speed towards hook
|
||||
var vdot = player_body.velocity.dot(hook_direction)
|
||||
if vdot < speed:
|
||||
player_body.velocity += hook_direction * (speed - vdot)
|
||||
|
||||
# Scale down velocity
|
||||
player_body.velocity *= 1.0 - friction * delta
|
||||
|
||||
# Perform exclusive movement as we have dealt with gravity
|
||||
player_body.velocity = player_body.move_player(player_body.velocity)
|
||||
return true
|
||||
|
||||
|
||||
# Called when the grapple collision mask has been modified
|
||||
func _set_grapple_collision_mask(new_value: int) -> void:
|
||||
grapple_collision_mask = new_value
|
||||
if is_inside_tree() and _grapple_raycast:
|
||||
_grapple_raycast.collision_mask = new_value
|
||||
|
||||
|
||||
# Set the grappling state and fire any signals
|
||||
func _set_grappling(active: bool) -> void:
|
||||
# Skip if no change
|
||||
if active == is_active:
|
||||
return
|
||||
|
||||
# Update the is_active flag
|
||||
is_active = active
|
||||
|
||||
# Report transition
|
||||
if is_active:
|
||||
emit_signal("grapple_started")
|
||||
else:
|
||||
emit_signal("grapple_finished")
|
||||
|
||||
|
||||
# Test if the raycast is striking a valid target
|
||||
func _is_raycast_valid() -> bool:
|
||||
# Test if the raycast hit a collider
|
||||
var target = _grapple_raycast.get_collider()
|
||||
if not is_instance_valid(target):
|
||||
return false
|
||||
|
||||
# Check collider layer
|
||||
return true if target.collision_layer & grapple_enable_mask else false
|
||||
|
||||
|
||||
# This method verifies the movement provider has a valid configuration.
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := super()
|
||||
|
||||
# Check the controller node
|
||||
if !XRHelpers.get_xr_controller(self):
|
||||
warnings.append("This node must be within a branch of an XRController3D node")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
1
addons/godot-xr-tools/functions/movement_grapple.gd.uid
Normal file
1
addons/godot-xr-tools/functions/movement_grapple.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://r8s1ep2dlajf
|
||||
28
addons/godot-xr-tools/functions/movement_grapple.tscn
Normal file
28
addons/godot-xr-tools/functions/movement_grapple.tscn
Normal file
@@ -0,0 +1,28 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://c78tjrtiyqna8"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://r8s1ep2dlajf" path="res://addons/godot-xr-tools/functions/movement_grapple.gd" id="1"]
|
||||
[ext_resource type="Material" path="res://addons/godot-xr-tools/materials/pointer.tres" id="2_n6olo"]
|
||||
|
||||
[sub_resource type="BoxMesh" id="1"]
|
||||
resource_local_to_scene = true
|
||||
size = Vector3(0.05, 0.05, 0.05)
|
||||
subdivide_depth = 20
|
||||
|
||||
[node name="MovementGrapple" type="Node3D" groups=["movement_providers"]]
|
||||
script = ExtResource("1")
|
||||
|
||||
[node name="Grapple_RayCast" type="RayCast3D" parent="."]
|
||||
collision_mask = 3
|
||||
debug_shape_custom_color = Color(0.862745, 0.278431, 0.278431, 1)
|
||||
debug_shape_thickness = 1
|
||||
|
||||
[node name="Grapple_Target" type="MeshInstance3D" parent="."]
|
||||
visible = false
|
||||
mesh = SubResource("1")
|
||||
surface_material_override/0 = ExtResource("2_n6olo")
|
||||
|
||||
[node name="LineHelper" type="Node3D" parent="."]
|
||||
|
||||
[node name="Line" type="CSGCylinder3D" parent="LineHelper"]
|
||||
transform = Transform3D(1.91069e-15, 4.37114e-08, 1, 1, -4.37114e-08, 0, 4.37114e-08, 1, -4.37114e-08, 0, 0, 0)
|
||||
radius = 0.02
|
||||
156
addons/godot-xr-tools/functions/movement_jog.gd
Normal file
156
addons/godot-xr-tools/functions/movement_jog.gd
Normal file
@@ -0,0 +1,156 @@
|
||||
@tool
|
||||
class_name XRToolsMovementJog
|
||||
extends XRToolsMovementProvider
|
||||
|
||||
|
||||
## XR Tools Movement Provider for Jog Movement
|
||||
##
|
||||
## This script provides jog-in-place movement for the player. This script
|
||||
## works with the [XRToolsPlayerBody] attached to the players [XROrigin3D].
|
||||
##
|
||||
## The implementation uses filtering of the controller Y velocities to measure
|
||||
## the approximate frequency of jog arm-swings, and uses that to
|
||||
## switch between stopped, slow, and fast movement speeds.
|
||||
|
||||
|
||||
## Speed mode enumeration
|
||||
enum SpeedMode {
|
||||
STOPPED, ## Not jogging
|
||||
SLOW, ## Jogging slowly
|
||||
FAST ## Jogging fast
|
||||
}
|
||||
|
||||
|
||||
## Jog arm-swing frequency in Hz to trigger slow movement
|
||||
const JOG_SLOW_FREQ := 3.5
|
||||
|
||||
## Jog arm-swing frequency in Hz to trigger fast movement
|
||||
const JOG_FAST_FREQ := 5.5
|
||||
|
||||
|
||||
## Movement provider order
|
||||
@export var order : int = 10
|
||||
|
||||
## Slow jogging speed in meters-per-second
|
||||
@export var slow_speed : float = 1.0
|
||||
|
||||
## Fast jogging speed in meters-per-second
|
||||
@export var fast_speed : float = 3.0
|
||||
|
||||
|
||||
# Jog arm-swing "stroke" detector "confidence-hat" signal
|
||||
var _conf_hat := 0.0
|
||||
|
||||
# Current jog arm-swing "stroke" duration
|
||||
var _current_stroke := 0.0
|
||||
|
||||
# Last jog arm-swing "stroke" total duration
|
||||
var _last_stroke := 0.0
|
||||
|
||||
# Current jog-speed mode
|
||||
var _speed_mode := SpeedMode.STOPPED
|
||||
|
||||
|
||||
# Left controller
|
||||
@onready var _left_controller := XRHelpers.get_left_controller(self)
|
||||
|
||||
# Right controller
|
||||
@onready var _right_controller := XRHelpers.get_right_controller(self)
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsMovementJog" or super(xr_name)
|
||||
|
||||
|
||||
# Perform jump movement
|
||||
func physics_movement(delta: float, player_body: XRToolsPlayerBody, _disabled: bool):
|
||||
# Skip if the either controller is inactive
|
||||
if !_left_controller.get_is_active() or !_right_controller.get_is_active():
|
||||
_speed_mode = SpeedMode.STOPPED
|
||||
return
|
||||
|
||||
# Get the arm-swing stroke frequency in Hz
|
||||
var freq := _get_stroke_frequency(delta)
|
||||
|
||||
# Transition between stopped/slow/fast speed-modes based on thresholds.
|
||||
# This thresholding has some hysteresis to make speed changes smoother.
|
||||
if freq == 0:
|
||||
_speed_mode = SpeedMode.STOPPED
|
||||
elif freq < JOG_SLOW_FREQ:
|
||||
_speed_mode = min(_speed_mode, SpeedMode.SLOW)
|
||||
elif freq < JOG_FAST_FREQ:
|
||||
_speed_mode = max(_speed_mode, SpeedMode.SLOW)
|
||||
else:
|
||||
_speed_mode = SpeedMode.FAST
|
||||
|
||||
# Pick the speed in meters-per-second based on the current speed-mode.
|
||||
var speed := 0.0
|
||||
if _speed_mode == SpeedMode.SLOW:
|
||||
speed = slow_speed
|
||||
elif _speed_mode == SpeedMode.FAST:
|
||||
speed = fast_speed
|
||||
|
||||
# Contribute to the player body speed - with clamping to the maximum speed
|
||||
player_body.ground_control_velocity.y += speed
|
||||
var length := player_body.ground_control_velocity.length()
|
||||
if length > fast_speed:
|
||||
player_body.ground_control_velocity *= fast_speed / length
|
||||
|
||||
|
||||
# Get the frequency of the last arm-swing "stroke" in Hz.
|
||||
func _get_stroke_frequency(delta : float) -> float:
|
||||
# Get the controller velocities
|
||||
var vl := _left_controller.get_pose().linear_velocity.y
|
||||
var vr := _right_controller.get_pose().linear_velocity.y
|
||||
|
||||
# Calculate the arm-swing "stroke" confidence. This is done by multiplying
|
||||
# the left and right controller vertical velocities. As these velocities
|
||||
# are highly anti-correlated while "jogging" the result is a confidence
|
||||
# signal with a high "peak" on every jog "stroke".
|
||||
var conf := vl * -vr
|
||||
|
||||
# Test for the confidence valley between strokes. This is used to signal
|
||||
# when to measure the duration between strokes.
|
||||
var valley := conf < _conf_hat
|
||||
|
||||
# Update confidence-hat. The confidence-hat signal has a fast-rise and
|
||||
# slow-decay. Rising with each jog arm-swing "stroke" and then taking time
|
||||
# to decay. The magnitude of the "confidence-hat" can be used as a good
|
||||
# indicator of when the user is jogging, and the difference between the
|
||||
# "confidence" and "confidence-hat" signals can be used to identify the
|
||||
# duration of a jog arm-swing "stroke".
|
||||
if valley:
|
||||
# Gently decay when in the confidence valley.
|
||||
_conf_hat = lerpf(_conf_hat, 0.0, delta * 2)
|
||||
else:
|
||||
# Quickly ramp confidence-hat to confidence
|
||||
_conf_hat = lerpf(_conf_hat, conf, delta * 20)
|
||||
|
||||
# If the "confidence-hat" signal is too low then the user is not jogging.
|
||||
# The stroke date-data is cleared and a stroke frequency of 0Hz is returned.
|
||||
if _conf_hat < 0.5:
|
||||
_current_stroke = 0.0
|
||||
_last_stroke = 0.0
|
||||
return 0.0
|
||||
|
||||
# Track the jog arm-swing "stroke" duration.
|
||||
if valley:
|
||||
# In the valley between jog arm-swing "strokes"
|
||||
_current_stroke += delta
|
||||
elif _current_stroke > 0.1:
|
||||
# Save the measured jog arm-swing "stroke" duration.
|
||||
_last_stroke = _current_stroke
|
||||
_current_stroke = 0.0
|
||||
|
||||
# If no previous jog arm-swing "stroke" duration to report, so return 0Hz.
|
||||
if _last_stroke < 0.1:
|
||||
return 0.0
|
||||
|
||||
# If the current jog arm-swing "stroke" is taking longer (slower) than 2Hz
|
||||
# then truncate to 0Hz.
|
||||
if _current_stroke > 0.75:
|
||||
return 0.0
|
||||
|
||||
# Return the last jog arm-swing "stroke" in Hz.
|
||||
return 1.0 / _last_stroke
|
||||
1
addons/godot-xr-tools/functions/movement_jog.gd.uid
Normal file
1
addons/godot-xr-tools/functions/movement_jog.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://f5tfo4ylkuhc
|
||||
6
addons/godot-xr-tools/functions/movement_jog.tscn
Normal file
6
addons/godot-xr-tools/functions/movement_jog.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://chcuj3jysipk8"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://f5tfo4ylkuhc" path="res://addons/godot-xr-tools/functions/movement_jog.gd" id="1_k4cao"]
|
||||
|
||||
[node name="MovementJog" type="Node3D" groups=["movement_providers"]]
|
||||
script = ExtResource("1_k4cao")
|
||||
62
addons/godot-xr-tools/functions/movement_jump.gd
Normal file
62
addons/godot-xr-tools/functions/movement_jump.gd
Normal file
@@ -0,0 +1,62 @@
|
||||
@tool
|
||||
class_name XRToolsMovementJump
|
||||
extends XRToolsMovementProvider
|
||||
|
||||
|
||||
## XR Tools Movement Provider for Jumping
|
||||
##
|
||||
## This script provides jumping mechanics for the player. This script works
|
||||
## with the [XRToolsPlayerBody] attached to the players [XROrigin3D].
|
||||
##
|
||||
## The player enables jumping by attaching an [XRToolsMovementJump] as a
|
||||
## child of the appropriate [XRController3D], then configuring the jump button
|
||||
## and jump velocity.
|
||||
|
||||
|
||||
## Movement provider order
|
||||
@export var order : int = 20
|
||||
|
||||
## Button to trigger jump
|
||||
@export var jump_button_action : String = "trigger_click"
|
||||
|
||||
|
||||
# Node references
|
||||
var _controller : XRController3D
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsMovementJump" or super(xr_name)
|
||||
|
||||
|
||||
# Called when our node is added to our scene tree
|
||||
func _enter_tree():
|
||||
_controller = XRHelpers.get_xr_controller(self)
|
||||
|
||||
|
||||
# Called when our node is removed from our scene tree
|
||||
func _exit_tree():
|
||||
_controller = null
|
||||
|
||||
|
||||
# Perform jump movement
|
||||
func physics_movement(_delta: float, player_body: XRToolsPlayerBody, _disabled: bool):
|
||||
# Skip if the jump controller isn't active
|
||||
if not _controller or not _controller.get_is_active():
|
||||
return
|
||||
|
||||
# Request jump if the button is pressed
|
||||
if _controller.is_button_pressed(jump_button_action):
|
||||
player_body.request_jump()
|
||||
|
||||
|
||||
# This method verifies the movement provider has a valid configuration.
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := super()
|
||||
|
||||
# Check the controller node
|
||||
if not XRHelpers.get_xr_controller(self):
|
||||
warnings.append("This node must be within a branch of an XRController3D node")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
1
addons/godot-xr-tools/functions/movement_jump.gd.uid
Normal file
1
addons/godot-xr-tools/functions/movement_jump.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dur4rs2uhr6cp
|
||||
6
addons/godot-xr-tools/functions/movement_jump.tscn
Normal file
6
addons/godot-xr-tools/functions/movement_jump.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://c2q5phg8w08o"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dur4rs2uhr6cp" path="res://addons/godot-xr-tools/functions/movement_jump.gd" id="1"]
|
||||
|
||||
[node name="MovementJump" type="Node3D" groups=["movement_providers"]]
|
||||
script = ExtResource("1")
|
||||
197
addons/godot-xr-tools/functions/movement_physical_jump.gd
Normal file
197
addons/godot-xr-tools/functions/movement_physical_jump.gd
Normal file
@@ -0,0 +1,197 @@
|
||||
@tool
|
||||
class_name XRToolsMovementPhysicalJump
|
||||
extends XRToolsMovementProvider
|
||||
|
||||
|
||||
## XR Tools Movement Provider for Player Physical Jump Detection
|
||||
##
|
||||
## This script can detect jumping based on either the players body jumping,
|
||||
## or by the player swinging their arms up.
|
||||
##
|
||||
## The player body jumping is detected by putting the cameras instantaneous
|
||||
## Y velocity (in the tracking space) into a sliding-window averager. If the
|
||||
## average Y velocity exceeds a threshold parameter then the player has
|
||||
## jumped.
|
||||
##
|
||||
## The player arms jumping is detected by putting both controllers instantaneous
|
||||
## Y velocity (in the tracking space) into a sliding-window averager. If both
|
||||
## average Y velocities exceed a threshold parameter then the player has
|
||||
## jumped.
|
||||
|
||||
|
||||
## Movement provider order
|
||||
@export var order : int = 20
|
||||
|
||||
## If true, jumps are detected via the players body (through the camera)
|
||||
@export var body_jump_enable : bool = true
|
||||
|
||||
## If true, the player jump is as high as the physical jump(no ground physics)
|
||||
@export var body_jump_player_only : bool = false
|
||||
|
||||
## Body jump detection threshold (M/S^2)
|
||||
@export var body_jump_threshold : float = 2.5
|
||||
|
||||
## If true, jumps are detected via the players arms (through the controllers)
|
||||
@export var arms_jump_enable : bool = false
|
||||
|
||||
## Arms jump detection threshold (M/S^2)
|
||||
@export var arms_jump_threshold : float = 5.0
|
||||
|
||||
|
||||
# Node Positions
|
||||
var _camera_position : float = 0.0
|
||||
var _controller_left_position : float = 0.0
|
||||
var _controller_right_position : float = 0.0
|
||||
|
||||
# Node Velocities
|
||||
var _camera_velocity : SlidingAverage = SlidingAverage.new(5)
|
||||
var _controller_left_velocity : SlidingAverage = SlidingAverage.new(5)
|
||||
var _controller_right_velocity : SlidingAverage = SlidingAverage.new(5)
|
||||
|
||||
|
||||
# Node references
|
||||
@onready var _origin_node := XRHelpers.get_xr_origin(self)
|
||||
@onready var _camera_node := XRHelpers.get_xr_camera(self)
|
||||
@onready var _controller_left_node := XRHelpers.get_left_controller(self)
|
||||
@onready var _controller_right_node := XRHelpers.get_right_controller(self)
|
||||
|
||||
|
||||
# Sliding Average class
|
||||
class SlidingAverage:
|
||||
# Sliding window size
|
||||
var _size: int
|
||||
|
||||
# Sum of items in the window
|
||||
var _sum := 0.0
|
||||
|
||||
# Position
|
||||
var _pos := 0
|
||||
|
||||
# Data window
|
||||
var _data := Array()
|
||||
|
||||
# Constructor
|
||||
func _init(size: int):
|
||||
# Set the size and fill the array
|
||||
_size = size
|
||||
for i in size:
|
||||
_data.push_back(0.0)
|
||||
|
||||
# Update the average
|
||||
func update(entry: float) -> float:
|
||||
# Add the new entry and subtract the old
|
||||
_sum += entry
|
||||
_sum -= _data[_pos]
|
||||
|
||||
# Store the new entry in the array and circularly advance the index
|
||||
_data[_pos] = entry
|
||||
_pos = (_pos + 1) % _size
|
||||
|
||||
# Return the average
|
||||
return _sum / _size
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsMovementPhysicalJump" or super(xr_name)
|
||||
|
||||
|
||||
# Perform jump detection
|
||||
func physics_movement(delta: float, player_body: XRToolsPlayerBody, _disabled: bool):
|
||||
# Handle detecting body jump
|
||||
if body_jump_enable:
|
||||
_detect_body_jump(delta, player_body)
|
||||
|
||||
# Handle detecting arms jump
|
||||
if arms_jump_enable:
|
||||
_detect_arms_jump(delta, player_body)
|
||||
|
||||
|
||||
# This method verifies the movement provider has a valid configuration.
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := super()
|
||||
|
||||
# Verify the camera
|
||||
if !XRHelpers.get_xr_origin(self):
|
||||
warnings.append("This node must be within a branch of an XROrigin3D node")
|
||||
|
||||
# Verify the camera
|
||||
if !XRHelpers.get_xr_camera(self):
|
||||
warnings.append("Unable to find XRCamera3D")
|
||||
|
||||
# Verify the left controller
|
||||
if !XRHelpers.get_left_controller(self):
|
||||
warnings.append("Unable to find left XRController3D node")
|
||||
|
||||
# Verify the right controller
|
||||
if !XRHelpers.get_right_controller(self):
|
||||
warnings.append("Unable to find left XRController3D node")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
|
||||
|
||||
# Detect the player jumping with their body (using the headset camera)
|
||||
func _detect_body_jump(delta: float, player_body: XRToolsPlayerBody) -> void:
|
||||
# Get the camera instantaneous velocity
|
||||
var new_camera_pos := _camera_node.transform.origin.y
|
||||
var camera_vel := (new_camera_pos - _camera_position) / delta
|
||||
_camera_position = new_camera_pos
|
||||
|
||||
# Ignore zero moves (either not tracking, or no update since last physics)
|
||||
if abs(camera_vel) < 0.001:
|
||||
return
|
||||
|
||||
# Correct for world-scale (convert to player units)
|
||||
camera_vel /= XRServer.world_scale
|
||||
|
||||
# Clamp the camera instantaneous velocity to +/- 2x the jump threshold
|
||||
camera_vel = clamp(camera_vel, -2.0 * body_jump_threshold, 2.0 * body_jump_threshold)
|
||||
|
||||
# Get the averaged velocity
|
||||
camera_vel = _camera_velocity.update(camera_vel)
|
||||
|
||||
# Detect a jump
|
||||
if camera_vel >= body_jump_threshold:
|
||||
player_body.request_jump(body_jump_player_only)
|
||||
|
||||
|
||||
# Detect the player jumping with their arms (using the controllers)
|
||||
func _detect_arms_jump(delta: float, player_body: XRToolsPlayerBody) -> void:
|
||||
# Skip if either of the controllers is disabled
|
||||
if !_controller_left_node.get_is_active() or !_controller_right_node.get_is_active():
|
||||
return
|
||||
|
||||
# Get the controllers instantaneous velocity
|
||||
var new_controller_left_pos := _controller_left_node.transform.origin.y
|
||||
var new_controller_right_pos := _controller_right_node.transform.origin.y
|
||||
var controller_left_vel := (new_controller_left_pos - _controller_left_position) / delta
|
||||
var controller_right_vel := (new_controller_right_pos - _controller_right_position) / delta
|
||||
_controller_left_position = new_controller_left_pos
|
||||
_controller_right_position = new_controller_right_pos
|
||||
|
||||
# Ignore zero moves (either not tracking, or no update since last physics)
|
||||
if abs(controller_left_vel) <= 0.001 and abs(controller_right_vel) <= 0.001:
|
||||
return
|
||||
|
||||
# Correct for world-scale (convert to player units)
|
||||
controller_left_vel /= XRServer.world_scale
|
||||
controller_right_vel /= XRServer.world_scale
|
||||
|
||||
# Clamp the controller instantaneous velocity to +/- 2x the jump threshold
|
||||
controller_left_vel = clamp(
|
||||
controller_left_vel,
|
||||
-2.0 * arms_jump_threshold,
|
||||
2.0 * arms_jump_threshold)
|
||||
controller_right_vel = clamp(
|
||||
controller_right_vel,
|
||||
-2.0 * arms_jump_threshold,
|
||||
2.0 * arms_jump_threshold)
|
||||
|
||||
# Get the averaged velocity
|
||||
controller_left_vel = _controller_left_velocity.update(controller_left_vel)
|
||||
controller_right_vel = _controller_right_velocity.update(controller_right_vel)
|
||||
|
||||
# Detect a jump
|
||||
if controller_left_vel >= arms_jump_threshold and controller_right_vel >= arms_jump_threshold:
|
||||
player_body.request_jump()
|
||||
@@ -0,0 +1 @@
|
||||
uid://c0podasns5j2h
|
||||
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://ckt118vcpmr6q"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://c0podasns5j2h" path="res://addons/godot-xr-tools/functions/movement_physical_jump.gd" id="1"]
|
||||
|
||||
[node name="MovementPhysicalJump" type="Node3D" groups=["movement_providers"]]
|
||||
script = ExtResource("1")
|
||||
93
addons/godot-xr-tools/functions/movement_provider.gd
Normal file
93
addons/godot-xr-tools/functions/movement_provider.gd
Normal file
@@ -0,0 +1,93 @@
|
||||
@tool
|
||||
@icon("res://addons/godot-xr-tools/editor/icons/movement_provider.svg")
|
||||
class_name XRToolsMovementProvider
|
||||
extends Node3D
|
||||
|
||||
|
||||
## XR Tools Movement Provider base class
|
||||
##
|
||||
## This movement provider class is the base class of all movement providers.
|
||||
## Movement providers are invoked by the [XRToolsPlayerBody] object in order
|
||||
## to apply motion to the player.
|
||||
##
|
||||
## Movement provider implementations should:
|
||||
## - Export an 'order' integer to control order of processing
|
||||
## - Override the physics_movement method to impelment motion
|
||||
|
||||
|
||||
## Player body scene
|
||||
const PLAYER_BODY := preload("res://addons/godot-xr-tools/player/player_body.tscn")
|
||||
|
||||
|
||||
## Enable movement provider
|
||||
@export var enabled : bool = true
|
||||
|
||||
|
||||
## If true, the movement provider is actively performing a move
|
||||
var is_active := false
|
||||
|
||||
|
||||
# If missing we need to add our [XRToolsPlayerBody]
|
||||
func _create_player_body_node():
|
||||
# get our origin node
|
||||
var xr_origin = XRHelpers.get_xr_origin(self)
|
||||
if !xr_origin:
|
||||
return
|
||||
|
||||
# Double check if it hasn't already been created by another movement function
|
||||
var player_body := XRToolsPlayerBody.find_instance(self)
|
||||
if !player_body:
|
||||
# create our XRToolsPlayerBody node and add it into our tree
|
||||
player_body = PLAYER_BODY.instantiate()
|
||||
player_body.set_name("PlayerBody")
|
||||
xr_origin.add_child(player_body)
|
||||
player_body.set_owner(get_tree().get_edited_scene_root())
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsMovementProvider"
|
||||
|
||||
|
||||
# Function run when node is added to scene
|
||||
func _ready():
|
||||
# If we're in the editor, help the user out by creating our XRToolsPlayerBody node
|
||||
# automatically when needed.
|
||||
if Engine.is_editor_hint():
|
||||
var player_body = XRToolsPlayerBody.find_instance(self)
|
||||
if !player_body:
|
||||
# This call needs to be deferred, we can't add nodes during scene construction
|
||||
call_deferred("_create_player_body_node")
|
||||
|
||||
|
||||
## Override this method to perform pre-movement updates to the PlayerBody
|
||||
func physics_pre_movement(_delta: float, _player_body: XRToolsPlayerBody):
|
||||
pass
|
||||
|
||||
|
||||
## Override this method to apply motion to the PlayerBody
|
||||
func physics_movement(_delta: float, _player_body: XRToolsPlayerBody, _disabled: bool):
|
||||
pass
|
||||
|
||||
|
||||
# This method verifies the movement provider has a valid configuration.
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := PackedStringArray()
|
||||
|
||||
# Verify we're within the tree of an XROrigin3D node
|
||||
if !XRHelpers.get_xr_origin(self):
|
||||
warnings.append("This node must be within a branch on an XROrigin3D node")
|
||||
|
||||
if !XRToolsPlayerBody.find_instance(self):
|
||||
warnings.append("Missing PlayerBody node on the XROrigin3D")
|
||||
|
||||
# Verify movement provider is in the correct group
|
||||
if !is_in_group("movement_providers"):
|
||||
warnings.append("Movement provider not in 'movement_providers' group")
|
||||
|
||||
# Verify order property exists
|
||||
if !"order" in self:
|
||||
warnings.append("Movement provider does not expose an order property")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
1
addons/godot-xr-tools/functions/movement_provider.gd.uid
Normal file
1
addons/godot-xr-tools/functions/movement_provider.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://i2t7j1aj60s0
|
||||
169
addons/godot-xr-tools/functions/movement_sprint.gd
Normal file
169
addons/godot-xr-tools/functions/movement_sprint.gd
Normal file
@@ -0,0 +1,169 @@
|
||||
@tool
|
||||
class_name XRToolsMovementSprint
|
||||
extends XRToolsMovementProvider
|
||||
|
||||
|
||||
## XR Tools Movement Provider for Sprinting
|
||||
##
|
||||
## This script provides sprinting movement for the player. It assumes there is
|
||||
## a direct movement node in the scene otherwise it will not be functional.
|
||||
##
|
||||
## There will not be an error, there just will not be any reason for it to
|
||||
## have any impact on the player. This node should be a direct child of
|
||||
## the [XROrigin3D] node rather than to a specific [XRController3D].
|
||||
|
||||
|
||||
## Signal emitted when sprinting starts
|
||||
signal sprinting_started()
|
||||
|
||||
## Signal emitted when sprinting finishes
|
||||
signal sprinting_finished()
|
||||
|
||||
|
||||
## Enumeration of controller to use for triggering sprinting. This allows the
|
||||
## developer to assign the sprint button to either controller.
|
||||
enum SprintController {
|
||||
LEFT, ## Use left controller
|
||||
RIGHT, ## Use right controller
|
||||
}
|
||||
|
||||
## Enumeration of sprinting modes - toggle or hold button
|
||||
enum SprintType {
|
||||
HOLD_TO_SPRINT, ## Hold button to sprint
|
||||
TOGGLE_SPRINT, ## Toggle sprinting on button press
|
||||
}
|
||||
|
||||
|
||||
## Type of sprinting
|
||||
@export var sprint_type : SprintType = SprintType.HOLD_TO_SPRINT
|
||||
|
||||
## Sprint speed multiplier (multiplier from speed set by direct movement node(s))
|
||||
@export_range(1.0, 4.0) var sprint_speed_multiplier : float = 2.0
|
||||
|
||||
## Movement provider order
|
||||
@export var order : int = 11
|
||||
|
||||
## Sprint controller
|
||||
@export var controller : SprintController = SprintController.LEFT
|
||||
|
||||
## Sprint button
|
||||
@export var sprint_button : String = "primary_click"
|
||||
|
||||
|
||||
# Sprint controller
|
||||
var _controller : XRController3D
|
||||
|
||||
# Sprint button down state
|
||||
var _sprint_button_down : bool = false
|
||||
|
||||
# Variable to hold left controller direct movement node original max speed
|
||||
var _left_controller_original_max_speed : float = 0.0
|
||||
|
||||
# Variable to hold right controller direct movement node original max speed
|
||||
var _right_controller_original_max_speed : float = 0.0
|
||||
|
||||
|
||||
# Variable used to cache left controller direct movement function, if any
|
||||
@onready var _left_controller_direct_move := XRToolsMovementDirect.find_left(self)
|
||||
|
||||
# Variable used to cache right controller direct movement function, if any
|
||||
@onready var _right_controller_direct_move := XRToolsMovementDirect.find_right(self)
|
||||
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsMovementSprint" or super(xr_name)
|
||||
|
||||
|
||||
func _ready():
|
||||
# In Godot 4 we must now manually call our super class ready function
|
||||
super()
|
||||
|
||||
# Get the sprinting controller
|
||||
if controller == SprintController.LEFT:
|
||||
_controller = XRHelpers.get_left_controller(self)
|
||||
else:
|
||||
_controller = XRHelpers.get_right_controller(self)
|
||||
|
||||
|
||||
# Perform sprinting
|
||||
func physics_movement(_delta: float, _player_body: XRToolsPlayerBody, disabled: bool):
|
||||
# Skip if the controller isn't active or is not enabled
|
||||
if !_controller.get_is_active() or disabled == true or !enabled:
|
||||
set_sprinting(false)
|
||||
return
|
||||
|
||||
# Detect sprint button down and pressed states
|
||||
var sprint_button_down := _controller.is_button_pressed(sprint_button)
|
||||
var sprint_button_pressed := sprint_button_down and !_sprint_button_down
|
||||
_sprint_button_down = sprint_button_down
|
||||
|
||||
# Calculate new sprinting state
|
||||
var sprinting := is_active
|
||||
match sprint_type:
|
||||
SprintType.HOLD_TO_SPRINT:
|
||||
# Sprint when button down
|
||||
sprinting = sprint_button_down
|
||||
|
||||
SprintType.TOGGLE_SPRINT:
|
||||
# Toggle when button pressed
|
||||
if sprint_button_pressed:
|
||||
sprinting = !sprinting
|
||||
|
||||
# Update sprinting state
|
||||
if sprinting != is_active:
|
||||
set_sprinting(sprinting)
|
||||
|
||||
|
||||
# Public function used to set sprinting active or not active
|
||||
func set_sprinting(active: bool) -> void:
|
||||
# Skip if no change
|
||||
if active == is_active:
|
||||
return
|
||||
|
||||
# Update state
|
||||
is_active = active
|
||||
|
||||
# Handle state change
|
||||
if is_active:
|
||||
# We are sprinting
|
||||
emit_signal("sprinting_started")
|
||||
|
||||
# Since max speeds could be changed while game is running, check
|
||||
# now for original max speeds of left and right nodes
|
||||
if _left_controller_direct_move:
|
||||
_left_controller_original_max_speed = _left_controller_direct_move.max_speed
|
||||
if _right_controller_direct_move:
|
||||
_right_controller_original_max_speed = _right_controller_direct_move.max_speed
|
||||
|
||||
# Set both controllers' direct movement functions, if appliable, to
|
||||
# the sprinting speed
|
||||
if _left_controller_direct_move:
|
||||
_left_controller_direct_move.max_speed = \
|
||||
_left_controller_original_max_speed * sprint_speed_multiplier
|
||||
if _right_controller_direct_move:
|
||||
_right_controller_direct_move.max_speed = \
|
||||
_right_controller_original_max_speed * sprint_speed_multiplier
|
||||
else:
|
||||
# We are not sprinting
|
||||
emit_signal("sprinting_finished")
|
||||
|
||||
# Set both controllers' direct movement functions, if applicable, to
|
||||
# their original speeds
|
||||
if _left_controller_direct_move:
|
||||
_left_controller_direct_move.max_speed = _left_controller_original_max_speed
|
||||
if _right_controller_direct_move:
|
||||
_right_controller_direct_move.max_speed = _right_controller_original_max_speed
|
||||
|
||||
|
||||
# This method verifies the movement provider has a valid configuration.
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := super()
|
||||
|
||||
# Make sure player has at least one direct movement node
|
||||
if !XRToolsMovementDirect.find_left(self) and !XRToolsMovementDirect.find_right(self):
|
||||
warnings.append("Player missing XRToolsMovementDirect nodes")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
1
addons/godot-xr-tools/functions/movement_sprint.gd.uid
Normal file
1
addons/godot-xr-tools/functions/movement_sprint.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cm2m0igkq32wy
|
||||
6
addons/godot-xr-tools/functions/movement_sprint.tscn
Normal file
6
addons/godot-xr-tools/functions/movement_sprint.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://drs4eeq721ojn"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cm2m0igkq32wy" path="res://addons/godot-xr-tools/functions/movement_sprint.gd" id="1"]
|
||||
|
||||
[node name="MovementSprint" type="Node3D" groups=["movement_providers"]]
|
||||
script = ExtResource("1")
|
||||
122
addons/godot-xr-tools/functions/movement_turn.gd
Normal file
122
addons/godot-xr-tools/functions/movement_turn.gd
Normal file
@@ -0,0 +1,122 @@
|
||||
@tool
|
||||
class_name XRToolsMovementTurn
|
||||
extends XRToolsMovementProvider
|
||||
|
||||
|
||||
## XR Tools Movement Provider for Turning
|
||||
##
|
||||
## This script provides turning support for the player. This script works
|
||||
## with the PlayerBody attached to the players XROrigin3D.
|
||||
|
||||
|
||||
## Movement mode
|
||||
enum TurnMode {
|
||||
DEFAULT, ## Use turn mode from project/user settings
|
||||
SNAP, ## Use snap-turning
|
||||
SMOOTH ## Use smooth-turning
|
||||
}
|
||||
|
||||
|
||||
## Movement provider order
|
||||
@export var order : int = 5
|
||||
|
||||
## Movement mode property
|
||||
@export var turn_mode : TurnMode = TurnMode.DEFAULT
|
||||
|
||||
## Smooth turn speed in radians per second
|
||||
@export var smooth_turn_speed : float = 2.0
|
||||
|
||||
## Seconds per step (at maximum turn rate)
|
||||
@export var step_turn_delay : float = 0.2
|
||||
|
||||
## Step turn angle in degrees
|
||||
@export var step_turn_angle : float = 20.0
|
||||
|
||||
## Our directional input
|
||||
@export var input_action : String = "primary"
|
||||
|
||||
# Turn step accumulator
|
||||
var _turn_step : float = 0.0
|
||||
|
||||
|
||||
# Controller node
|
||||
var _controller : XRController3D
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsMovementTurn" or super(xr_name)
|
||||
|
||||
|
||||
# Called when our node is added to our scene tree
|
||||
func _enter_tree():
|
||||
_controller = XRHelpers.get_xr_controller(self)
|
||||
|
||||
|
||||
# Called when our node is removed from our scene tree
|
||||
func _exit_tree():
|
||||
_controller = null
|
||||
|
||||
|
||||
# Perform jump movement
|
||||
func physics_movement(delta: float, player_body: XRToolsPlayerBody, _disabled: bool):
|
||||
# Skip if the controller isn't active
|
||||
if not _controller or not _controller.get_is_active():
|
||||
return
|
||||
|
||||
var deadzone = 0.1
|
||||
if _snap_turning():
|
||||
deadzone = XRTools.get_snap_turning_deadzone()
|
||||
|
||||
# Read the left/right joystick axis
|
||||
var left_right := _controller.get_vector2(input_action).x
|
||||
if abs(left_right) <= deadzone:
|
||||
# Not turning
|
||||
_turn_step = 0.0
|
||||
return
|
||||
|
||||
# Handle smooth rotation
|
||||
if !_snap_turning():
|
||||
left_right -= deadzone * sign(left_right)
|
||||
player_body.rotate_player(smooth_turn_speed * delta * left_right)
|
||||
return
|
||||
|
||||
# Disable repeat snap turning if delay is zero
|
||||
if step_turn_delay == 0.0 and _turn_step < 0.0:
|
||||
return
|
||||
|
||||
# Update the next turn-step delay
|
||||
_turn_step -= abs(left_right) * delta
|
||||
if _turn_step >= 0.0:
|
||||
return
|
||||
|
||||
# Turn one step in the requested direction
|
||||
if step_turn_delay != 0.0:
|
||||
_turn_step = step_turn_delay
|
||||
|
||||
player_body.rotate_player(deg_to_rad(step_turn_angle) * sign(left_right))
|
||||
|
||||
|
||||
# This method verifies the movement provider has a valid configuration.
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := super()
|
||||
|
||||
# Check the controller node
|
||||
if not XRHelpers.get_xr_controller(self):
|
||||
warnings.append("Unable to find XRController3D node")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
|
||||
|
||||
# Test if snap turning should be used
|
||||
func _snap_turning():
|
||||
match turn_mode:
|
||||
TurnMode.SNAP:
|
||||
return true
|
||||
|
||||
TurnMode.SMOOTH:
|
||||
return false
|
||||
|
||||
_:
|
||||
return XRToolsUserSettings.snap_turning
|
||||
1
addons/godot-xr-tools/functions/movement_turn.gd.uid
Normal file
1
addons/godot-xr-tools/functions/movement_turn.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://kuxqh057vr5y
|
||||
6
addons/godot-xr-tools/functions/movement_turn.tscn
Normal file
6
addons/godot-xr-tools/functions/movement_turn.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://b6bk2pj8vbj28"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://kuxqh057vr5y" path="res://addons/godot-xr-tools/functions/movement_turn.gd" id="1"]
|
||||
|
||||
[node name="MovementTurn" type="Node3D" groups=["movement_providers"]]
|
||||
script = ExtResource("1")
|
||||
44
addons/godot-xr-tools/functions/movement_wall_walk.gd
Normal file
44
addons/godot-xr-tools/functions/movement_wall_walk.gd
Normal file
@@ -0,0 +1,44 @@
|
||||
@tool
|
||||
class_name XRToolsMovementWallWalk
|
||||
extends XRToolsMovementProvider
|
||||
|
||||
|
||||
# Default wall-walk mask of 4:wall-walk
|
||||
const DEFAULT_MASK := 0b0000_0000_0000_0000_0000_0000_0000_1000
|
||||
|
||||
|
||||
## Wall walking provider order
|
||||
@export var order : int = 25
|
||||
|
||||
## Set our follow layer mask
|
||||
@export_flags_3d_physics var follow_mask : int = DEFAULT_MASK
|
||||
|
||||
## Wall stick distance
|
||||
@export var stick_distance : float = 1.0
|
||||
|
||||
## Wall stick strength
|
||||
@export var stick_strength : float = 9.8
|
||||
|
||||
|
||||
func physics_pre_movement(_delta: float, player_body: XRToolsPlayerBody):
|
||||
# Test for collision with wall under feet
|
||||
var wall_collision := player_body.move_and_collide(
|
||||
player_body.up_player * -stick_distance, true)
|
||||
if !wall_collision:
|
||||
return
|
||||
|
||||
# Get the wall information
|
||||
var wall_node := wall_collision.get_collider()
|
||||
var wall_normal := wall_collision.get_normal()
|
||||
|
||||
# Skip if the wall node doesn't have a collision layer
|
||||
if not "collision_layer" in wall_node:
|
||||
return
|
||||
|
||||
# Skip if the wall doesn't match the follow layer
|
||||
var wall_layer : int = wall_node.collision_layer
|
||||
if (wall_layer & follow_mask) == 0:
|
||||
return
|
||||
|
||||
# Modify the player gravity
|
||||
player_body.gravity = -wall_normal * stick_strength
|
||||
@@ -0,0 +1 @@
|
||||
uid://bv4jc67lcmo75
|
||||
6
addons/godot-xr-tools/functions/movement_wall_walk.tscn
Normal file
6
addons/godot-xr-tools/functions/movement_wall_walk.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://bk6ban0hctyym"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bv4jc67lcmo75" path="res://addons/godot-xr-tools/functions/movement_wall_walk.gd" id="1"]
|
||||
|
||||
[node name="MovementWallWalk" type="Node3D" groups=["movement_providers"]]
|
||||
script = ExtResource("1")
|
||||
135
addons/godot-xr-tools/functions/movement_wind.gd
Normal file
135
addons/godot-xr-tools/functions/movement_wind.gd
Normal file
@@ -0,0 +1,135 @@
|
||||
@tool
|
||||
class_name XRToolsMovementWind
|
||||
extends XRToolsMovementProvider
|
||||
|
||||
|
||||
## XR Tools Movement Provider for Wind
|
||||
##
|
||||
## This script provides wind mechanics for the player. This script works
|
||||
## with the [XRToolsPlayerBody] attached to the players [XROrigin3D].
|
||||
##
|
||||
## When the player enters an [XRToolsWindArea], the wind pushes the player
|
||||
## around, and can even lift the player into the air.
|
||||
|
||||
|
||||
## Signal invoked when changing active wind areas
|
||||
signal wind_area_changed(active_wind_area)
|
||||
|
||||
|
||||
# Default wind area collision mask of 20:player-body
|
||||
const DEFAULT_MASK := 0b0000_0000_0000_1000_0000_0000_0000_0000
|
||||
|
||||
|
||||
## Movement provider order
|
||||
@export var order : int = 25
|
||||
|
||||
## Drag multiplier for the player
|
||||
@export var drag_multiplier : float = 1.0
|
||||
|
||||
# Set our collision mask
|
||||
@export_flags_3d_physics var collision_mask : int = DEFAULT_MASK: set = set_collision_mask
|
||||
|
||||
|
||||
# Wind detection area
|
||||
var _sense_area : Area3D
|
||||
|
||||
# Array of wind areas the player is in
|
||||
var _in_wind_areas := Array()
|
||||
|
||||
# Currently active wind area
|
||||
var _active_wind_area : XRToolsWindArea
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsMovementWind" 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()
|
||||
|
||||
# Skip if running in the editor
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# Skip if we don't have a camera
|
||||
var camera := XRHelpers.get_xr_camera(self)
|
||||
if !camera:
|
||||
return
|
||||
|
||||
# Construct the sphere shape
|
||||
var sphere_shape := SphereShape3D.new()
|
||||
sphere_shape.radius = 0.3
|
||||
|
||||
# Construct the collision shape
|
||||
var collision_shape := CollisionShape3D.new()
|
||||
collision_shape.set_name("WindSensorShape")
|
||||
collision_shape.shape = sphere_shape
|
||||
|
||||
# Construct the sense area
|
||||
_sense_area = Area3D.new()
|
||||
_sense_area.set_name("WindSensorArea")
|
||||
_sense_area.collision_mask = collision_mask
|
||||
_sense_area.add_child(collision_shape)
|
||||
|
||||
# Add the sense area to the camera
|
||||
camera.add_child(_sense_area)
|
||||
|
||||
# Subscribe to area notifications
|
||||
_sense_area.area_entered.connect(_on_area_entered)
|
||||
_sense_area.area_exited.connect(_on_area_exited)
|
||||
|
||||
|
||||
func set_collision_mask(new_mask: int) -> void:
|
||||
collision_mask = new_mask
|
||||
if is_inside_tree() and _sense_area:
|
||||
_sense_area.collision_mask = collision_mask
|
||||
|
||||
|
||||
func _on_area_entered(area: Area3D):
|
||||
# Skip if not wind area
|
||||
var wind_area = area as XRToolsWindArea
|
||||
if !wind_area:
|
||||
return
|
||||
|
||||
# Save area and set active
|
||||
_in_wind_areas.push_front(wind_area)
|
||||
_active_wind_area = wind_area
|
||||
|
||||
# Report the wind area change
|
||||
emit_signal("wind_area_changed", _active_wind_area)
|
||||
|
||||
|
||||
func _on_area_exited(area: Area3D):
|
||||
# Erase from the wind area
|
||||
_in_wind_areas.erase(area)
|
||||
|
||||
# If we didn't leave the active wind area then we're done
|
||||
if area != _active_wind_area:
|
||||
return
|
||||
|
||||
# Select a new active wind area
|
||||
if _in_wind_areas.is_empty():
|
||||
_active_wind_area = null
|
||||
else:
|
||||
_active_wind_area = _in_wind_areas.front()
|
||||
|
||||
# Report the wind area change
|
||||
emit_signal("wind_area_changed", _active_wind_area)
|
||||
|
||||
|
||||
# Perform wind movement
|
||||
func physics_movement(delta: float, player_body: XRToolsPlayerBody, _disabled: bool):
|
||||
# Skip if no active wind area
|
||||
if !_active_wind_area:
|
||||
return
|
||||
|
||||
# Calculate the global wind velocity of the wind area
|
||||
var wind_velocity := _active_wind_area.global_transform.basis * _active_wind_area.wind_vector
|
||||
|
||||
# Drag the player into the wind
|
||||
var drag_factor := _active_wind_area.drag * drag_multiplier * delta
|
||||
drag_factor = clamp(drag_factor, 0.0, 1.0)
|
||||
player_body.velocity = player_body.velocity.lerp(wind_velocity, drag_factor)
|
||||
1
addons/godot-xr-tools/functions/movement_wind.gd.uid
Normal file
1
addons/godot-xr-tools/functions/movement_wind.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://oexcdd5m45l8
|
||||
6
addons/godot-xr-tools/functions/movement_wind.tscn
Normal file
6
addons/godot-xr-tools/functions/movement_wind.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://bgts3vpmjn6bb"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://oexcdd5m45l8" path="res://addons/godot-xr-tools/functions/movement_wind.gd" id="1"]
|
||||
|
||||
[node name="MovementWind" type="Node3D" groups=["movement_providers"]]
|
||||
script = ExtResource("1")
|
||||
217
addons/godot-xr-tools/functions/movement_world_grab.gd
Normal file
217
addons/godot-xr-tools/functions/movement_world_grab.gd
Normal file
@@ -0,0 +1,217 @@
|
||||
@tool
|
||||
class_name XRToolsMovementWorldGrab
|
||||
extends XRToolsMovementProvider
|
||||
|
||||
|
||||
## XR Tools Movement Provider for World-Grab
|
||||
##
|
||||
## This script provides world-grab movement for the player. To add world-grab
|
||||
## support, the player must also have [XRToolsFunctionPickup] nodes attached
|
||||
## to the left and right controllers, and an [XRToolsPlayerBody] under the
|
||||
## [XROrigin3D].
|
||||
##
|
||||
## World-Grab areas inherit from the world_grab_area scene, or be [Area3D]
|
||||
## nodes with the [XRToolsWorldGrabArea] script attached to them.
|
||||
|
||||
|
||||
## Signal invoked when the player starts world-grab movement
|
||||
signal player_world_grab_start
|
||||
|
||||
## Signal invoked when the player ends world-grab movement
|
||||
signal player_world_grab_end
|
||||
|
||||
|
||||
## Movement provider order
|
||||
@export var order : int = 15
|
||||
|
||||
## Smallest world scale
|
||||
@export var world_scale_min := 0.5
|
||||
|
||||
## Largest world scale
|
||||
@export var world_scale_max := 2.0
|
||||
|
||||
|
||||
# Left world-grab handle
|
||||
var _left_handle : Node3D
|
||||
|
||||
# Right world-grab handle
|
||||
var _right_handle : Node3D
|
||||
|
||||
|
||||
# Left pickup node
|
||||
@onready var _left_pickup_node := XRToolsFunctionPickup.find_left(self)
|
||||
|
||||
# Right pickup node
|
||||
@onready var _right_pickup_node := XRToolsFunctionPickup.find_right(self)
|
||||
|
||||
# Left controller
|
||||
@onready var _left_controller := XRHelpers.get_left_controller(self)
|
||||
|
||||
# Right controller
|
||||
@onready var _right_controller := XRHelpers.get_right_controller(self)
|
||||
|
||||
|
||||
# Add support for is_xr_class on XRTools classes
|
||||
func is_xr_class(xr_name: String) -> bool:
|
||||
return xr_name == "XRToolsMovementGrabWorld" 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()
|
||||
|
||||
# Do not initialise if in the editor
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
# Connect pickup funcitons
|
||||
if _left_pickup_node.connect("has_picked_up", _on_left_picked_up):
|
||||
push_error("Unable to connect left picked up signal")
|
||||
if _right_pickup_node.connect("has_picked_up", _on_right_picked_up):
|
||||
push_error("Unable to connect right picked up signal")
|
||||
if _left_pickup_node.connect("has_dropped", _on_left_dropped):
|
||||
push_error("Unable to connect left dropped signal")
|
||||
if _right_pickup_node.connect("has_dropped", _on_right_dropped):
|
||||
push_error("Unable to connect right dropped signal")
|
||||
|
||||
|
||||
## Perform player physics movement
|
||||
func physics_movement(delta: float, player_body: XRToolsPlayerBody, disabled: bool):
|
||||
# Disable world-grab movement if requested
|
||||
if disabled or !enabled:
|
||||
_set_world_grab_moving(false)
|
||||
return
|
||||
|
||||
# Always set velocity to zero if enabled
|
||||
player_body.velocity = Vector3.ZERO
|
||||
|
||||
# Check for world-grab handles being deleted while held
|
||||
if not is_instance_valid(_left_handle):
|
||||
_left_handle = null
|
||||
if not is_instance_valid(_right_handle):
|
||||
_right_handle = null
|
||||
|
||||
# Disable world-grab movement if not holding the world
|
||||
if not _left_handle and not _right_handle:
|
||||
_set_world_grab_moving(false)
|
||||
return
|
||||
|
||||
# World grabbed
|
||||
_set_world_grab_moving(true)
|
||||
|
||||
# Handle world-grab movement
|
||||
var offset := Vector3.ZERO
|
||||
if _left_handle and not _right_handle:
|
||||
# Left-hand movement only
|
||||
var left_pickup_pos := _left_controller.global_position
|
||||
var left_grab_pos := _left_handle.global_position
|
||||
offset = left_pickup_pos - left_grab_pos
|
||||
elif _right_handle and not _left_handle:
|
||||
# Right-hand movement only
|
||||
var right_pickup_pos := _right_controller.global_position
|
||||
var right_grab_pos := _right_handle.global_position
|
||||
offset = right_pickup_pos - right_grab_pos
|
||||
else:
|
||||
# Get the world-grab handle positions
|
||||
var left_grab_pos := _left_handle.global_position
|
||||
var right_grab_pos := _right_handle.global_position
|
||||
var grab_l2r := (right_grab_pos - left_grab_pos).slide(player_body.up_player)
|
||||
var grab_mid := (left_grab_pos + right_grab_pos) * 0.5
|
||||
|
||||
# Get the pickup positions
|
||||
var left_pickup_pos := _left_controller.global_position
|
||||
var right_pickup_pos := _right_controller.global_position
|
||||
var pickup_l2r := (right_pickup_pos - left_pickup_pos).slide(player_body.up_player)
|
||||
var pickup_mid := (left_pickup_pos + right_pickup_pos) * 0.5
|
||||
|
||||
# Apply rotation
|
||||
var angle := grab_l2r.signed_angle_to(pickup_l2r, player_body.up_player)
|
||||
player_body.rotate_player(angle)
|
||||
|
||||
# Apply scale
|
||||
var new_world_scale := XRServer.world_scale * grab_l2r.length() / pickup_l2r.length()
|
||||
new_world_scale = clamp(new_world_scale, world_scale_min, world_scale_max)
|
||||
XRServer.world_scale = new_world_scale
|
||||
|
||||
# Apply offset
|
||||
offset = pickup_mid - grab_mid
|
||||
|
||||
# Move the player by the offset
|
||||
var old_position := player_body.global_position
|
||||
player_body.move_player(-offset / delta)
|
||||
player_body.velocity = Vector3.ZERO
|
||||
#player_body.move_and_collide(-offset)
|
||||
|
||||
# Report exclusive motion performed (to bypass gravity)
|
||||
return true
|
||||
|
||||
|
||||
## Start or stop world-grab movement
|
||||
func _set_world_grab_moving(active: bool) -> void:
|
||||
# Skip if no change
|
||||
if active == is_active:
|
||||
return
|
||||
|
||||
# Update state
|
||||
is_active = active
|
||||
|
||||
# Handle state change
|
||||
if is_active:
|
||||
emit_signal("player_world_grab_start")
|
||||
else:
|
||||
emit_signal("player_world_grab_end")
|
||||
|
||||
|
||||
## Handler for left controller picked up
|
||||
func _on_left_picked_up(what : Node3D) -> void:
|
||||
# Get the world-grab area
|
||||
var world_grab_area = what as XRToolsWorldGrabArea
|
||||
if not world_grab_area:
|
||||
return
|
||||
|
||||
# Get the handle
|
||||
_left_handle = world_grab_area.get_grab_handle(_left_pickup_node)
|
||||
if not _left_handle:
|
||||
return
|
||||
|
||||
|
||||
## Handler for right controller picked up
|
||||
func _on_right_picked_up(what : Node3D) -> void:
|
||||
# Get the world-grab area
|
||||
var world_grab_area = what as XRToolsWorldGrabArea
|
||||
if not world_grab_area:
|
||||
return
|
||||
|
||||
# Get the handle
|
||||
_right_handle = world_grab_area.get_grab_handle(_right_pickup_node)
|
||||
if not _right_handle:
|
||||
return
|
||||
|
||||
|
||||
## Handler for left controller dropped
|
||||
func _on_left_dropped() -> void:
|
||||
# Release handle and transfer dominance
|
||||
_left_handle = null
|
||||
|
||||
|
||||
## Handler for righ controller dropped
|
||||
func _on_right_dropped() -> void:
|
||||
# Release handle and transfer dominance
|
||||
_right_handle = null
|
||||
|
||||
|
||||
# This method verifies the movement provider has a valid configuration.
|
||||
func _get_configuration_warnings() -> PackedStringArray:
|
||||
var warnings := super()
|
||||
|
||||
# Verify the left controller pickup
|
||||
if !XRToolsFunctionPickup.find_left(self):
|
||||
warnings.append("Unable to find left XRToolsFunctionPickup node")
|
||||
|
||||
# Verify the right controller pickup
|
||||
if !XRToolsFunctionPickup.find_right(self):
|
||||
warnings.append("Unable to find right XRToolsFunctionPickup node")
|
||||
|
||||
# Return warnings
|
||||
return warnings
|
||||
@@ -0,0 +1 @@
|
||||
uid://bgq1jyvtogcll
|
||||
6
addons/godot-xr-tools/functions/movement_world_grab.tscn
Normal file
6
addons/godot-xr-tools/functions/movement_world_grab.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dg3gr6ofd8yx4"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bgq1jyvtogcll" path="res://addons/godot-xr-tools/functions/movement_world_grab.gd" id="1_0qp8q"]
|
||||
|
||||
[node name="MovementWorldGrab" type="Node3D" groups=["movement_providers"]]
|
||||
script = ExtResource("1_0qp8q")
|
||||
Reference in New Issue
Block a user