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,66 @@
@tool
class_name XRToolsFadeCollision
extends Node3D
@export_category("Collison")
## Layers to collide with
@export_flags_3d_physics var collision_layers : int = 3
## Collision distance at which fading begins
@export var fade_start_distance : float = 0.3
## Collision distance for totally obscuring the view
@export var fade_full_distance : float = 0.15
# Shape to use for collision detection
var _collision_shape : Shape3D
# Parameters to use for collision detection
var _collision_parameters : PhysicsShapeQueryParameters3D
# World space to use for collision detection
var _space : PhysicsDirectSpaceState3D
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsFadeCollision"
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
# Construct a sphere for the collision shape
_collision_shape = SphereShape3D.new()
_collision_shape.radius = fade_start_distance
# Construct the collosion parameters
_collision_parameters = PhysicsShapeQueryParameters3D.new()
_collision_parameters.collision_mask = collision_layers
_collision_parameters.set_shape(_collision_shape)
# Get the space to test collisions in
_space = get_world_3d().direct_space_state
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _physics_process(_delta : float) -> void:
# Update the collision parameters to include our global location
_collision_parameters.transform = global_transform
# Find closest collision
var results = _space.get_rest_info(_collision_parameters)
if "point" in results:
# Collision detected, calculate distance to closet collision point
var delta_pos = global_transform.origin - results["point"]
var length = delta_pos.length()
# Fade based on distance
var alpha := inverse_lerp(fade_start_distance, fade_full_distance, length)
XRToolsFade.set_fade(self, Color(0, 0, 0, alpha))
else:
# No collision
XRToolsFade.set_fade(self, Color(0, 0, 0, 0))

View File

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

View File

@@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://b228p8k6sonve"]
[ext_resource type="Script" uid="uid://qk8ofik5trvx" path="res://addons/godot-xr-tools/player/fade/fade_collision.gd" id="1_xrp2u"]
[node name="FadeCollision" type="Node3D"]
script = ExtResource("1_xrp2u")

View File

