823 lines
28 KiB
GDScript
823 lines
28 KiB
GDScript
@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
|
|
|
|
## 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
|
|
|
|
@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
|
|
|
|
|
|
## 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)
|
|
|
|
# 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)
|
|
|
|
# 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
|
|
_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)
|
|
|
|
|
|
## 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
|
|
|
|
# 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
|
|
|
|
# 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
|
|
|
|
# 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
|
|
|
|
# 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)
|
|
|
|
# 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, 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,
|
|
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)
|
|
|
|
# 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:
|
|
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")
|
|
|
|
# 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
|