How to deal with slope sliding and slope launching?

Godot Version

Godot 4.6

Question

I’ve been banging my head on the wall over this issue for literal months now, dropped my project temporarily because of it, and now that I got a little spare time, I’m picking this up again.

My question is pretty simple. I’m making a 2D platformer with slope physics, and I want my player to 1- slide on a slope when standing on it, and 2- be able to launch themselves from a slope when running fast enough. The player is a CharacterBody2D.

For slope sliding, just setting floor_stop_on_slope to false doesn’t make it slide fast enough, so I decided to apply a slope force after the gravity manually:

# This is called on _physics_process().
# _body_on_floor stores body.is_on_floor() for the current frame,
# _gravity_vector stores body.get_gravity().
func _apply_slope_slide(delta : float) -> void:
	if _body_on_floor:
		var floor_normal = body.get_floor_normal()
		body.velocity += floor_normal * _gravity_vector.length() * delta

Not sure if this is the right way to go, but it if works, it works, I guess.

Now here’s the annoying part: launch the player from the slope. Nothing I do ever works. For some reason, if the player run towards the edge of a platform, instead of keeping the same velocity in both axes, the vertical velocity always get set to 0 for some reason. This happens when running either up or down the slope. Here’s a video showing the issue:

I was having this other issue where the player would snap off the slope if running too fast on the horizontal axis, but turning up floor_snap_lengthseemed to make things better.

I really need some help here :frowning:

Okay, I think I did a workaround to the slope launching thing that got me almost there:

func _do_body_movement() -> void:
	var body_was_on_floor := _on_floor
	var last_real_velocity := body.get_real_velocity()
	
	body.move_and_slide()
	var body_is_on_floor := body.is_on_floor()
	
	# If the player left the floor without jumping, keep previous real velocity
	if body_was_on_floor and not body_is_on_floor and not _just_jumped:
        print("Dropping down a platform!")
		body.velocity = last_real_velocity

But it’s not really consistent yet, sometimes the character is launched off the slope just right, sometimes it sort of “trips” on the edge of the slope and gets launched in more of a downward angle instead. I’m also still having the other issue where the player would snap off the slope if running too fast. Here’s how it looks right now:

Tweaking the body’s floor_snap_length seems to fix one issue but make the the other one worse, or maybe I’m just going insane…

Could you maybe make a short video what is it looks like?

I did actually, there’s a link in my last post. I can’t post the link again for some reason.

When the body moves away from the floor, it prints a message on the console. You can see how it’s happening inconsistently in the video.

Be aware that Imgur stuff cannot be viewed in the UK, as Imgur refused to sign up to the UK legislation concerning child-friendly content. Just sayin’. :slight_smile:

Thanks for letting me know, I uploaded the videos to a different host.

EDIT: Nevermind, I managed to add the video files directly on my posts :sweat_smile:

Okay, I think I found a solution. Since I really need more granular control over the character’s velocity and using move_and_slide() was causing so many issues, I decided to ditch it altogether and make my own body movement code using move_and_collide() instead (which is what move_and_slide() uses internally anyway).

I used this tutorial in the docs along with this video and the article linked in its description as the basis for this new movement code:

func _physics_process(delta: float) -> void:
	_handle_movement(delta)


func _handle_movement(delta : float) -> void:
	# Add gravity to the body
	body.velocity += body.get_gravity() * delta

	# Do whatever else you want here, player movement, jumping and whatnot
	
	# Apply final movement
	_collide_and_slide(delta)

func _collide_and_slide(delta : float) -> void:
	# Make sure you don't divide by zero
	# Can't have any movement while delta is zero anyway
	if delta == 0:
		return
	
	# We'll use the CharacterBody2D's own properties for calculations
	# The body's movement in this frame will be its velocity vector times delta
	var remainder := body.velocity * delta

	# Also count the number of slides occurred 
	# so we can limit how many of them can happen
	var slides : int = 0
	
	# Repeat this as long as we haven't reached the max number of slides
	while slides < body.max_slides:
		# Apply remaining movement
		var collision := body.move_and_collide(remainder)

		# If no collision happened, wrap it up
		if not collision:
			body.velocity = remainder / delta
			return
		
		# Otherwise, "slide" the remaining movement vector along the surface
		remainder = remainder.slide(collision.get_normal())
		slides += 1
	
	# Max number of slides reached, so this is our final velocity
	body.velocity = remainder / delta

With this code I managed to get both the slope sliding and launching working properly, so that’s two birds killed with one stone.

If you follow this method, keep in mind you’ll have to reimplement other CharacterBody2D functionalities that rely on move_and_slide() such as is_on_floor() and get_platform_velocity(). Honestly after all I went through, this is a fair trade-off.

So far I only needed to check for floor collision so this is what I did:

func _update_floor_state() -> void:
	# Make a "test" movement downward (or whichever direction "down" is to you)
	# to check if the player is on the floor
	var collision := body.move_and_collide(
		-body.up_direction * body.floor_snap_length,
		true,
		body.safe_margin,
		true
	)
	
	# No collision, we are definitely in the air
	if not collision:
		_body_on_floor = false
		return
	
	# We consider the surface a floor if its inclination is within the max angle
	var angle_diff := absf(body.up_direction.angle_to(collision.get_normal()))
	_body_on_floor = (angle_diff <= body.floor_max_angle)

Hope this helps anyone with the same trouble as me :face_holding_back_tears: