2D Slope Physics Bug

3.5.3

Hello, I’m trying to make a 2D platformer with Sonic-like slope physics. I’m trying to make it so the player slowly eases into an inclination, but it only seems to snap into it over a few frames instead of the whole way through. Here’s some relevant code that returns the orientation the player should assume while on a slope; Raycasts are 3, 2 and 1 from left to right. This section of code only applies when raycasts 1 and 2 collide with the ground, and not 3.

#Rotate normal to align with surface
var surfaceNormal1 = rayCollisionNormal1.rotated(-PI / 2)
#Rotate normal to align with player rotation
surfaceNormal1 = surfaceNormal1.rotated(-rotation)
#Get slope from normal
if (surfaceNormal1.x == 0):
	return(rayCollisionNormal2)
# Convert normal from vector to a single value that applies only to Y
var m = surfaceNormal1.y / surfaceNormal1.x
#Find change in Y if continue down the slope to raycast2's position
var a = m * distBetweenRays
#If y change is BELOW raycast2's collision point, the normals must connect somewhere to form a slope
if to_local(raycast1.get_collision_point()).y + a >= to_local(raycast2.get_collision_point()).y:
	#Slope works!
	#Essentially, find how far up the slope could possibly go if it continued to the right from raycast2's position
	#Then compare that with how far up it actually went, and create a scale from 0 to 1
	#We can then multiply this ray's normal by the scale to determine the influence it should have
	var b = rayHalfLength - a
	var c = b / rayHalfLength
					
	return(rayCollisionNormal2 + (rayCollisionNormal1 * c))
else:
	#Slope does not work :/ Ignore rayCollisionNormal1 and just use 2
	return(rayCollisionNormal2)

For the scenario where all three raycasts collide with the ground, the code is MUCH more complicated:

#All rays collide
if (rayCollisionDistance1 == rayCollisionDistance2 and rayCollisionDistance2 == rayCollisionDistance3):
	return(rayCollisionNormalAvg)
elif (rayCollisionDistance1 == rayCollisionDistance2):
	## Only left ray has different distance
	#Rotate normal to align with surface
	var surfaceNormal3 = rayCollisionNormal3.rotated(PI / 2)
	#Rotate normal to align with player rotation
	surfaceNormal3 = surfaceNormal3.rotated(-rotation)
	#Get slope from normal
	if surfaceNormal3.x == 0:
		return(rayCollisionNormal2)
	var m = surfaceNormal3.y / surfaceNormal3.x
	#Find change in Y if continue down the slope to raycast2's position
	var a = m * distBetweenRays
	#If y change is BELOW raycast2's collision point, the normals must connect somewhere to form a slope
	if to_local(raycast3.get_collision_point()).y - a >= to_local(raycast2.get_collision_point()).y:
		#Slope works!
		#Essentially, find how far up the slope could possibly go if it continued left from raycast2's position
		#Then compare that with how far up it actually went, and create a scale from 0 to 1
		var b = rayHalfLength + a
		var c = b / rayHalfLength
					
		return((rayCollisionNormal2 + (rayCollisionNormal3 * c)).normalized())
	else:
		#Slope does not work :/ Ignore rayCollisionNormal3 and just use 2
		return(rayCollisionNormal2)
elif (rayCollisionDistance2 == rayCollisionDistance3):
	## Only right ray has different distance
	#Rotate normal to align with surface
	var surfaceNormal1 = rayCollisionNormal1.rotated(-PI / 2)
	#Rotate normal to align with player rotation
	surfaceNormal1 = surfaceNormal1.rotated(-rotation)
	#Get slope from normal
	if surfaceNormal1.x == 0:
		return(rayCollisionNormal2)
	var m = surfaceNormal1.y / surfaceNormal1.x
	#Find change in Y if continue down the slope to raycast2's position
	var a = m * distBetweenRays
	#If y change is BELOW raycast2's collision point, the normals must connect somewhere to form a slope
	if to_local(raycast1.get_collision_point()).y + a >= to_local(raycast2.get_collision_point()).y:
		#Slope works!
		#Essentially, find how far up the slope could possibly go if it continued left from raycast2's position
		#Then compare that with how far up it actually went, and create a scale from 0 to 1
		var b = rayHalfLength - a
		var c = b / rayHalfLength
					
		return((rayCollisionNormal2 + (rayCollisionNormal1 * c)).normalized())
	else:
		#Slope does not work :/ Ignore rayCollisionNormal3 and just use 2
		return(rayCollisionNormal2)