@@ -0,0 +1,993 @@
@tool
@icon("res://addons/godot-xr-tools/editor/icons/body.svg")
class_name XRToolsPlayerBody
extends CharacterBody3D
## XR Tools Player Physics Body Script
##
## This node provides the player with a physics body. The body is a
## [CapsuleShape3D] which tracks the player location as measured by the
## [XRCamera3D] for the players head.
##
## The player body can detect when the player is in the air, on the ground,
## or on a steep slope.
##
## Player movement is achieved by a number of movement providers attached to
## either the player or their controllers.
##
## After the player body moves, the [XROrigin3D] is updated as necessary to
## track the players movement.
## Signal emitted when the player jumps.
signal player_jumped()
## Signal emitted when the player teleports.
signal player_teleported(delta_transform)
## Signal emitted when the player bounces.
signal player_bounced(collider, magnitude)
## Signal emitted when the player has moved (excluding teleport).
## This only captures movement handled by the player body logic.
signal player_moved(delta_transform)
## Enumeration indicating when ground control can be used
enum GroundControl {
ON_GROUND, ## Apply ground control only when on ground
NEAR_GROUND, ## Apply ground control when near ground
ALWAYS ## Apply ground control always
}
## Ground distance considered "on" the ground
const ON_GROUND_DISTANCE := 0.1
## Ground distance considered "near" the ground
const NEAR_GROUND_DISTANCE := 1.0
## If true, the player body performs physics processing and movement
@export var enabled : bool = true: set = set_enabled
@export_group("Player setup")
## Automatically calibrate player body on next frame
@export var player_calibrate_height : bool = true
## Radius of the player body collider
@export var player_radius : float = 0.2: set = set_player_radius
## Player head height (distance between between camera and top of head)
@export var player_head_height : float = 0.1
## Minimum player height
@export var player_height_min : float = 0.6
## Maximum player height
@export var player_height_max : float = 2.5
## Slew-rate for player height overriding (button-crouch)
@export var player_height_rate : float = 4.0
## Eyes forward offset from center of body in player_radius units
@export_range(0.0, 1.0) var eye_forward_offset : float = 0.5
## Mix factor for body orientation
@export_range(0.0, 1.0) var body_forward_mix : float = 0.75
## Maximum distance the head may move away from the player body
@export_range(0.0, 2.0, 0.01) var max_head_distance = 1.0
## Behaviour mode when players head collides, or moves beyond [member max_head_distance].
## Push away, pushes the player body away.
## Fade, fades view to black.
@export_enum("Push away", "Fade", "Disabled") var head_behavior_mode = 1
@export_group("Collisions")
## Lets the player push rigid bodies
@export var push_rigid_bodies : bool = true
## If push_rigid_bodies is enabled, provides a strength factor for the impulse
@export var push_strength_factor : float = 1.0
@export_group("Physics")
## Default ground physics settings
@export var physics : XRToolsGroundPhysicsSettings: set = set_physics
## Option for specifying when ground control is allowed
@export var ground_control : GroundControl = GroundControl.ON_GROUND
## Player 3D Velocity - modified by [XRToolsMovementProvider] nodes
#var velocity : Vector3 = Vector3.ZERO
## Current player gravity
var gravity : Vector3 = Vector3.ZERO
## Set true when the player is on the ground
var on_ground : bool = true
## Set true when the player is near the ground
var near_ground : bool = true
## Normal vector for the ground under the player
var ground_vector : Vector3 = Vector3.UP
## Ground slope angle
var ground_angle : float = 0.0
## Ground node the player is touching
var ground_node : Node3D = null
## Ground physics override (if present)
var ground_physics : XRToolsGroundPhysicsSettings = null
## Ground control velocity - modifiable by [XRToolsMovementProvider] nodes
var ground_control_velocity : Vector2 = Vector2.ZERO
## Player height offset - used for height calibration
var player_height_offset : float = 0.0
## Velocity of the ground under the players feet
var ground_velocity : Vector3 = Vector3.ZERO
## Gravity-based "up" direction
var up_gravity := Vector3.UP
## Player-based "up" direction
var up_player := Vector3.UP
# Array of [XRToolsMovementProvider] nodes for the player
var _movement_providers := Array()
# Player height overrides
var _player_height_overrides := { }
# Player height override - current height
var _player_height_override_current : float = 0.0
# Player height override - target height
var _player_height_override_target : float = 0.0
# Player height override - enabled
var _player_height_override_enabled : bool = false
# Player height override - lerp between real and override
var _player_height_override_lerp : float = 0.0
# Previous ground node
var _previous_ground_node : Node3D = null
# Previous ground local position
var _previous_ground_local : Vector3 = Vector3.ZERO
# Previous ground global position
var _previous_ground_global : Vector3 = Vector3.ZERO
# Player body Collision node
var _collision_node : CollisionShape3D
# Player head shape cast
var _head_shape_cast : ShapeCast3D
# True while we're handling physics
var _in_physics_movement : bool = false
# Fade object
var _fade : XRToolsFade
# Fade value
var _fade_value : float = 0.0
## XROrigin3D node
@onready var origin_node : XROrigin3D = XRHelpers.get_xr_origin(self)
## XRCamera3D node
@onready var camera_node : XRCamera3D = XRHelpers.get_xr_camera(self)
## Left hand XRController3D node
@onready var left_hand_node : XRController3D = XRHelpers.get_left_controller(self)
## Right hand XRController3D node
@onready var right_hand_node : XRController3D = XRHelpers.get_right_controller(self)
## Default physics (if not specified by the user or the current ground)
@onready var default_physics = _guaranteed_physics()
## Function to sort movement providers by order
func sort_by_order(a, b) -> bool:
return true if a.order < b.order else false
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsPlayerBody"
# Called when the node enters the scene tree for the first time.
func _ready():
if Engine.is_editor_hint():
# In editing, keep player body linked to our origin
set_as_top_level(false)
transform = Transform3D()
else:
# Set as toplevel means our PlayerBody is positioned in global space.
# It is not moved when its parent moves.
set_as_top_level(true)
if get_parent():
# Make sure we're positioned correctly at the start.
global_transform = get_parent().global_transform
# Create our collision shape, height will be updated later
var capsule = CapsuleShape3D.new()
capsule.radius = player_radius
capsule.height = 1.4
_collision_node = CollisionShape3D.new()
_collision_node.shape = capsule
_collision_node.transform.origin = Vector3(0.0, 0.8, 0.0)
add_child(_collision_node)
# Create the shape-cast for head collisions
_head_shape_cast = ShapeCast3D.new()
_head_shape_cast.enabled = false
_head_shape_cast.exclude_parent = true
_head_shape_cast.margin = 0.01
_head_shape_cast.collision_mask = collision_mask
_head_shape_cast.max_results = 1
_head_shape_cast.shape = SphereShape3D.new()
_head_shape_cast.shape.radius = player_radius
add_child(_head_shape_cast)
# Get the movement providers ordered by increasing order
_movement_providers = get_tree().get_nodes_in_group("movement_providers")
_movement_providers.sort_custom(sort_by_order)
# Propagate defaults
_update_enabled()
_update_player_radius()
func set_enabled(new_value) -> void:
enabled = new_value
if is_inside_tree():
_update_enabled()
func _update_enabled() -> void:
# Update collision_shape
if _collision_node:
_collision_node.disabled = !enabled
# Update physics processing
if enabled:
set_physics_process(true)
func set_player_radius(new_value: float) -> void:
player_radius = new_value
if is_inside_tree():
_update_player_radius()
func _update_player_radius() -> void:
if _collision_node and _collision_node.shape:
_collision_node.shape.radius = player_radius
func set_physics(new_value: XRToolsGroundPhysicsSettings) -> void:
# Save the property
physics = new_value
default_physics = _guaranteed_physics()
func _physics_process(delta: float):
# Do not run physics if in the editor
if Engine.is_editor_hint():
return
# If disabled then turn of physics processing and bail out
if !enabled:
set_physics_process(false)
return
# We're handling physics right now
_in_physics_movement = true
# Remember where we are now
var current_transform : Transform3D = global_transform
# Calculate the players "up" direction and plane
up_player = origin_node.global_transform.basis.y
# Determine environmental gravity
var gravity_state := PhysicsServer3D.body_get_direct_state(get_rid())
gravity = gravity_state.total_gravity
# Update the kinematic body to be under the camera
_update_body_under_camera(delta)
# Allow the movement providers a chance to perform pre-movement updates. The providers can:
# - Adjust the gravity direction
for p in _movement_providers:
if p.enabled:
p.physics_pre_movement(delta, self)
# Determine the gravity "up" direction and plane
if gravity.is_equal_approx(Vector3.ZERO):
# Gravity too weak - use player
up_gravity = up_player
else:
# Use gravity direction
up_gravity = -gravity.normalized()
# Update the ground information
_update_ground_information(delta)
# Get the player body location before movement occurs
var position_before_movement := global_transform.origin
# Run the movement providers in order. The providers can:
# - Move the kinematic node around (to move the player)
# - Rotate the XROrigin3D around the camera (to rotate the player)
# - Read and modify the player velocity
# - Read and modify the ground-control velocity
# - Perform exclusive updating of the player (bypassing other movement providers)
# - Request a jump
# - Modify gravity direction
ground_control_velocity = Vector2.ZERO
var exclusive := false
for p in _movement_providers:
if p.is_active or (p.enabled and not exclusive):
if p.physics_movement(delta, self, exclusive):
exclusive = true
# If no controller has performed an exclusive-update then apply gravity and
# perform any ground-control
if !exclusive:
if on_ground and ground_physics.stop_on_slope and ground_angle < ground_physics.move_max_slope:
# Apply gravity towards slope to prevent sliding
velocity += -ground_vector * gravity.length() * delta
else:
# Apply gravity
velocity += gravity * delta
_apply_velocity_and_control(delta)
# Apply the player-body movement to the XR origin
var movement := global_transform.origin - position_before_movement
origin_node.global_transform.origin += movement
# Orient the player towards (potentially modified) gravity
slew_up(-gravity.normalized(), 5.0 * delta)
# If we moved our player, emit signal
var delta_transform : Transform3D = global_transform * current_transform.inverse()
if delta_transform.origin.length() > 0.001:
player_moved.emit(delta_transform)
# And we're done!
_in_physics_movement = false
## Teleport the player body.
## This moves the player without checking for collisions.
func teleport(target : Transform3D) -> void:
var inv_global_transform : Transform3D = global_transform.inverse()
# Get the player-to-origin transform
var player_to_origin : Transform3D = inv_global_transform * origin_node.global_transform
# Set the player
global_transform = target
# Set the origin
origin_node.global_transform = target * player_to_origin
# Report the player teleported
player_teleported.emit(target * inv_global_transform)
## Request a jump
func request_jump(skip_jump_velocity := false):
# Skip if not on ground
if !on_ground:
return
# Skip if we have any vertical velocity with regards to the ground-plane
var ground_relative := velocity - ground_velocity
if abs(ground_relative.dot(ground_vector)) > 0.01:
return
# Skip if jump disabled on this ground
var jump_velocity := XRToolsGroundPhysicsSettings.get_jump_velocity(
ground_physics, default_physics)
if jump_velocity == 0.0:
return
# Skip if the ground is too steep to jump
var max_slope := XRToolsGroundPhysicsSettings.get_jump_max_slope(
ground_physics, default_physics)
if ground_angle > max_slope:
return
# Perform the jump
if !skip_jump_velocity:
velocity += ground_vector * jump_velocity * XRServer.world_scale
# Report the jump
emit_signal("player_jumped")
## This method moves the players body using the provided velocity. Movement
## providers may use this function if they are exclusively driving the player.
func move_player(p_velocity: Vector3) -> Vector3:
velocity = p_velocity
max_slides = 4
up_direction = up_gravity
# Get the player body location before we apply our movement.
var transform_before_movement : Transform3D = global_transform
move_and_slide()
if not _in_physics_movement:
# Apply the player-body movement to the XR origin
var movement := global_transform.origin - transform_before_movement.origin
origin_node.global_transform.origin += movement
var delta_transform : Transform3D = global_transform * transform_before_movement.inverse()
if delta_transform.origin.length() > 0.001:
player_moved.emit(delta_transform)
# Check if we collided with rigid bodies and apply impulses to them to move them out of the way
if push_rigid_bodies:
for idx in range(get_slide_collision_count()):
var with = get_slide_collision(idx)
var obj = with.get_collider()
if obj.is_class("RigidBody3D"):
var rb : RigidBody3D = obj
# Get our relative impact velocity
var impact_velocity = p_velocity - rb.linear_velocity
# Determine the strength of the impulse we're about to give
var strength = impact_velocity.dot(-with.get_normal(0)) * push_strength_factor
# Our impulse is applied in the opposite direction
# of the normal of the surface we're hitting
var impulse = -with.get_normal(0) * strength
# Determine the location at which we're hitting in the object local space
# but in global orientation
var pos = with.get_position(0) - rb.global_transform.origin
# And apply the impulse
rb.apply_impulse(impulse, pos)
return velocity
## This method rotates the player by rotating the [XROrigin3D] around the camera.
func rotate_player(angle: float):
var inv_global_transform : Transform3D = global_transform.inverse()
var t1 := Transform3D()
var t2 := Transform3D()
var rot := Transform3D()
t1.origin = -camera_node.transform.origin
t2.origin = camera_node.transform.origin
rot = rot.rotated(Vector3.DOWN, angle)
origin_node.transform = (origin_node.transform * t2 * rot * t1).orthonormalized()
if not _in_physics_movement:
player_moved.emit(global_transform * inv_global_transform)
## This method slews the players up vector by rotating the [ARVROrigin] around
## the players feet.
func slew_up(up: Vector3, slew: float) -> void:
# Skip if the up vector is not valid
if up.is_equal_approx(Vector3.ZERO):
return
# Get the current origin
var current_origin := origin_node.global_transform
# Save the player foot global and local positions
var ref_pos_global := global_position
var ref_pos_local : Vector3 = ref_pos_global * current_origin
# Calculate the target origin
var target_origin := current_origin
target_origin.basis.y = up.normalized()
target_origin.basis.x = target_origin.basis.y.cross(target_origin.basis.z).normalized()
target_origin.basis.z = target_origin.basis.x.cross(target_origin.basis.y).normalized()
target_origin.origin = ref_pos_global - target_origin.basis * ref_pos_local
# Calculate the new origin
var new_origin := current_origin.interpolate_with(target_origin, slew).orthonormalized()
# Update the origin
origin_node.global_transform = new_origin
## This method calibrates the players height on the assumption
## the player is in rest position
func calibrate_player_height():
var base_height = camera_node.transform.origin.y + (player_head_height * XRServer.world_scale)
var player_height = XRToolsUserSettings.player_height * XRServer.world_scale
player_height_offset = (player_height - base_height) / XRServer.world_scale
## This method sets or clears a named height override
func override_player_height(key, value: float = -1.0):
# Clear or set the override
if value < 0.0:
_player_height_overrides.erase(key)
else:
_player_height_overrides[key] = value
# Evaluate whether a height override is active
var override = _player_height_overrides.values().min()
if override != null:
# Enable override with the target height
_player_height_override_target = override
_player_height_override_enabled = true
else:
# Disable height override
_player_height_override_enabled = false
# Estimate body forward direction
func _estimate_body_forward_dir() -> Vector3:
var forward = Vector3()
var camera_basis : Basis = camera_node.global_transform.basis
var camera_forward : Vector3 = -camera_basis.z
var camera_elevation := camera_forward.dot(up_player)
if camera_elevation > 0.75:
# User is looking up
forward = -camera_basis.y.slide(up_player).normalized()
elif camera_elevation < -0.75:
# User is looking down
forward = camera_basis.y.slide(up_player).normalized()
else:
forward = camera_forward.slide(up_player).normalized()
if (left_hand_node and left_hand_node.get_is_active()
and right_hand_node and right_hand_node.get_is_active()
and body_forward_mix > 0.0):
# See if we can mix in our estimated forward vector based on controller position
# Note, in Godot 4.0 we should check tracker confidence
var tangent = right_hand_node.global_transform.origin - left_hand_node.global_transform.origin
tangent = tangent.slide(up_player).normalized()
var hands_forward = up_player.cross(tangent).normalized()
# Rotate our forward towards our hand direction but not more than 60 degrees
var dot = forward.dot(hands_forward)
var cross = forward.cross(hands_forward).normalized()
var angle = clamp(acos(dot) * body_forward_mix, 0.0, 0.33 * PI)
forward = forward.rotated(cross, angle)
return forward
# This method updates the player body to match the player position
func _update_body_under_camera(delta : float):
# Initially calibration of player height
if player_calibrate_height:
calibrate_player_height()
player_calibrate_height = false
var adj_player_radius = player_radius * XRServer.world_scale
var adj_player_head_height = player_head_height * XRServer.world_scale
# Calculate the player height based on the camera position in the origin and the calibration
var player_height: float = clamp(
camera_node.transform.origin.y
+ adj_player_head_height
+ (player_height_offset * XRServer.world_scale),
player_height_min * XRServer.world_scale,
player_height_max * XRServer.world_scale)
# Manage any player height overriding such as:
# - Slewing between software override heights
# - Slewing the lerp between player and software-override heights
if _player_height_override_enabled:
# Update the current override height to the target height
if _player_height_override_lerp <= 0.0:
# Override not in use, snap to target
_player_height_override_current = _player_height_override_target
elif _player_height_override_current < _player_height_override_target:
# Override in use, slew up to target override height
_player_height_override_current = min(
_player_height_override_current + player_height_rate * delta,
_player_height_override_target)
elif _player_height_override_current > _player_height_override_target:
# Override in use, slew down to target override height
_player_height_override_current = max(
_player_height_override_current - player_height_rate * delta,
_player_height_override_target)
# Slew towards height being controlled by software-override
_player_height_override_lerp = min(
_player_height_override_lerp + player_height_rate * delta,
1.0)
else:
# Slew towards height being controlled by player
_player_height_override_lerp = max(
_player_height_override_lerp - player_height_rate * delta,
0.0)
# Blend the player height between the player and software-override
player_height = lerp(
player_height,
_player_height_override_current,
_player_height_override_lerp)
# Ensure player height makes mathematical sense
player_height = max(player_height, adj_player_radius)
# Test if the player is trying to get taller
var current_height : float = _collision_node.shape.height
if player_height > current_height:
# Calculate how tall we would like to get this frame
var target_height : float = min(
current_height + player_height_rate * delta,
player_height)
# Calculate a reduced height - slghtly smaller than the current player
# height so we can cast a virtual head up and probe the where we hit the
# ceiling.
var reduced_height : float = max(
current_height - 0.1,
adj_player_radius)
# Calculate how much we want to grow to hit the target height
var grow := target_height - reduced_height
# Cast the virtual head up from the reduced-height position up to the
# target height to check for ceiling collisions.
_head_shape_cast.shape.radius = adj_player_radius
_head_shape_cast.transform.origin.y = reduced_height - adj_player_radius
_head_shape_cast.collision_mask = collision_mask
_head_shape_cast.target_position = Vector3.UP * grow
_head_shape_cast.force_shapecast_update()
# Use the ceiling collision information to decide how much to grow the
# player height
var safe := _head_shape_cast.get_closest_collision_safe_fraction()
player_height = max(
reduced_height + grow * safe,
current_height)
# Adjust the collision shape to match the player geometry
_collision_node.shape.radius = adj_player_radius
_collision_node.shape.height = player_height
_collision_node.transform.origin.y = (player_height / 2.0)
# Center the kinematic body on the ground under the camera
var target_transform := global_transform
var camera_transform := camera_node.global_transform
target_transform.basis = origin_node.global_transform.basis
target_transform.origin = camera_transform.origin
target_transform.origin += up_player * (adj_player_head_height - player_height)
# The camera/eyes are towards the front of the body, so move the body back slightly
var forward_dir := _estimate_body_forward_dir()
if forward_dir.length() > 0.01:
target_transform = target_transform.looking_at(target_transform.origin + forward_dir, up_player)
target_transform.origin -= forward_dir.normalized() * eye_forward_offset * adj_player_radius
# If head behavior is disabled, just move
if head_behavior_mode == 2:
global_transform = target_transform
return
# Apply rotation
global_basis = target_transform.basis
# Always apply height
global_position += (target_transform.origin - global_position).project(global_basis.y)
# But do lateral movement with move and collide
var body_movement = target_transform.origin - global_position
var collision : KinematicCollision3D = move_and_collide(body_movement)
var fade : bool = false
if collision and collision.get_collision_count() > 0:
var camera_local_transform = global_transform.inverse() * camera_node.global_transform
var camera_local_position = camera_local_transform.origin
# Move it to our head center
camera_local_position += camera_local_transform.basis.z * eye_forward_offset * adj_player_radius
# If we can't move here, check if our head can move
_head_shape_cast.shape.radius = adj_player_head_height
_head_shape_cast.transform.origin.y = player_height - adj_player_head_height
_head_shape_cast.collision_mask = collision_mask
_head_shape_cast.target_position = (camera_local_position - _head_shape_cast.transform.origin) * Vector3(1.0, 0.0, 1.0)
var target_move_distance = _head_shape_cast.target_position.length()
# Cast shape
_head_shape_cast.force_shapecast_update()
# See how far we can move
var safe := min(_head_shape_cast.get_closest_collision_safe_fraction(), max_head_distance / target_move_distance)
if safe < 1.0:
# print("Attempted to move head from ", _head_shape_cast.transform.origin, " to ", camera_local_position, " => ", _head_shape_cast.target_position, ", safe: ", safe)
if head_behavior_mode == 0:
# Push body back, we actually move our player body into the collision,
# by the amount of movement left after the collision.
# Then in our actual move and slide we'll get pushed out.
# Do note that safe isn't super accurate.
var push_back_by = body_movement * (1.0 - safe)
global_position += push_back_by
else:
# Fade to black
fade = true
if fade:
if not _fade:
# Use global fade if we have one
_fade = XRToolsFade.get_fade_node()
if not _fade:
# Else create a local instance
var fade_scene : PackedScene = load("res://addons/godot-xr-tools/effects/fade.tscn")
_fade = fade_scene.instantiate()
add_child(_fade, false, Node.INTERNAL_MODE_BACK)
_fade_value = max(_fade_value + delta * 3.0, 0.0)
_fade.set_fade_level(self, Color(0, 0, 0, _fade_value))
elif _fade and _fade_value > 0.0:
_fade_value = max(_fade_value - delta * 3.0, 0.0)
_fade.set_fade_level(self, Color(0, 0, 0, _fade_value))
# Called when we're removed from the scene tree
func _exit_tree():
if _fade:
# Just in case our fade was global, make sure we clean up.
_fade.set_fade_level(self, Color(0 ,0 ,0 ,0 ))
# This method updates the information about the ground under the players feet
func _update_ground_information(delta: float):
# Test how close we are to the ground
var ground_collision := move_and_collide(
up_gravity * -NEAR_GROUND_DISTANCE, true)
# Handle no collision (or too far away to care about)
if !ground_collision:
near_ground = false
on_ground = false
ground_vector = up_gravity
ground_angle = 0.0
ground_node = null
ground_physics = null
_previous_ground_node = null
return
# Categorize the type of ground contact
near_ground = true
on_ground = ground_collision.get_travel().length() <= ON_GROUND_DISTANCE
# Save the ground information from the collision
ground_vector = ground_collision.get_normal()
ground_angle = rad_to_deg(ground_collision.get_angle(0, up_gravity))
ground_node = ground_collision.get_collider()
# Select the ground physics
var physics_node := ground_node.get_node_or_null("GroundPhysics") as XRToolsGroundPhysics
ground_physics = XRToolsGroundPhysics.get_physics(physics_node, default_physics)
# Detect if we're sliding on a wall
# TODO: consider reworking this magic angle
if ground_angle > 85:
on_ground = false
# Detect ground velocity under players feet
if _previous_ground_node == ground_node:
var pos_old := _previous_ground_global
var pos_new := ground_node.to_global(_previous_ground_local)
ground_velocity = (pos_new - pos_old) / delta
# Update ground velocity information
_previous_ground_node = ground_node
_previous_ground_global = ground_collision.get_position()
_previous_ground_local = ground_node.to_local(_previous_ground_global)
# This method applies the player velocity and ground-control velocity to the physical body
func _apply_velocity_and_control(delta: float):
# Calculate local velocity
var local_velocity := velocity - ground_velocity
# Split the velocity into horizontal and vertical components
var horizontal_velocity := local_velocity.slide(up_gravity)
var vertical_velocity := local_velocity - horizontal_velocity
# If the player is on the ground then give them control
if _can_apply_ground_control() and ground_control_velocity.length() >= 0.1:
# If ground control is being supplied then update the horizontal velocity
var control_velocity := Vector3.ZERO
var camera_transform := camera_node.global_transform
var dir_forward := camera_transform.basis.z.slide(up_gravity).normalized()
var dir_right := camera_transform.basis.x.slide(up_gravity).normalized()
control_velocity = (
dir_forward * -ground_control_velocity.y +
dir_right * ground_control_velocity.x
) * XRServer.world_scale
# Apply control velocity to horizontal velocity based on traction
var current_traction := XRToolsGroundPhysicsSettings.get_move_traction(
ground_physics, default_physics)
var traction_factor: float = clamp(current_traction * delta, 0.0, 1.0)
horizontal_velocity = horizontal_velocity.lerp(control_velocity, traction_factor)
# Prevent the player from moving up steep slopes
if on_ground:
var current_max_slope := XRToolsGroundPhysicsSettings.get_move_max_slope(
ground_physics, default_physics)
if ground_angle > current_max_slope:
# Get a vector in the down-hill direction
var down_direction := ground_vector.slide(up_gravity).normalized()
var vdot: float = down_direction.dot(horizontal_velocity)
if vdot < 0:
horizontal_velocity -= down_direction * vdot
# Combine the velocities back to a 3-space velocity
local_velocity = horizontal_velocity + vertical_velocity
# Move the player body with the desired velocity
velocity = move_player(local_velocity + ground_velocity)
# Apply ground-friction after the move
if _can_apply_ground_control() and ground_control_velocity.length() < 0.1:
# User is not trying to move, so apply the ground drag
var current_drag := XRToolsGroundPhysicsSettings.get_move_drag(
ground_physics, default_physics)
var drag_factor: float = clamp(current_drag * delta, 0, 1)
# Apply drag to horizontal velocity relative to ground
local_velocity = velocity - ground_velocity
horizontal_velocity = local_velocity.slide(up_gravity)
vertical_velocity = local_velocity - horizontal_velocity
horizontal_velocity = horizontal_velocity.lerp(Vector3.ZERO, drag_factor)
velocity = horizontal_velocity + vertical_velocity + ground_velocity
# Perform bounce test if a collision occurred
if get_slide_collision_count():
# Get the collider the player collided with
var collision := get_slide_collision(0)
var collision_node := collision.get_collider()
# Check for a GroundPhysics node attached to the collider
var collision_physics_node := \
collision_node.get_node_or_null("GroundPhysics") as XRToolsGroundPhysics
# Get the collision physics associated with the collider
var collision_physics = XRToolsGroundPhysics.get_physics(
collision_physics_node, default_physics)
# Get the bounce parameters associated with the collider
var bounce_threshold := XRToolsGroundPhysicsSettings.get_bounce_threshold(
collision_physics, default_physics)
var bounciness := XRToolsGroundPhysicsSettings.get_bounciness(
collision_physics, default_physics)
var magnitude := -collision.get_normal().dot(local_velocity)
# Detect if bounce should be performed
if bounciness > 0.0 and magnitude >= bounce_threshold:
local_velocity += 2 * collision.get_normal() * magnitude * bounciness
velocity = local_velocity + ground_velocity
emit_signal("player_bounced", collision_node, magnitude)
# Hack to ensure feet stick to ground (if not jumping)
# TODO: FIX
#if abs(velocity.y) < 0.001:
# velocity.y = ground_velocity.y
# Test if the player can apply ground control given the settings and the ground state.
func _can_apply_ground_control() -> bool:
match ground_control:
GroundControl.ON_GROUND:
return on_ground
GroundControl.NEAR_GROUND:
return near_ground
GroundControl.ALWAYS:
return true
_:
return false
# Get a guaranteed-valid physics
func _guaranteed_physics():
# Ensure we have a guaranteed-valid XRToolsGroundPhysicsSettings value
var valid_physics := physics as XRToolsGroundPhysicsSettings
if !valid_physics:
valid_physics = XRToolsGroundPhysicsSettings.new()
valid_physics.resource_name = "default"
# Return the guaranteed-valid physics
return valid_physics
# This method verifies the XRToolsPlayerBody has a valid configuration. Specifically it
# checks the following:
# - XROrigin3D can be identified
# - XRCamera3D can be identified
# - Player radius is valid
# - Maximum slope is valid
func _get_configuration_warnings() -> PackedStringArray:
var warnings := PackedStringArray()
# Check the origin node
var test_origin_node := XRHelpers.get_xr_origin(self)
if !test_origin_node:
warnings.append("Unable to find XR Origin node")
# Check the camera node
var test_camera_node := XRHelpers.get_xr_camera(self)
if !test_camera_node:
warnings.append("Unable to find XR Camera node")
# Verify the player radius is valid
if player_radius <= 0:
warnings.append("Player radius must be configured")
# Verify the player height minimum is valid
if player_height_min < player_radius * 2.0:
warnings.append("Player height minimum smaller than 2x radius")
# Verify the player height maximum is valid
if player_height_max < player_height_min:
warnings.append("Player height maximum cannot be smaller than minimum")
if head_behavior_mode == 1 and player_radius <= player_head_height:
warnings.append("When using fade mode, player radius should be larger than head height")
# Verify eye-forward does not allow near-clip-plane look through
var eyes_to_collider = (1.0 - eye_forward_offset) * player_radius
if test_camera_node and eyes_to_collider < test_camera_node.near:
warnings.append(
"Eyes too far forwards. Move eyes back or decrease camera near clipping plane")
# If specified, verify the ground physics is a valid type
if physics and !physics is XRToolsGroundPhysicsSettings:
warnings.append("Physics resource must be a GroundPhysicsSettings")
# Return warnings
return warnings
# Check property config
func _validate_property(property):
if property.name == "position" or property.name == "rotation" or property.name == "scale" \
or property.name == "rotation_edit_mode" or property.name == "rotation_order" \
or property.name == "top_level":
# We control these, don't let the user set them.
property.usage = PROPERTY_USAGE_NONE
## Find an [XRToolsPlayerBody] node.
##
## This function searches from the specified node for an [XRToolsPlayerBody]
## assuming the node is a sibling of the body under an [XROrigin3D].
static func find_instance(node: Node) -> XRToolsPlayerBody:
return XRTools.find_xr_child(
XRHelpers.get_xr_origin(node),
"*",
"XRToolsPlayerBody") as XRToolsPlayerBody

