diff --git a/src/ingame/player.gd b/src/ingame/player.gd index aa40acf..a552e29 100644 --- a/src/ingame/player.gd +++ b/src/ingame/player.gd @@ -85,6 +85,8 @@ func _physics_process(delta: float) -> void: velocity.x = move_toward(velocity.x, 0, SPEED) velocity.z = move_toward(velocity.z, 0, SPEED) + call("stair_step_up", direction) + if move_and_slide() and is_on_floor(): for idx in range(get_slide_collision_count()): var collision := get_slide_collision(idx) @@ -94,6 +96,8 @@ func _physics_process(delta: float) -> void: global_position = spawn_point.global_position reset_physics_interpolation() + call("stair_step_down") + @rpc("any_peer", "call_local", "reliable") func hold_thing() -> void: diff --git a/src/ingame/player.tscn b/src/ingame/player.tscn index 4a33807..eceb4b3 100644 --- a/src/ingame/player.tscn +++ b/src/ingame/player.tscn @@ -1,6 +1,6 @@ [gd_scene load_steps=12 format=3 uid="uid://cs8c570bxh6u"] -[ext_resource type="Script" path="res://src/ingame/player.gd" id="1_isrmf"] +[ext_resource type="Script" path="res://src/lib/player_character.gd" id="1_sba4x"] [ext_resource type="PackedScene" uid="uid://tdsbo3e5ic86" path="res://src/ingame/water_bomb.tscn" id="2_naek4"] [ext_resource type="AudioStream" uid="uid://3dlhs18w1fa2" path="res://assets/sfx/boom.wav" id="3_u2hxa"] [ext_resource type="Shader" path="res://scenes/interactivity_outline.gdshader" id="4_a2qfj"] @@ -49,7 +49,7 @@ roughness = 0.4 [node name="Player" type="CharacterBody3D" node_paths=PackedStringArray("_projectile_holder", "_projectile_point", "_camera_pivot", "_camera", "_line_of_sight", "_shot_sound")] collision_layer = 2 -script = ExtResource("1_isrmf") +script = ExtResource("1_sba4x") _projectile_scene = ExtResource("2_naek4") _projectile_holder = NodePath("ProjectileHolder") _projectile_point = NodePath("ProjectilePoint") diff --git a/src/lib/player_character.gd b/src/lib/player_character.gd new file mode 100644 index 0000000..79e253b --- /dev/null +++ b/src/lib/player_character.gd @@ -0,0 +1,134 @@ +class_name StairStepper extends Player +## Credits: +# Special thanks to Majikayo Games for original solution to stair_step_down! +# (https://youtu.be/-WjM1uksPIk) +# +# Special thanks to Myria666 for their paper on Quake movement mechanics (used for stair_step_up)! +# (https://github.com/myria666/qMovementDoc) +# +# Special thanks to Andicraft for their help with implementation stair_step_up! +# (https://github.com/Andicraft) + +## Notes: +# 0. All shape colliders are supported. Although, I would recommend Capsule colliders for enemies +# as it works better with the Navigation Meshes. Its up to you what shape you want to use +# for players. +# +# 1. To adjust the step-up/down height, just change the MAX_STEP_UP/MAX_STEP_DOWN values below. +# +# 2. This uses Jolt Physics as the default Godot Physics has a few bugs: +# 1: Small gaps that you should be able to fit through both ways will block you in Godot Physics. +# You can see this demonstrated with the floating boxes in front of the big stairs. +# 2: Walking into some objects may push the player downward by a small amount which causes +# jittering and causes the floor to be detected as a step. +# TLDR: This still works with default Godot Physics, although it feels a lot better in Jolt Physics. + +@export var MAX_STEP_UP := 0.5 # Maximum height in meters the player can step up. +@export var MAX_STEP_DOWN := -0.5 # Maximum height in meters the player can step down. + +var was_grounded: bool = true +var is_grounded: bool = true + + +func _physics_process(delta: float) -> void: + was_grounded = is_grounded + is_grounded = is_on_floor() + super._physics_process(delta) + + +# Function: Handle walking down stairs +func stair_step_down(): + if is_on_floor(): + return + + # If we're falling from a step + if velocity.y <= 0 and was_grounded: + # Initialize body test variables + var body_test_result = PhysicsTestMotionResult3D.new() + var body_test_params = PhysicsTestMotionParameters3D.new() + + body_test_params.from = self.global_transform ## We get the player's current global_transform + body_test_params.motion = Vector3(0, MAX_STEP_DOWN, 0) ## We project the player downward + + if PhysicsServer3D.body_test_motion(self.get_rid(), body_test_params, body_test_result): + # Enters if a collision is detected by body_test_motion + # Get distance to step and move player downward by that much + position.y += body_test_result.get_travel().y + apply_floor_snap() + is_grounded = true + + +# Function: Handle walking up stairs +func stair_step_up(wish_dir: Vector3): + if wish_dir == Vector3.ZERO: + return + + # 0. Initialize testing variables + var body_test_params = PhysicsTestMotionParameters3D.new() + var body_test_result = PhysicsTestMotionResult3D.new() + + var test_transform = global_transform ## Storing current global_transform for testing + var distance = wish_dir * 0.1 ## Distance forward we want to check + body_test_params.from = self.global_transform ## Self as origin point + body_test_params.motion = distance ## Go forward by current distance + + # Pre-check: Are we colliding? + if !PhysicsServer3D.body_test_motion(self.get_rid(), body_test_params, body_test_result): + ## If we don't collide, return + return + + # 1. Move test_transform to collision location + var remainder = body_test_result.get_remainder() ## Get remainder from collision + test_transform = test_transform.translated(body_test_result.get_travel()) ## Move test_transform by distance traveled before collision + + # 2. Move test_transform up to ceiling (if any) + var step_up = MAX_STEP_UP * Vector3.UP + body_test_params.from = test_transform + body_test_params.motion = step_up + PhysicsServer3D.body_test_motion(self.get_rid(), body_test_params, body_test_result) + test_transform = test_transform.translated(body_test_result.get_travel()) + + # 3. Move test_transform forward by remaining distance + body_test_params.from = test_transform + body_test_params.motion = remainder + PhysicsServer3D.body_test_motion(self.get_rid(), body_test_params, body_test_result) + test_transform = test_transform.translated(body_test_result.get_travel()) + + # 3.5 Project remaining along wall normal (if any) + ## So you can walk into wall and up a step + if body_test_result.get_collision_count() != 0: + remainder = body_test_result.get_remainder().length() + + ### Uh, there may be a better way to calculate this in Godot. + var wall_normal = body_test_result.get_collision_normal() + var dot_div_mag = wish_dir.dot(wall_normal) / (wall_normal * wall_normal).length() + var projected_vector = (wish_dir - dot_div_mag * wall_normal).normalized() + + body_test_params.from = test_transform + body_test_params.motion = remainder * projected_vector + PhysicsServer3D.body_test_motion(self.get_rid(), body_test_params, body_test_result) + test_transform = test_transform.translated(body_test_result.get_travel()) + + # 4. Move test_transform down onto step + body_test_params.from = test_transform + body_test_params.motion = MAX_STEP_UP * -Vector3.UP + + # Return if no collision + if !PhysicsServer3D.body_test_motion(self.get_rid(), body_test_params, body_test_result): + return + + test_transform = test_transform.translated(body_test_result.get_travel()) + + # 5. Check floor normal for un-walkable slope + var surface_normal = body_test_result.get_collision_normal() + var temp_floor_max_angle = floor_max_angle + deg_to_rad(20) + if (snappedf(surface_normal.angle_to(Vector3.UP), 0.001) > temp_floor_max_angle): + return + + + # 6. Move player up + var global_pos = global_position + var step_up_dist = test_transform.origin.y - global_pos.y + + global_pos.y = test_transform.origin.y + global_position = global_pos