Skip to content

Commit

Permalink
Add smooth player height control
Browse files Browse the repository at this point in the history
Smoothly blend between player-controlled-height and software-override-height.
Block player-height from growing when blocked by a low ceiling.
  • Loading branch information
Malcolmnixon committed Oct 8, 2023
1 parent 8d77446 commit d80ffba
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 35 deletions.
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
113 changes: 104 additions & 9 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 @@ -136,8 +139,17 @@ 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 +163,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 +208,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 @@ -252,7 +277,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 @@ -443,9 +468,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 +513,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 +527,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

0 comments on commit d80ffba

Please sign in to comment.