mrezai/GodotStairs

Player bounces off slopes, keeps horizontal velocity when walking into walls and can't step up sloped stairs

Closed this issue · 1 comments

Hi, really like what's been made here. Would like to point out 3 issues with this player controller. Feel free to put this up as 3 issues on Github if you want.
A video demonstrating these 3 issues.

  • If the stairs are sloped differently, it doesn't step at all(haven't looked into how it behaves if the stairs are just sloped but the same angle).
  • If you walk up a slope that's too steep, you just bounce up and down it.
  • If you move in a particular direction then jump, the stair stepping triggers, but you keep all your horizontal velocity from walking into the wall, meaning you get adjusted up by the stair stepping and move off the platform uncontrollably due to the horizontal velocity.

Hi
This implementation should be considered as a proof of concept to start learning about its subject and isn't complete or bug free in any means.

  • You can modify STEP_MAX_SLOPE_DEGREE in Player's code to solve this problem but it may cause other problems!
  • This is default behaviour of move_and_slide, I have some ideas to fix it that I'll implement and test them later.
  • I think this issue is related more to how someone want character movement feels for example in fast shooter game, current implementation would be desirable but you can clamp horizontal velocity by keeping some state in the code like this(is_jumping added to code, also STEP_MAX_SLOPE_DEGREE is set to 30 for issue one) :
extends KinematicBody

const ACCEL_DEFAULT: float = 7.0
const ACCEL_AIR: float = 1.0
var accel: float = ACCEL_DEFAULT
const SPEED_DEFAULT: float = 7.0
const SPEED_ON_STAIRS: float = 5.0
var speed: float = SPEED_DEFAULT
var gravity: float = 9.8
var jump: float = 5.0
const stairs_feeling_coefficient: float = 2.5

var mouse_sense: float = 0.1
var snap: Vector3 = Vector3.ZERO

var direction: Vector3 = Vector3.ZERO
var velocity: Vector3 = Vector3.ZERO
var gravity_vec: Vector3 = Vector3.ZERO
var movement: Vector3 = Vector3.ZERO

onready var body = $Body
onready var head = $Body/Head
onready var camera = $Body/Head/Camera
onready var head_position: Vector3 = head.translation
onready var body_euler_y = body.global_transform.basis.get_euler().y

var head_offset: Vector3 = Vector3.ZERO
var is_step: bool = false

const WALL_MARGIN: float = 0.001
const STEP_HEIGHT_DEFAULT: Vector3 = Vector3(0, 0.6, 0)
const STEP_MAX_SLOPE_DEGREE: float = 30.0
const STEP_CHECK_COUNT: int = 2

var step_check_height: Vector3 = STEP_HEIGHT_DEFAULT / STEP_CHECK_COUNT

var camera_target_position : Vector3 = Vector3()
var camera_coefficient: float = 1.0
var time_in_air: float = 0.0
var is_jumping = false


func _ready():
	#hides the cursor
	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
	
	camera_target_position = camera.global_transform.origin
	camera.set_as_toplevel(true)
	camera.physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_OFF

func _process(delta: float) -> void:
	# Find the current interpolated transform of the target
	var tr : Transform = head.get_global_transform_interpolated()

	# Provide some delayed smoothed lerping towards the target position 
	camera_target_position = lerp(camera_target_position, tr.origin, delta * speed * stairs_feeling_coefficient * camera_coefficient)

	#camera.translation = camera_target_position
	camera.translation.x = tr.origin.x

	if is_on_floor():
		time_in_air = 0.0
		camera_coefficient = 1.0
		camera.translation.y = camera_target_position.y
	else:
		time_in_air += delta
		if time_in_air > 1.0:
			camera_coefficient += delta
			camera_coefficient = clamp(camera_coefficient, 2.0, 4.0)
		else: 
			camera_coefficient = 2.0

		camera.translation.y = camera_target_position.y

	camera.translation.z = tr.origin.z
	camera.rotation.x = head.rotation.x
	camera.rotation.y = body.rotation.y + body_euler_y

func _input(event):
	#get mouse input for camera rotation
	if event is InputEventMouseMotion:
		body.rotate_y(deg2rad(-event.relative.x * mouse_sense))
		head.rotate_x(deg2rad(-event.relative.y * mouse_sense))
		head.rotation.x = clamp(head.rotation.x, deg2rad(-89), deg2rad(89))