else:
	## No adjacent rays are the same distance from their origins
	#Rotate normals to align with surface
	var surfaceNormal1 = rayCollisionNormal1.rotated(-PI / 2)
	var surfaceNormal3 = rayCollisionNormal3.rotated(PI / 2)
	#Rotate normals to align with player rotation
	surfaceNormal1 = surfaceNormal1.rotated(-rotation)
	surfaceNormal3 = surfaceNormal3.rotated(-rotation)
	#Get slope from normals
	if surfaceNormal1.x == 0:
		#OOPS. Skip normal1, just calculate 2 and 3
		if surfaceNormal3.x == 0:
			#UGGGH just do normal2
			return(rayCollisionNormal2)
					
		var m = surfaceNormal3.y / surfaceNormal3.x
		#Find change in Y if continue down the slope to raycast2's position
		var a = m * distBetweenRays
		#If y change is BELOW raycast2's collision point, the normals must connect somewhere to form a slope
		if to_local(raycast3.get_collision_point()).y - a >= to_local(raycast2.get_collision_point()).y:
			#Slope works!
			#Essentially, find how far up the slope could possibly go if it continued left from raycast2's position
			#Then compare that with how far up it actually went, and create a scale from 0 to 1
			var b = rayHalfLength + a
			var c = b / rayHalfLength
						
			return((rayCollisionNormal2 + (rayCollisionNormal3 * c)).normalized())
		else:
			#Slope does not work :/ Ignore rayCollisionNormal3 and just use 2
			return(rayCollisionNormal2)
				
	if surfaceNormal3.x == 0:
		#OOPS. Skip normal1, just calculate 2 and 3
		if surfaceNormal1.x == 0:
			#UGGGH just do normal2
			return(rayCollisionNormal2)
					
		var m = surfaceNormal1.y / surfaceNormal1.x
		#Find change in Y if continue down the slope to raycast2's position
		var a = m * distBetweenRays
		#If y change is BELOW raycast2's collision point, the normals must connect somewhere to form a slope
		if to_local(raycast1.get_collision_point()).y + a >= to_local(raycast2.get_collision_point()).y:
			#Slope works!
			#Essentially, find how far up the slope could possibly go if it continued right from raycast2's position
			#Then compare that with how far up it actually went, and create a scale from 0 to 1
			var b = rayHalfLength - a
			var c = b / rayHalfLength
						
			return((rayCollisionNormal2 + (rayCollisionNormal1 * c)).normalized())
		else:
			#Slope does not work :/ Ignore rayCollisionNormal3 and just use 2
			return(rayCollisionNormal2)
				
	## Both normals 1 and 3 aren't 0; we can proceed
	var m1 = surfaceNormal1.y / surfaceNormal1.x
	var m2 = surfaceNormal3.y / surfaceNormal3.x
	#Find change in Y if continue down the slope to raycast2's position
	var a = m1 * distBetweenRays
	var d = m2 * distBetweenRays
	#If y change is BELOW raycast2's collision point, the normals must connect somewhere to form a slope
	if to_local(raycast1.get_collision_point()).y + a >= to_local(raycast2.get_collision_point()).y:
		#Slope works!
		#Essentially, find how far up the slope could possibly go if it continued to the relevant ray's position from raycast2's position
		#Then compare that with how far up it actually went, and create a scale from 0 to 1
		var b = rayHalfLength - a
		var c = b / rayHalfLength
					
		if to_local(raycast3.get_collision_point()).y - d >= to_local(raycast2.get_collision_point()).y:
			#All slopes work!
			#Essentially, find how far up the slope could possibly go if it continued to the relevant ray's position from raycast2's position
			#Then compare that with how far up it actually went, and create a scale from 0 to 1
			var e = rayHalfLength - d
			var f = e / rayHalfLength
						
			return(((rayCollisionNormal3 * f) + rayCollisionNormal2 + (rayCollisionNormal1 * c)).normalized())
		else:
			#Only right slope works...
			return((rayCollisionNormal2 + (rayCollisionNormal1 * c)).normalized())
	elif to_local(raycast3.get_collision_point()).y - d >= to_local(raycast2.get_collision_point()).y:
		#Only left slope works...
		var e = rayHalfLength - d
		var f = e / rayHalfLength
					
		return(((rayCollisionNormal3 * f) + rayCollisionNormal2).normalized())
	else:
		#Slope does not work :/ Ignore rayCollisionNormal1 and 3 and just use 2
		return(rayCollisionNormal2)

As a programmer who is very new to godot and currently may be overshooting in scope, some help would be appreciated :slight_smile:

Update: I did some testing, and I think I’m on the right track. It IS ‘smoothing’ into a curve, but it’s doing so extremely jittery, and when I stand still directly between two connected slopes, the character jitters around and doesn’t settle. Here’s the updated code, just the portion with only two raycasts for simplicity.

if rayCollisionDistance1 == rayCollisionDistance2:
	return (rayCollisionNormal2
else:
	#Rotate normal to align with surface
	var surfaceNormal1 = rayCollisionNormal1.rotated(-PI / 2)
	#Rotate normal to align with player rotation
	surfaceNormal1 = surfaceNormal1.rotated(-rotation)
	#Get slope from normal
	if (surfaceNormal1.x == 0):
		return(rayCollisionNormal2)
	# Convert normal from vector to a single value that applies only to Y
	var m = surfaceNormal1.y / abs(surfaceNormal1.x)
	#Find change in Y if continue down the slope to raycast2's position
	var a = m * distBetweenRays
	#If y change is BELOW raycast2's collision point, the normals must connect somewhere to form a slope
	if to_local(raycast1.get_collision_point()).y + a >= to_local(raycast2.get_collision_point()).y:
		#Slope works!
		#Essentially, find how far up the slope could possibly go if it continued to the right from raycast2's position
		#Then compare that with how far up it actually went, and create a scale from 0 to 1
		#We can then multiply this ray's normal by the scale to determine the influence it should have
		var b = rayHalfLength - a
		var c = b / rayHalfLength
					
		return((rayCollisionNormal2 + (rayCollisionNormal1 * c)).normalized())
	else:
		#Slope does not work :/ Ignore rayCollisionNormal1 and just use 2
		return(rayCollisionNormal2)