From 49a2c795137f6270c855d4eb40042cbf59cae5be Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Thu, 7 Sep 2023 19:55:21 -0400 Subject: [PATCH] Add smooth player height control Smoothly blend between player-controlled-height and software-override-height. Block player-height from growing when blocked by a low ceiling. Prevent double-jumping when jumping and standing-up at the same time. --- VERSIONS.md | 1 + addons/godot-xr-tools/player/player_body.gd | 130 +++++++++++++++--- .../basic_movement_demo.tscn | 77 +++++++---- 3 files changed, 161 insertions(+), 47 deletions(-) diff --git a/VERSIONS.md b/VERSIONS.md index a88a5221..d89f7541 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -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. diff --git a/addons/godot-xr-tools/player/player_body.gd b/addons/godot-xr-tools/player/player_body.gd index 9efb51d6..2fb897e5 100644 --- a/addons/godot-xr-tools/player/player_body.gd +++ b/addons/godot-xr-tools/player/player_body.gd @@ -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 @@ -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 @@ -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) @@ -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) @@ -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 @@ -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 @@ -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) @@ -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. @@ -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: @@ -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() @@ -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 diff --git a/scenes/basic_movement_demo/basic_movement_demo.tscn b/scenes/basic_movement_demo/basic_movement_demo.tscn index abb33d61..87bc618a 100644 --- a/scenes/basic_movement_demo/basic_movement_demo.tscn +++ b/scenes/basic_movement_demo/basic_movement_demo.tscn @@ -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"] @@ -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") @@ -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 @@ -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")] @@ -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) @@ -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"]