func _physics_process(delta):
	if Input.is_action_just_pressed("ui_cancel"):
		if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
			Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
		else:
			Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
		
	is_step = false
	
	#get keyboard input
	direction = Vector3.ZERO
	var h_rot: float = body.global_transform.basis.get_euler().y
	var f_input: float = Input.get_action_strength("move_backward") - Input.get_action_strength("move_forward")
	var h_input: float = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
	direction = Vector3(h_input, 0, f_input).rotated(Vector3.UP, h_rot).normalized()

	#jumping and gravity
	if is_on_floor():
		snap = -get_floor_normal()
		accel = ACCEL_DEFAULT
		gravity_vec = Vector3.ZERO
		is_jumping = false
	else:
		snap = Vector3.DOWN
		accel = ACCEL_AIR
		gravity_vec += Vector3.DOWN * gravity * delta

	if Input.is_action_just_pressed("jump") and is_on_floor():
		snap = Vector3.ZERO
		gravity_vec = Vector3.UP * jump
		is_jumping = true

	#make it move
	velocity = velocity.linear_interpolate(direction * speed, accel * delta)
	
	if gravity_vec.y >= 0:
		for i in range(STEP_CHECK_COUNT):
			var test_motion_result: PhysicsTestMotionResult = PhysicsTestMotionResult.new()
			
			var step_height: Vector3 = STEP_HEIGHT_DEFAULT - i * step_check_height
			var transform3d: Transform = global_transform
			var motion: Vector3 = step_height
			var is_player_collided: bool = PhysicsServer.body_test_motion(self.get_rid(), transform3d, motion, false, test_motion_result)
			
			if test_motion_result.collision_normal.y < 0:
				continue
				
			if not is_player_collided:
				transform3d.origin += step_height
				motion = velocity * delta
				is_player_collided = PhysicsServer.body_test_motion(self.get_rid(), transform3d, motion, false, test_motion_result)
				if not is_player_collided:
					transform3d.origin += motion
					motion = -step_height
					is_player_collided = PhysicsServer.body_test_motion(self.get_rid(), transform3d, motion, false, test_motion_result)
					if is_player_collided:
						if test_motion_result.collision_normal.angle_to(Vector3.UP) <= deg2rad(STEP_MAX_SLOPE_DEGREE):
							head_offset = -test_motion_result.motion_remainder
							is_step = true
							global_transform.origin += -test_motion_result.motion_remainder
							break
				else:
					var wall_collision_normal: Vector3 = test_motion_result.collision_normal

					transform3d.origin += test_motion_result.collision_normal * WALL_MARGIN
					motion = (velocity * delta).slide(wall_collision_normal)
					is_player_collided = PhysicsServer.body_test_motion(self.get_rid(), transform3d, motion, false, test_motion_result)
					if not is_player_collided:
						transform3d.origin += motion
						motion = -step_height
						is_player_collided = PhysicsServer.body_test_motion(self.get_rid(), transform3d, motion, false, test_motion_result)
						if is_player_collided:
							if test_motion_result.collision_normal.angle_to(Vector3.UP) <= deg2rad(STEP_MAX_SLOPE_DEGREE):
								head_offset = -test_motion_result.motion_remainder
								is_step = true
								global_transform.origin += -test_motion_result.motion_remainder
								break
			else:
				var wall_collision_normal: Vector3 = test_motion_result.collision_normal
				transform3d.origin += test_motion_result.collision_normal * WALL_MARGIN
				motion = step_height
				is_player_collided = PhysicsServer.body_test_motion(self.get_rid(), transform3d, motion, false, test_motion_result)
				if not is_player_collided:
					transform3d.origin += step_height
					motion = (velocity * delta).slide(wall_collision_normal)
					is_player_collided = PhysicsServer.body_test_motion(self.get_rid(), transform3d, motion, false, test_motion_result)
					if not is_player_collided:
						transform3d.origin += motion
						motion = -step_height
						is_player_collided = PhysicsServer.body_test_motion(self.get_rid(), transform3d, motion, false, test_motion_result)
						if is_player_collided:
							if test_motion_result.collision_normal.angle_to(Vector3.UP) <= deg2rad(STEP_MAX_SLOPE_DEGREE):
								head_offset = -test_motion_result.motion_remainder
								is_step = true
								global_transform.origin += -test_motion_result.motion_remainder
								break

	var is_falling: bool = false
	
	if not is_step and is_on_floor():
		var test_motion_result: PhysicsTestMotionResult = PhysicsTestMotionResult.new()
		var step_height: Vector3 = STEP_HEIGHT_DEFAULT
		var transform3d: Transform = global_transform
		var motion: Vector3 = velocity * delta
		var is_player_collided: bool = PhysicsServer.body_test_motion(self.get_rid(), transform3d, motion, false, test_motion_result)
			
		if not is_player_collided:
			transform3d.origin += motion
			motion = -step_height
			is_player_collided = PhysicsServer.body_test_motion(self.get_rid(), transform3d, motion, false, test_motion_result)
			if is_player_collided:
				if test_motion_result.collision_normal.angle_to(Vector3.UP) <= deg2rad(STEP_MAX_SLOPE_DEGREE):
					head_offset = test_motion_result.motion
					is_step = true
					global_transform.origin += test_motion_result.motion
			else:
				is_falling = true
		else:
			if test_motion_result.collision_normal.y == 0:
				var wall_collision_normal: Vector3 = test_motion_result.collision_normal
				transform3d.origin += test_motion_result.collision_normal * WALL_MARGIN
				motion = (velocity * delta).slide(wall_collision_normal)
				is_player_collided = PhysicsServer.body_test_motion(self.get_rid(), transform3d, motion, false, test_motion_result)
				if not is_player_collided:
					transform3d.origin += motion
					motion = -step_height
					is_player_collided = PhysicsServer.body_test_motion(self.get_rid(), transform3d, motion, false, test_motion_result)
					if is_player_collided:
						if test_motion_result.collision_normal.angle_to(Vector3.UP) <= deg2rad(STEP_MAX_SLOPE_DEGREE):
							head_offset = test_motion_result.motion
							is_step = true
							global_transform.origin += test_motion_result.motion
					else:
						is_falling = true
		
	if is_step:
		speed = SPEED_ON_STAIRS
		if is_jumping:
			velocity.x *= 0.1
			velocity.z *= 0.1
	else:
		head_offset = head_offset.linear_interpolate(Vector3.ZERO, delta * speed * stairs_feeling_coefficient)
		
		if abs(head_offset.y) <= 0.01:
			speed = SPEED_DEFAULT
	
	movement = velocity + gravity_vec

	if is_falling:
		snap = Vector3.ZERO
		
# warning-ignore:return_value_discarded
	move_and_slide_with_snap(movement, snap, Vector3.UP, false, 4, deg2rad(46), false)