Godot Version
Godot 4.4.1
Question
I have a particular issue when trying to work on 2D slope physics, which seems to be a huge limitation when attempting to interact with more complicated movement physics. I would like to know if this behavior has a specific workaround.
The issue is that when setting downwards floor velocities on slopes (example here is using 45 degree slopes), move_and_slide will always completely negate the velocity.y component and reset that to 0. What happens is an incorrect velocity length, about only 0.707 of the original set velocity.
Here is a dummy script that exemplifies this issue. In theory the SPEED of the character should be 800 always when moving up and down slopes, but it is actually outputting ~565 on downward slopes, due to velocity being reset to Vector2(800, 0) every frame, then adjusting the velocity angle.
Here is the actual source code I am working with:
Setting floor_stop_on_slope and floor_constant_speed as below and floor_snap_length to 0 will occasionally fix this issue, but it isn’t consistent when it decides to work or not… meaning it’s not a viable solution.
extends CharacterBody2D
const SPEED = 800.0
const JUMP_VELOCITY = -1000.0
func _init():
self.floor_stop_on_slope = false
self.floor_constant_speed = true
self.floor_snap_length = 50
self.set_floor_stop_on_slope_enabled(false)
func _physics_process(delta: float) -> void:
# Add the gravity.
if not is_on_floor():
velocity += get_gravity() * delta
# Handle jump.
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
velocity.y = JUMP_VELOCITY
# Get the input direction and handle the movement/deceleration.
var direction := Input.get_axis("ui_left", "ui_right")
print(velocity.length()) # this is incorrectly 565 when moving down slopes
if direction:
if self.is_on_floor():
velocity = direction * SPEED * self.get_floor_normal().orthogonal().normalized() * -1
else:
velocity.x = direction * SPEED
else:
if self.is_on_floor():
velocity = Vector2(0, 0)
else:
velocity.x = 0
move_and_slide()
I understand what your issue is but how does that affect how your character moves versus how it’s supposed to?
Can’t test ur game cuz im on my phone rn
In the case of the dummy script - it simply doesn’t move at the velocity i want it set to. It makes moving downwards clunkier because it’s slower than moving upwards or on flat ground.
For how I actually want it implemented - it completely kills any form of logic requiring acceleration. I want interesting ground slope interaction for sliding, running, and transferring falling momentum directly into slope sliding momentum. That simply isn’t possible at all with how the engine handles it currently.
Play with these settings on your CharacterBody2D. For example, it sounds like you want to turn stop_on_slope = false You also might consider changing the max_angle to a greater or lesser angle to see if that gives the feel you want.
Okay, this is an arguably stupid solution to this but it’s the one I devised while reading this post trying to fix the exact same problem LOL
So i have a more complex state pairing to determine if i should be on the ground (and thus perform snapping actions and such) and i was able to use that to determine if I just touched the ground and apply an extra instance of the velocity.y component.
All in all the relevant code to fix this is below. Also note, all the var declarations are outside this to allow these extra datas to be used in other object relevant scripts
func _physics_process(delta: float):
if not is_on_floor():
actionstate = PST_AIRBORNE
else:
if actionstate == PST_AIRBORNE:
justlanded = true
else:
justlanded = false
actionstate = PST_GROUNDED
#Jump Handler (truncated for only what is needed to fix this)
justjumped = false
if Input.is_action_just_pressed("jump") and is_on_floor():
actionstate = PST_AIRBORNE
if justlanded == true: #This fix is stupid why does this work????? (Here's the magic)
velocity.y += lastvelocity.y
lastvelocity = velocity
move_and_slide() #here we go, this thing
if actionstate == PST_GROUNDED: #do we or dont we snap to the ground?
apply_floor_snap()
Hope this solution isnt as dumb as it looks, if you can improve upon this please let me know, I think we both are trying to fix the same thing LOL.
Sorry to revisit this so late, but I’ve finally come back to this with a suitable solution.
The main issue here is that Godot’s engine doesn’t play nicely with moving on slopes - and will reset the y velocity to 0 when it’s moving downwards while is_on_floor() is true.
So my solution was actually to disable being “on floors” that while on slopes completely.
# disable is_on_floor while on 45 degree slopes
self.set_floor_max_angle(PI / 4 - 0.1)
Using two slightly protruding raycasts, I could calculate everything else I needed, including using my own custom “grounded” implementation.
## Determine if the player is grounded.
func _is_grounded() -> bool:
var touching: bool = false
var raycast_one: RayCast2D = get_node("DownRaycast1")
var raycast_two: RayCast2D = get_node("DownRaycast2")
if raycast_one.is_colliding():
var angle: float = raycast_one.get_collision_normal().angle()
touching = touching or (-PI / 4 + 0.1 > angle and angle > -3 * PI / 4 - 0.1)
if raycast_two.is_colliding():
var angle: float = raycast_one.get_collision_normal().angle()
touching = touching or (-PI / 4 - 0.1 > angle and angle > -3 * PI / 4 - 0.1)
if touching:
self.apply_floor_snap()
return touching
## Get the floor slope angle.
func _get_floor_slope() -> Vector2:
var raycast_one: RayCast2D = get_node("DownRaycast1")
var raycast_two: RayCast2D = get_node("DownRaycast2")
var slope: Vector2 = Vector2(1, 0)
if raycast_one.is_colliding():
var temp_slope: Vector2 = raycast_one.get_collision_normal().orthogonal()
slope = temp_slope if temp_slope.x > 0 else -temp_slope
if raycast_two.is_colliding():
var temp_slope: Vector2 = raycast_two.get_collision_normal().orthogonal()
temp_slope = temp_slope if temp_slope.x > 0 else -temp_slope
if abs(temp_slope.y / temp_slope.x) > abs(slope.y / slope.x):
slope = temp_slope
return slope
And with this solution I was able to project landing velocity onto the slope using a projection formula and a stored state of the last frame’s velocity (what you had actually). This allows landing onto the slope and carrying momentum into a slide.
func project_vectors(a: Vector2, b: Vector2) -> Vector2:
# projb a = ((a ⋅ b) / |b|²) * b
# redundant power, b.length() is 1
var new_magnitude: float = a.dot(b) / pow(b.length(), 2)
return new_magnitude * b
Hope this helps! This was a straight nightmare to figure out but I’m glad I’ve finally gotten it figured out.