This commit is contained in:
sharpoff
2026-01-14 13:01:37 +09:00
commit 49d7e22132
660 changed files with 41243 additions and 0 deletions

View 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)

View File

@@ -0,0 +1 @@
uid://f60byrk7xjak

View 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")

View 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()

View File

@@ -0,0 +1 @@
uid://b18kkstg643yw

View 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

View 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

View File

@@ -0,0 +1 @@
uid://cvnagt08r6ul4

View 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")

View 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()

View File

@@ -0,0 +1 @@
uid://cupmjxb4tpq3p

View 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")

View 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)

View File

@@ -0,0 +1 @@
uid://blaa027erhjlp

View 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")

View 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

View File

@@ -0,0 +1 @@
uid://dtl3uo5dnibof

View 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")

View 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

View File

@@ -0,0 +1 @@
uid://vuhhcx5m67gt

View 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")

View 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

View File

@@ -0,0 +1 @@
uid://dc4hg8kq5ia5y

View 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")

View 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

View File

@@ -0,0 +1 @@
uid://vyag4h2e7n0p

View 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")

View 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

View File

@@ -0,0 +1 @@
uid://clpt3lske02eh

View 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="."]

View 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

View File

@@ -0,0 +1 @@
uid://5pticrbivdx6

View 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")

View 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

View File

@@ -0,0 +1 @@
uid://r8s1ep2dlajf

View 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

View 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

View File

@@ -0,0 +1 @@
uid://f5tfo4ylkuhc

View 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")

View 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

View File

@@ -0,0 +1 @@
uid://dur4rs2uhr6cp

View 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")

View 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()

View File

@@ -0,0 +1 @@
uid://c0podasns5j2h

View File

@@ -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")

View 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

View File

@@ -0,0 +1 @@
uid://i2t7j1aj60s0

View 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

View File

@@ -0,0 +1 @@
uid://cm2m0igkq32wy

View 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")

View 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

View File

@@ -0,0 +1 @@
uid://kuxqh057vr5y

View 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")

View 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

View File

@@ -0,0 +1 @@
uid://bv4jc67lcmo75

View 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")

View 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)

View File

@@ -0,0 +1 @@
uid://oexcdd5m45l8

View 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")

View 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

View File

@@ -0,0 +1 @@
uid://bgq1jyvtogcll

View 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")