View File

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

View File

@@ -0,0 +1,10 @@
[gd_scene load_steps=2 format=3 uid="uid://diyu06cw06syv"]
[ext_resource type="Script" uid="uid://ck4yn3hxuobj7" path="res://addons/godot-xr-tools/player/player_body.gd" id="1"]
[node name="PlayerBody" type="CharacterBody3D" groups=["player_body"]]
process_priority = -100
process_physics_priority = -100
collision_layer = 524288
collision_mask = 1023
script = ExtResource("1")

View File

@@ -0,0 +1,222 @@
@tool
class_name XRToolsPoke
extends Node3D
## XR Tools Poke Script
##
## This node a finger push mechanism that can be attached to a finger bone
## using a [BoneAttachment3D].
##
## The poke can interact with user interfaces, and can optionally push rigid
## bodies.
## Signal emitted when this object pokes another object
signal pointing_event(event)
# Default layer of 18:player-hands
const DEFAULT_LAYER := 0b0000_0000_0000_0010_0000_0000_0000_0000
# Default mask [1..16] and 23:ui-objects
const DEFAULT_MASK := 0b0000_0000_0100_0000_1111_1111_1111_1111
## Enables or disables the poke functionality
@export var enabled : bool = true: set = set_enabled
## Sets the radius of the poke mesh and collision
@export var radius : float = 0.005: set = set_radius
## Set the color of the poke mesh
@export var color : Color = Color(0.8, 0.8, 1.0, 0.5): set = set_color
## Set the poke teleport distance
@export var teleport_distance : float = 0.1: set = set_teleport_distance
@export_category("Poke Collison")
## Sets the collision layer
@export_flags_3d_physics var layer : int = DEFAULT_LAYER: set = set_layer
## Sets the collision mask
@export_flags_3d_physics var mask : int = DEFAULT_MASK: set = set_mask
## Enables or disables pushing bodies
@export var push_bodies : bool = true: set = set_push_bodies
## Control the stiffness of the finger
@export var stiffness : float = 10.0: set = set_stiffness
## Control the maximum force the finger can push with
@export var maximum_force : float = 1.0: set = set_maximum_force
var is_ready = false
var material : StandardMaterial3D
var target : Node ## Node we last started touching
var last_collided_at : Vector3
func set_enabled(new_enabled : bool) -> void:
enabled = new_enabled
if is_ready:
_update_enabled()
func _update_enabled():
$PokeBody/CollisionShape.disabled = !enabled
func set_radius(new_radius : float) -> void:
radius = new_radius
if is_ready:
_update_radius()
func _update_radius() -> void:
# Calculate the user-scaled radius
var sr := radius * XRServer.world_scale
# Update the collision shape
var shape : SphereShape3D = $PokeBody/CollisionShape.shape
if shape:
shape.radius = sr
# Update the mesh shape
var mesh : SphereMesh = $PokeBody/MeshInstance.mesh
if mesh:
mesh.radius = sr
mesh.height = sr * 2.0
func set_teleport_distance(new_distance : float) -> void:
teleport_distance = new_distance
if is_ready:
_update_teleport_distance()
func _update_teleport_distance() -> void:
$PokeBody.teleport_distance = teleport_distance
func set_push_bodies(new_push_bodies : bool) -> void:
push_bodies = new_push_bodies
if is_ready:
_update_push_bodies()
func _update_push_bodies() -> void:
$PokeBody.push_bodies = push_bodies
func set_layer(new_layer : int) -> void:
layer = new_layer
if is_ready:
_update_layer()
func _update_layer() -> void:
$PokeBody.collision_layer = layer
func set_mask(new_mask : int) -> void:
mask = new_mask
if is_ready:
_update_mask()
func _update_mask() -> void:
$PokeBody.collision_mask = mask
func set_stiffness(new_stiffness : float) -> void:
stiffness = new_stiffness
if is_ready:
_update_stiffness()
func _update_stiffness() -> void:
$PokeBody.stiffness = stiffness
func set_maximum_force(new_maximum_force : float) -> void:
maximum_force = new_maximum_force
if is_ready:
_update_maximum_force()
func _update_maximum_force() -> void:
$PokeBody.maximum_force = maximum_force
func set_color(new_color : Color) -> void:
color = new_color
if is_ready:
_update_color()
func _update_color() -> void:
if material:
material.albedo_color = color
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsPoke"
# Called when the node enters the scene tree for the first time.
func _ready():
# Set as top level ensures we're placing this object in global space
$PokeBody.set_as_top_level(true)
is_ready = true
# Construct the poke material
material = StandardMaterial3D.new()
material.flags_unshaded = true
material.flags_transparent = true
$PokeBody/MeshInstance.set_surface_override_material(0, material)
_update_enabled()
_update_radius()
_update_teleport_distance()
_update_layer()
_update_mask()
_update_push_bodies()
_update_stiffness()
_update_maximum_force()
_update_color()
# Detect hand scale changing
var hand := XRToolsHand.find_instance(self)
if hand:
hand.hand_scale_changed.connect(_on_hand_scale_changed)
func _process(_delta):
# If no target then disable processing
if not is_instance_valid(target):
set_process(false)
return
# Update moving on the target
var new_at : Vector3 = $PokeBody.global_transform.origin
XRToolsPointerEvent.moved(self, target, new_at, last_collided_at)
last_collided_at = new_at
func _on_hand_scale_changed(_scale : float) -> void:
# Update the radius to account for the new hand scale
_update_radius()
func _on_PokeBody_body_contact_start(body):
# We are going to poke this body at our current position.
# This will be slightly above the object but since this
# mostly targets Viewport2Din3D, this will work
# Report body pressed
target = body
last_collided_at = $PokeBody.global_transform.origin
XRToolsPointerEvent.entered(self, body, last_collided_at)
XRToolsPointerEvent.pressed(self, body, last_collided_at)
# Enable processing to track movement
set_process(true)
func _on_PokeBody_body_contact_end(body):
# Skip if not current target
if body != target:
return
# Report release
XRToolsPointerEvent.released(self, target, last_collided_at)
XRToolsPointerEvent.exited(self, target, last_collided_at)
target = null

