On OP demand, here is how I would set this up using RayCast2D
.
First of all, the RayCast2D
will be a child of the KinematicBody2D
and we need to position it to the side and pointing down. Also make sure the RayCast2D
is enabled. As seen here:

And for the code I'm using this:
extends KinematicBody2D
export var gravity:float = 100
var _velocity:Vector2 = Vector2.RIGHT * 100
const FLOOR_NORMAL:Vector2 = Vector2.UP
func _physics_process(delta:float) -> void:
_velocity.y += gravity * delta
_velocity.y = move_and_slide(_velocity, FLOOR_NORMAL).y
if is_on_wall() or (is_on_floor() and !$RayCast2D.is_colliding()):
_velocity.x *= -1.0
$RayCast2D.position.x *= -1.0
To check for the hole, we want to verify that the RayCast2D
is NOT colliding. If it is colliding, it means there is floor ahead. The check for is_on_floor
is to avoid it changing direction in the air.
Also, I'm checking both conditions on a single statement. First to avoid repetition, and second because if there are two checks that can change the direction, and they both change the direction… The result is that it didn't change direction! Next frame it will try to change direction again, at infinitum. Any similar situation will result in the KinematicBody2D
changing direction in place. It would appear to not move.
Finally, the reason why I do the move_and_slide
before all the checks is because, is_on_wall
and is_on_floor
are updated when you call move_and_slide
.
And this is the game running with collision shapes visible (debug option):

As you can see, it changes direction both on a wall and on a hole.