Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add smooth player height control #519

Merged
merged 1 commit into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions VERSIONS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# 4.3.0
- Upgraded project to Godot 4.1 as the new minimum version.
- Added reporting of stage load errors.
- Blend player height changes and prevent the player from standing up under a low ceiling.

# 4.2.1
- Fixed snap-zones showing highlight when disabled.
Expand Down
130 changes: 109 additions & 21 deletions addons/godot-xr-tools/player/player_body.gd
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ const NEAR_GROUND_DISTANCE := 1.0
## 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

Expand Down Expand Up @@ -130,14 +133,20 @@ var up_player := Vector3.UP
# Array of [XRToolsMovementProvider] nodes for the player
var _movement_providers := Array()

# Jump cool-down counter
var _jump_cooldown := 0

# Player height overrides
var _player_height_overrides := { }

# Player height override (enabled when non-negative)
var _player_height_override : float = -1.0
# 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
Expand All @@ -151,6 +160,9 @@ 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)
Expand Down Expand Up @@ -193,6 +205,16 @@ func _ready():
_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)
Expand Down Expand Up @@ -240,10 +262,6 @@ func _physics_process(delta: float):
set_physics_process(false)
return

# Decrement the jump cool-down on each physics update
if _jump_cooldown:
_jump_cooldown -= 1

# Calculate the players "up" direction and plane
up_player = origin_node.global_transform.basis.y

Expand All @@ -252,7 +270,7 @@ func _physics_process(delta: float):
gravity = gravity_state.total_gravity

# Update the kinematic body to be under the camera
_update_body_under_camera()
_update_body_under_camera(delta)

# Allow the movement providers a chance to perform pre-movement updates. The providers can:
# - Adjust the gravity direction
Expand Down Expand Up @@ -325,14 +343,15 @@ func teleport(target : Transform3D) -> void:

## Request a jump
func request_jump(skip_jump_velocity := false):
# Skip if cooling down from a previous jump
if _jump_cooldown:
return;

# 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)
Expand All @@ -351,7 +370,6 @@ func request_jump(skip_jump_velocity := false):

# Report the jump
emit_signal("player_jumped")
_jump_cooldown = 4

## This method moves the players body using the provided velocity. Movement
## providers may use this function if they are exclusively driving the player.
Expand Down Expand Up @@ -443,9 +461,15 @@ func override_player_height(key, value: float = -1.0):
else:
_player_height_overrides[key] = value

# Set or clear the override value
# Evaluate whether a height override is active
var override = _player_height_overrides.values().min()
_player_height_override = override if override != null else -1.0
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:
Expand Down Expand Up @@ -482,7 +506,7 @@ func _estimate_body_forward_dir() -> Vector3:
return forward

# This method updates the player body to match the player position
func _update_body_under_camera():
func _update_body_under_camera(delta : float):
# Initially calibration of player height
if player_calibrate_height:
calibrate_player_height()
Expand All @@ -496,13 +520,77 @@ func _update_body_under_camera():
player_height_min * XRServer.world_scale,
player_height_max * XRServer.world_scale)

# Allow forced overriding of height
if _player_height_override >= 0.0:
player_height = _player_height_override * 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
Expand Down
77 changes: 51 additions & 26 deletions scenes/basic_movement_demo/basic_movement_demo.tscn
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[gd_scene load_steps=32 format=3 uid="uid://bbcamgruwhrq4"]
[gd_scene load_steps=36 format=3 uid="uid://bbcamgruwhrq4"]

[ext_resource type="PackedScene" uid="uid://qbmx03iibuuu" path="res://addons/godot-xr-tools/staging/scene_base.tscn" id="1"]
[ext_resource type="Script" path="res://scenes/demo_scene_base.gd" id="2_5ptmo"]
Expand All @@ -19,69 +19,82 @@
[ext_resource type="PackedScene" uid="uid://dipg8euybm3f1" path="res://scenes/basic_movement_demo/objects/instructions.tscn" id="12_qasi6"]
[ext_resource type="PackedScene" uid="uid://ct3p5sgwvkmva" path="res://assets/meshes/control_pad/control_pad.tscn" id="13_51xax"]
[ext_resource type="PackedScene" uid="uid://ca6c2h3xsflxf" path="res://assets/maps/holodeck_map.tscn" id="16_v54dt"]
[ext_resource type="Texture2D" uid="uid://bskwc0drdadnd" path="res://assets/wahooney.itch.io/brown_grid.png" id="20_k5co8"]