View File

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

View File

@@ -0,0 +1,47 @@
[gd_scene load_steps=8 format=3 uid="uid://bjcxf427un2wp"]
[ext_resource type="Script" uid="uid://dbxa71yertw7v" path="res://addons/godot-xr-tools/player/poke/poke.gd" id="1"]
[ext_resource type="Script" uid="uid://bsrl2m5idli7k" path="res://addons/godot-xr-tools/player/poke/poke_body.gd" id="2"]
[ext_resource type="Script" uid="uid://dff408qq7s26e" path="res://addons/godot-xr-tools/rumble/rumbler.gd" id="3_41fwo"]
[ext_resource type="Resource" uid="uid://brci6umrcd157" path="res://addons/godot-xr-tools/rumble/tap_rumble.tres" id="4_mskmk"]
[sub_resource type="SphereShape3D" id="1"]
resource_local_to_scene = true
radius = 0.005
[sub_resource type="SphereMesh" id="2"]
resource_local_to_scene = true
radius = 0.005
height = 0.01
radial_segments = 32
rings = 16
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_uvdwq"]
transparency = 1
shading_mode = 0
albedo_color = Color(0.8, 0.8, 1, 0.5)
[node name="Poke" type="Node3D"]
script = ExtResource("1")
[node name="PokeBody" type="AnimatableBody3D" parent="."]
top_level = true
collision_layer = 131072
collision_mask = 4259839
sync_to_physics = false
script = ExtResource("2")
[node name="CollisionShape" type="CollisionShape3D" parent="PokeBody"]
shape = SubResource("1")
[node name="MeshInstance" type="MeshInstance3D" parent="PokeBody"]
mesh = SubResource("2")
surface_material_override/0 = SubResource("StandardMaterial3D_uvdwq")
[node name="Rumbler" type="Node" parent="."]
script = ExtResource("3_41fwo")
event = ExtResource("4_mskmk")
[connection signal="pointing_event" from="." to="Rumbler" method="rumble_pointer"]
[connection signal="body_contact_end" from="PokeBody" to="." method="_on_PokeBody_body_contact_end"]
[connection signal="body_contact_start" from="PokeBody" to="." method="_on_PokeBody_body_contact_start"]

