immersive-home/app/addons/godot-xr-tools/player/player_body.gd

823 lines
28 KiB
GDScript3
Raw Normal View History

2023-10-16 20:10:20 +03:00
@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()
## Signal emitted when the player bounces
signal player_bounced(collider, magnitude)
## 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
2023-12-14 01:03:03 +02:00
## Slew-rate for player height overriding (button-crouch)
@export var player_height_rate : float = 4.0
2023-10-16 20:10:20 +03:00
## 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
@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 := { }
2023-12-14 01:03:03 +02:00
# 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
2023-10-16 20:10:20 +03:00
# 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
2023-12-14 01:03:03 +02:00
# Player head shape cast
var _head_shape_cast : ShapeCast3D
2023-10-16 20:10:20 +03:00
## 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(name : String) -> bool:
return name == "XRToolsPlayerBody"
# Called when the node enters the scene tree for the first time.
func _ready():
# Set as toplevel means our PlayerBody is positioned in global space.
# It is not moved when its parent moves.
set_as_top_level(true)
# 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)
2023-12-14 01:03:03 +02:00
# Create the shape-cast for head collisions
_head_shape_cast = ShapeCast3D.new()
_head_shape_cast.enabled = false
_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)
2023-10-16 20:10:20 +03:00
# 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
# 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
2023-12-14 01:03:03 +02:00
_update_body_under_camera(delta)
2023-10-16 20:10:20 +03:00
# 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)
## Teleport the player body
func teleport(target : Transform3D) -> void:
# Get the player-to-origin transform
var player_to_origin = global_transform.inverse() * 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()
## Request a jump
func request_jump(skip_jump_velocity := false):
# Skip if not on ground
if !on_ground:
return
2023-12-14 01:03:03 +02:00
# Skip if our vertical velocity is not essentially the same as the ground
var ground_relative := velocity - ground_velocity
if abs(ground_relative.dot(up_player)) > 0.01:
return
2023-10-16 20:10:20 +03:00
# 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_body(p_velocity: Vector3) -> Vector3:
velocity = p_velocity
max_slides = 4
up_direction = ground_vector
move_and_slide()
# 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 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()
## 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
2023-12-14 01:03:03 +02:00
# Evaluate whether a height override is active
2023-10-16 20:10:20 +03:00
var override = _player_height_overrides.values().min()
2023-12-14 01:03:03 +02:00
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
2023-10-16 20:10:20 +03:00
# 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
2023-12-14 01:03:03 +02:00
func _update_body_under_camera(delta : float):
2023-10-16 20:10:20 +03:00
# Initially calibration of player height
if player_calibrate_height:
calibrate_player_height()
player_calibrate_height = false
# 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
+ (player_head_height * XRServer.world_scale)
+ (player_height_offset * XRServer.world_scale),
player_height_min * XRServer.world_scale,
player_height_max * XRServer.world_scale)
2023-12-14 01:03:03 +02:00
# 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)
2023-10-16 20:10:20 +03:00
# Ensure player height makes mathematical sense
player_height = max(player_height, player_radius)
2023-12-14 01:03:03 +02:00
# 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,
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 = player_radius
_head_shape_cast.transform.origin.y = reduced_height - 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)
2023-10-16 20:10:20 +03:00
# Adjust the collision shape to match the player geometry
_collision_node.shape.radius = 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 curr_transform := global_transform
var camera_transform := camera_node.global_transform
curr_transform.basis = origin_node.global_transform.basis
curr_transform.origin = camera_transform.origin
curr_transform.origin += up_player * (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:
curr_transform = curr_transform.looking_at(curr_transform.origin + forward_dir, up_player)
curr_transform.origin -= forward_dir.normalized() * eye_forward_offset * player_radius
# Set the body position
global_transform = curr_transform
# 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():
# If ground control is being supplied then update the horizontal velocity
var control_velocity := Vector3.ZERO
if abs(ground_control_velocity.x) > 0.1 or abs(ground_control_velocity.y) > 0.1:
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
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
else:
# 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)
horizontal_velocity = horizontal_velocity.lerp(control_velocity, drag_factor)
# 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_body(local_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:
2023-12-14 01:03:03 +02:00
local_velocity += 2 * collision.get_normal() * magnitude * bounciness
2023-10-16 20:10:20 +03:00
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")
# 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
## 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