[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_kag4k"]
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_334h1"]
animation = &"Grip"

[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_yd4dj"]
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_owb1g"]
animation = &"Grip"

[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_ord26"]
[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_boi4n"]
filter_enabled = true
filters = ["Armature/Skeleton3D:Little_Distal_L", "Armature/Skeleton3D:Little_Intermediate_L", "Armature/Skeleton3D:Little_Metacarpal_L", "Armature/Skeleton3D:Little_Proximal_L", "Armature/Skeleton3D:Middle_Distal_L", "Armature/Skeleton3D:Middle_Intermediate_L", "Armature/Skeleton3D:Middle_Metacarpal_L", "Armature/Skeleton3D:Middle_Proximal_L", "Armature/Skeleton3D:Ring_Distal_L", "Armature/Skeleton3D:Ring_Intermediate_L", "Armature/Skeleton3D:Ring_Metacarpal_L", "Armature/Skeleton3D:Ring_Proximal_L", "Armature/Skeleton3D:Thumb_Distal_L", "Armature/Skeleton3D:Thumb_Metacarpal_L", "Armature/Skeleton3D:Thumb_Proximal_L", "Armature/Skeleton:Little_Distal_L", "Armature/Skeleton:Little_Intermediate_L", "Armature/Skeleton:Little_Proximal_L", "Armature/Skeleton:Middle_Distal_L", "Armature/Skeleton:Middle_Intermediate_L", "Armature/Skeleton:Middle_Proximal_L", "Armature/Skeleton:Ring_Distal_L", "Armature/Skeleton:Ring_Intermediate_L", "Armature/Skeleton:Ring_Proximal_L", "Armature/Skeleton:Thumb_Distal_L", "Armature/Skeleton:Thumb_Proximal_L"]

[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_qxgwl"]
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_fnyu5"]
animation = &"Grip 5"

[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_emf7m"]
[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_v8rnq"]
filter_enabled = true
filters = ["Armature/Skeleton3D:Index_Distal_L", "Armature/Skeleton3D:Index_Intermediate_L", "Armature/Skeleton3D:Index_Metacarpal_L", "Armature/Skeleton3D:Index_Proximal_L", "Armature/Skeleton:Index_Distal_L", "Armature/Skeleton:Index_Intermediate_L", "Armature/Skeleton:Index_Proximal_L"]

[sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_tyo0e"]
[sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_ljr1p"]
graph_offset = Vector2(-536, 11)
nodes/ClosedHand1/node = SubResource("AnimationNodeAnimation_kag4k")
nodes/ClosedHand1/node = SubResource("AnimationNodeAnimation_334h1")
nodes/ClosedHand1/position = Vector2(-600, 300)
nodes/ClosedHand2/node = SubResource("AnimationNodeAnimation_yd4dj")
nodes/ClosedHand2/node = SubResource("AnimationNodeAnimation_owb1g")
nodes/ClosedHand2/position = Vector2(-360, 300)
nodes/Grip/node = SubResource("AnimationNodeBlend2_ord26")
nodes/Grip/node = SubResource("AnimationNodeBlend2_boi4n")
nodes/Grip/position = Vector2(0, 20)
nodes/OpenHand/node = SubResource("AnimationNodeAnimation_qxgwl")
nodes/OpenHand/node = SubResource("AnimationNodeAnimation_fnyu5")
nodes/OpenHand/position = Vector2(-600, 100)
nodes/Trigger/node = SubResource("AnimationNodeBlend2_emf7m")
nodes/Trigger/node = SubResource("AnimationNodeBlend2_v8rnq")
nodes/Trigger/position = Vector2(-360, 20)
node_connections = [&"output", 0, &"Grip", &"Grip", 0, &"Trigger", &"Grip", 1, &"ClosedHand2", &"Trigger", 0, &"OpenHand", &"Trigger", 1, &"ClosedHand1"]

[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_hpaa3"]
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_3t6qm"]
animation = &"Grip"

[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_46u4t"]
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_3i43j"]
animation = &"Grip"

[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_xf6le"]
[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_n5y7i"]
filter_enabled = true
filters = ["Armature/Skeleton3D:Little_Distal_R", "Armature/Skeleton3D:Little_Intermediate_R", "Armature/Skeleton3D:Little_Metacarpal_R", "Armature/Skeleton3D:Little_Proximal_R", "Armature/Skeleton3D:Middle_Distal_R", "Armature/Skeleton3D:Middle_Intermediate_R", "Armature/Skeleton3D:Middle_Metacarpal_R", "Armature/Skeleton3D:Middle_Proximal_R", "Armature/Skeleton3D:Ring_Distal_R", "Armature/Skeleton3D:Ring_Intermediate_R", "Armature/Skeleton3D:Ring_Metacarpal_R", "Armature/Skeleton3D:Ring_Proximal_R", "Armature/Skeleton3D:Thumb_Distal_R", "Armature/Skeleton3D:Thumb_Metacarpal_R", "Armature/Skeleton3D:Thumb_Proximal_R", "Armature/Skeleton:Little_Distal_R", "Armature/Skeleton:Little_Intermediate_R", "Armature/Skeleton:Little_Proximal_R", "Armature/Skeleton:Middle_Distal_R", "Armature/Skeleton:Middle_Intermediate_R", "Armature/Skeleton:Middle_Proximal_R", "Armature/Skeleton:Ring_Distal_R", "Armature/Skeleton:Ring_Intermediate_R", "Armature/Skeleton:Ring_Proximal_R", "Armature/Skeleton:Thumb_Distal_R", "Armature/Skeleton:Thumb_Proximal_R"]

[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_uoku2"]
[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_q31qn"]
animation = &"Grip 5"

[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_xxigd"]
[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_shlyq"]
filter_enabled = true
filters = ["Armature/Skeleton3D:Index_Distal_R", "Armature/Skeleton3D:Index_Intermediate_R", "Armature/Skeleton3D:Index_Metacarpal_R", "Armature/Skeleton3D:Index_Proximal_R", "Armature/Skeleton:Index_Distal_R", "Armature/Skeleton:Index_Intermediate_R", "Armature/Skeleton:Index_Proximal_R"]

[sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_s02f4"]
[sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_gmumo"]
graph_offset = Vector2(-552.664, 107.301)
nodes/ClosedHand1/node = SubResource("AnimationNodeAnimation_hpaa3")
nodes/ClosedHand1/node = SubResource("AnimationNodeAnimation_3t6qm")
nodes/ClosedHand1/position = Vector2(-600, 300)
nodes/ClosedHand2/node = SubResource("AnimationNodeAnimation_46u4t")
nodes/ClosedHand2/node = SubResource("AnimationNodeAnimation_3i43j")
nodes/ClosedHand2/position = Vector2(-360, 300)
nodes/Grip/node = SubResource("AnimationNodeBlend2_xf6le")
nodes/Grip/node = SubResource("AnimationNodeBlend2_n5y7i")
nodes/Grip/position = Vector2(0, 40)
nodes/OpenHand/node = SubResource("AnimationNodeAnimation_uoku2")
nodes/OpenHand/node = SubResource("AnimationNodeAnimation_q31qn")
nodes/OpenHand/position = Vector2(-600, 100)
nodes/Trigger/node = SubResource("AnimationNodeBlend2_xxigd")
nodes/Trigger/node = SubResource("AnimationNodeBlend2_shlyq")
nodes/Trigger/position = Vector2(-360, 40)
node_connections = [&"output", 0, &"Grip", &"Grip", 0, &"Trigger", &"Grip", 1, &"ClosedHand2", &"Trigger", 0, &"OpenHand", &"Trigger", 1, &"ClosedHand1"]

[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_hw50d"]
albedo_texture = ExtResource("20_k5co8")
uv1_scale = Vector3(0.5, 0.5, 0.5)
uv1_triplanar = true

[sub_resource type="BoxMesh" id="BoxMesh_urqsc"]
material = SubResource("StandardMaterial3D_hw50d")
size = Vector3(2, 0.5, 8)

[sub_resource type="BoxShape3D" id="BoxShape3D_24jr5"]
size = Vector3(2, 0.5, 8)

[node name="BasicMovementDemo" instance=ExtResource("1")]
script = ExtResource("2_5ptmo")

Expand Down Expand Up @@ -119,7 +132,7 @@ mask = 4194304
push_bodies = false

[node name="AnimationTree" parent="XROrigin3D/LeftHand/LeftHand" index="1"]
tree_root = SubResource("AnimationNodeBlendTree_tyo0e")
tree_root = SubResource("AnimationNodeBlendTree_ljr1p")

[node name="MovementDirect" parent="XROrigin3D/LeftHand" index="1" instance=ExtResource("5")]
strafe = true
Expand Down Expand Up @@ -167,7 +180,7 @@ mask = 4194304
push_bodies = false

[node name="AnimationTree" parent="XROrigin3D/RightHand/RightHand" index="1"]
tree_root = SubResource("AnimationNodeBlendTree_s02f4")
tree_root = SubResource("AnimationNodeBlendTree_gmumo")

[node name="MovementDirect" parent="XROrigin3D/RightHand" index="1" instance=ExtResource("5")]

Expand All @@ -191,8 +204,10 @@ crouch_type = 1

[node name="MainMenuTeleport" parent="." index="2" instance=ExtResource("11")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 7)
scene_base = NodePath("..")
title = ExtResource("12")
spawn_point_name = ""
spawn_point_position = Vector3(0, 0, 0)
spawn_point_transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0)

[node name="Instructions" parent="." index="3" instance=ExtResource("12_qasi6")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4, -4)
Expand All @@ -203,6 +218,16 @@ transform = Transform3D(-4.37114e-08, 0, -1, 0, 1, 0, 1, 0, -4.37114e-08, 6, 0,
[node name="Mound" parent="." index="5" instance=ExtResource("9")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -7, 0, 0)

[node name="Block" type="StaticBody3D" parent="." index="6"]
transform = Transform3D(-4.2222e-08, -0.258819, 0.965926, -1.13133e-08, 0.965926, 0.258819, -1, 0, -4.37114e-08, 0, 2.5, -10)

[node name="MeshInstance3D" type="MeshInstance3D" parent="Block" index="0"]
mesh = SubResource("BoxMesh_urqsc")

[node name="CollisionShape3D" type="CollisionShape3D" parent="Block" index="1"]
transform = Transform3D(1, -8.88178e-16, -3.55271e-15, 0, 1, 0, 0, 0, 1, 0, 0, 0)
shape = SubResource("BoxShape3D_24jr5")

[editable path="XROrigin3D/LeftHand/LeftHand"]
[editable path="XROrigin3D/LeftHand/LeftHand/Hand_Nails_low_L"]
[editable path="XROrigin3D/RightHand/RightHand"]
Expand Down
Loading