View File

@@ -0,0 +1,96 @@
@tool
extends XRToolsForceBody
## Signal called when we start to contact an object
signal body_contact_start(node)
## Signal called when we end contact with an object
signal body_contact_end(node)
## Distance at which we teleport our poke body
@export var teleport_distance : float = 0.1
# Node currently in contact with
var _contact : Node3D = null
# Target XRToolsPoke
@onready var _target : XRToolsPoke = get_parent()
# Add support for is_xr_class on XRTools classes
func is_xr_class(xr_name: String) -> bool:
return xr_name == "XRToolsPokeBody" or super(xr_name)
func _validate_property(property):
if property.name == "top_level":
property.usage = PROPERTY_USAGE_NONE
func _ready():
# Do not initialise if in the editor
if Engine.is_editor_hint():
# In editor, show it at its start location
top_level = false
transform = Transform3D()
return
# In runtime, we control the position
top_level = true
# Connect to player body signals (if applicable)
var player_body = XRToolsPlayerBody.find_instance(self)
if player_body:
player_body.player_moved.connect(_on_player_moved)
player_body.player_teleported.connect(_on_player_teleported)
# Try moving to the parent Poke node
func _physics_process(_delta):
# Do not process if in the editor
if Engine.is_editor_hint():
return
# Calculate the movement to perform
var target := _target.global_position
var to_target := target - global_position
# Decide whether to teleport or slide
var old_contact := _contact
if to_target.length() > teleport_distance:
# Teleport to the target
global_position = target
else:
# Move and slide to the target
var collision := move_and_slide(to_target)
_contact = collision.collider if collision else null
# Report when we stop being in contact with the current object
if old_contact and old_contact != _contact:
body_contact_end.emit(old_contact)
# Report when we start touching a new object
if _contact and _contact != old_contact:
body_contact_start.emit(_contact)
# If our player moved, attempt to move our poke.
func _on_player_moved(delta_transform : Transform3D):
var target : Transform3D = delta_transform * global_transform
# Rotate
global_basis = target.basis
# And attempt to move (we'll detect contact change in physics process).
move_and_slide(target.origin - global_position)
force_update_transform()
# If our player teleported, just move.
func _on_player_teleported(delta_transform : Transform3D):
global_transform = delta_transform * global_transform
force_update_transform()

View File

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