• 2D
  • Getting a platforming sidescroller character fully sticking on slope using move_and_slide_with_snap

I'm trying to get my 2D platforming character moving left and right without him involuntarily leaving out of a slanted ground until he goes off a ledge or a jump button is pressed. All that while determining if he's colliding to a floor using two RayCast2D nodes with each placed on each side of the playable character, and a rectangle-shaped CollisionShape2D for colliding with any ground. So far, I've done it successfully, except it has some unintended behaviors...:

  • The character would hop when moving on a joint that separates two different downward slopes, despite the collision mask clearly shows that both ends share the same Y coordinate
  • When my character slows down quickly enough while escalating a slope, he hops on his own
  • When my character lands on a slope, the character would slide downwards for a bit before entirely stopping. I don't want the vertical movement affecting the left & right velocity upon landing in a slanted ground.

How do I solve these problems? If these can't absolutely be fixed with the current setup, feel free to provide alternatives that can get the character sticking on the ground until he leaves off ledge or a jump input is triggered.

Picture of the Node tree:

Here's Level00.gd for altering the playable character's position using x and y motion calculated from Player.gd:

extends Node2D

func _physics_process(_delta):
	$Player.position.x += $Player.motion.x
	$Player.position.y += $Player.motion.y

And Player.gd for all player interactions with the environment:

extends KinematicBody2D

const TARGET_FPS = 60
const FRICTION = 3
const AIR_RESISTANCE = 0
const GRAVITY = 0.2

var JUMP_FORCE
var OFFLEDGE_TIMER = 6
var ACCELERATION
var MAX_SPEED = 3
var SPEED_MODIFIER = 1
var JUMP_MODIFIER = 1
var ACCELERATION_MODIFIER = 1

var motion = Vector2.ZERO
var motion_backup = Vector2.ZERO
var motion_noslide = Vector2.ZERO
var snap = Vector2.DOWN

onready var sprite = $Star
##onready var animationPlayer = $AnimationPlayer

##PHYSICS##

func _physics_process(delta):
	var x_input = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
	
	##GENERAL MOVEMENT
	
	##CHECKS FOR SPEED MODIFIER (SUCH AS SUPERSPEED POWERUP HERE)
	if Input.is_action_pressed("ui_superspeed"):
		SPEED_MODIFIER = 1.5
		JUMP_MODIFIER = 1.1
		ACCELERATION_MODIFIER = 1.25
	else:
		SPEED_MODIFIER = 1
		ACCELERATION_MODIFIER = 1
		JUMP_MODIFIER = 1
	##CHECKS FOR ANY MOVEMENT INPUT
	if x_input != 0:
		
		##IF THE RUN BUTTON IS HELD, CHARACTER WILL ACCELERATE AND MOVE QUICKER ALONGSIDE JUMPING HIGHER
		if Input.is_action_pressed("ui_speed"):
			##animationPlayer.play("Run")
			ACCELERATION = ACCELERATION_MODIFIER * 0.1
			MAX_SPEED = SPEED_MODIFIER * 3
			JUMP_FORCE = JUMP_MODIFIER * 6.25
		else:
			##animationPlayer.play("Walk")
			ACCELERATION = ACCELERATION_MODIFIER * 0.1
			if MAX_SPEED > SPEED_MODIFIER * 2:
				if $RayCast2D_Left.is_colliding() || $RayCast2D_Right.is_colliding():
					MAX_SPEED -= SPEED_MODIFIER * 0.05
			else:
				MAX_SPEED = SPEED_MODIFIER * 2
			JUMP_FORCE = JUMP_MODIFIER * 5.5
		
		##ACCELERATING CHARACTER
		motion.x += x_input * (ACCELERATION_MODIFIER * ACCELERATION) * delta * TARGET_FPS
		motion.x = clamp(motion.x, -(SPEED_MODIFIER * MAX_SPEED), (SPEED_MODIFIER * MAX_SPEED))
		motion_backup.x = motion.x
		motion_noslide.x = motion.x
		$Star.flip_h = x_input < 0
	else:
		JUMP_FORCE = JUMP_MODIFIER * 5
		##animationPlayer.play("Stand")
	
	##GRAVITY INTERACTION
	
	##PREVENTING THE CHARACTER FROM FALLING TOO FAST, WHERE motion_noslide DOESN'T CALCULATE motion.y
	if motion.y <= 5:
		motion.y += GRAVITY * delta * TARGET_FPS
		motion_backup.y = motion.y
	
	##IF THE CHARACTER HITS ON GROUND
	if $RayCast2D_Left.is_colliding() || $RayCast2D_Right.is_colliding():
		OFFLEDGE_TIMER = 6
		snap = Vector2.DOWN
		if x_input == 0:
			motion.x = lerp(motion.x, 0, FRICTION * delta)
			motion_backup.x = motion.x
			motion_noslide.x = motion.x
			##MAKING CHARACTER TO STOP ONCE THE motion.x value GETS LOW-ENOUGH MOMENTUM
			if motion.x < 0.075 && (motion.x) > -0.075:
				motion.x = 0
				motion_backup.x = 0
				motion_noslide.x = 0
		#IF JUMP BUTTON IS PRESSED WHILE ON GROUND, CHARACTER WILL JUMP ACCORDING TO HOW LONG IT IS HELD
		if Input.is_action_just_pressed("ui_jump"):
			motion.y = -(JUMP_MODIFIER * JUMP_FORCE)
			motion_backup.y = motion.y
			motion_noslide.y = motion.y
	else:
		##animationPlayer.play("Jump")
		OFFLEDGE_TIMER += -1
		snap = Vector2.ZERO
		if Input.is_action_just_released("ui_jump"): 
			OFFLEDGE_TIMER = 0
			if motion.y < -(JUMP_MODIFIER * (JUMP_FORCE/3)):
				motion.y = -(JUMP_MODIFIER * (JUMP_FORCE/3))
				motion_backup.y = motion.y
				motion_noslide.y = motion.y
		
		if x_input == 0:
			motion.x = lerp(motion.x, 0, AIR_RESISTANCE * delta)
			motion_backup.x = motion.x
			motion_noslide.x = motion.x
		
		if OFFLEDGE_TIMER > 0 and Input.is_action_just_pressed("ui_jump"):
			motion.y = -(JUMP_MODIFIER * JUMP_FORCE)
			motion_backup.y = motion.y
			motion_noslide.y = motion.y
			OFFLEDGE_TIMER = 0

	if snap.y == 1:
		motion = move_and_slide_with_snap(motion_noslide, snap, Vector2.UP, true, 4, deg2rad(52))
		motion_noslide.y = motion.y
		motion_backup = motion_noslide
	else:
		motion = move_and_slide(motion_backup, Vector2.UP)
		motion_backup = motion
		motion_noslide.y = motion_backup.y
6 days later

I resolved a few issues on my own:

On line 117, I renamed "motion" into "motion.y" and appended a ".y" on the last character. It should be read as: motion.y = move_and_slide_with_snap(motion_noslide, snap, Vector2.UP, true, 4, deg2rad(52)).y

With that into effect, the Y momentum no longer causes the character to gain way more X momentum than needed upon landing on a slanted ground.

As a result, the motion_noslide and motion_backup variables are no longer needed and instances where they were called upon are either removed or replaced with just motion. This also resolves the issue of the character involuntarily hopping while turning to the opposite direction when scaling a slope.

I'm still leaving the thread unanswered, since the character would still hop when moving on a joint that separates two different downward slopes on higher moving speed. Is there a way for a character to completely sticking on a slanted ground whose inclination angle is around 45° regardless of how fast he moves?

Actually, I found if you set velocity.y to 0.0 when is_on_floor() is true then it stops sliding.

@cybereality said: Actually, I found if you set velocity.y to 0.0 when is_on_floor() is true then it stops sliding.

velocity in my code is actually referred to as motion. And due to my floor detection being based on the downward-pointing RayCast2D nodes to determine whether the character can jump or not, every node within the Player node but their parent tells the debugger that the is_on_floor() constant is nonexistent. I forgot to mention that none of the .gd files from the current project folder uses the character's CollisionShape2D node, although said node interacts with the ground tiles within layer 0 collision mask, as in preventing the character from going through any solid tile.

However, I found a workaround that mitigates the slope joints issue, involving extending the Y value of Cast To length of the two RayCast2D nodes. I guess that one of these nodes needed to cover more collision ground for the move_and_slide_with_snap to prevent sliding while on floor. By tweaking the GRAVITY constant and MAX_SPEED variable to a certain range, it also allows the character jumping despite the CollisionShape2D being slightly off ground very briefly at worst. Another tradeoff from a RayCast2D's longer Y Cast To is that if the player repeatedly mash the ui_jump button very rapidly, the character would perform a double jump that ends up being very slightly higher than usual.

Then, I found out that my character tended to recoil further after stopping pressing any directional input while scaling a 45° slope upwards. Here's what I had on line 85: if motion.x < 0.075 && (motion.x) > -0.075: If I replace 0.075 and -0.075 with values like 0.5 and -0.5, my character won't practically recoil while climbing.

Unrelated to the issues of this question topic, but I managed to prevent a character from jumping at its highest height by lightly tapping the ui_jump button. All I did is to add the following line of code just underneath line 93:


			OFFLEDGE_TIMER = 0
		elif Input.is_action_just_released("ui_jump"):
			OFFLEDGE_TIMER = 0
			if motion.y < -(JUMP_MODIFIER * (JUMP_FORCE/3)):
				motion.y = -(JUMP_MODIFIER * (JUMP_FORCE/3))

In case you're wondering, here's the relevant Player.gd code so far:

! extends KinematicBody2D !
! const TARGET_FPS = 60 ! const FRICTION = 3 ! const AIR_RESISTANCE = 0 ! const GRAVITY = 0.3 !
! var JUMP_FORCE ! var OFFLEDGE_TIMER = 6 ! var ACCELERATION ! var MAX_SPEED = 3 ! var SPEED_MODIFIER = 1 ! var JUMP_MODIFIER = 1 ! var ACCELERATION_MODIFIER = 1 !
! var SHIELD_TIMER = 0 ! var SHIELD_READY = 1 !
! var TILE_TYPE ! var SWIM_LEVEL = 0 !
! var motion = Vector2.ZERO ! var snap = Vector2.DOWN !
! onready var sprite = $Star !
! ##PHYSICS## !
! func _physics_process(delta): ! var x_input = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left") ! ! ##GENERAL MOVEMENT ! ! ##CHECKS FOR SPEED MODIFIER (SUCH AS SUPERSPEED POWERUP HERE) ! if Input.is_action_pressed("ui_superspeed"): ! SPEED_MODIFIER = 1.3 ! JUMP_MODIFIER = 1.1 ! ACCELERATION_MODIFIER = 1.2 ! else: ! SPEED_MODIFIER = 1 ! ACCELERATION_MODIFIER = 1 ! JUMP_MODIFIER = 1 ! ##CHECKS FOR ANY MOVEMENT INPUT ! if x_input != 0: ! ! ##IF THE RUN BUTTON IS HELD, CHARACTER WILL ACCELERATE AND MOVE QUICKER ALONGSIDE JUMPING HIGHER ! if Input.is_action_pressed("ui_speed"): ! ACCELERATION = ACCELERATION_MODIFIER 0.1 ! MAX_SPEED = SPEED_MODIFIER 4 ! JUMP_FORCE = JUMP_MODIFIER 8 ! else: ! ##animationPlayer.play("Walk") ! ACCELERATION = ACCELERATION_MODIFIER 0.1 ! if MAX_SPEED > SPEED_MODIFIER 2: ! if $RayCast2D_Left.is_colliding() || $RayCast2D_Right.is_colliding(): ! MAX_SPEED -= SPEED_MODIFIER 0.05 ! else: ! MAX_SPEED = SPEED_MODIFIER 2 ! JUMP_FORCE = JUMP_MODIFIER 7 ! ! ##ACCELERATING CHARACTER ! motion.x += x_input (ACCELERATION_MODIFIER ACCELERATION) delta TARGET_FPS ! motion.x = clamp(motion.x, -(SPEED_MODIFIER MAX_SPEED), (SPEED_MODIFIER MAX_SPEED)) ! $Star.flip_h = x_input < 0 ! else: ! JUMP_FORCE = JUMP_MODIFIER 6.5 ! ! ##GRAVITY INTERACTION !
! ##PREVENTING THE CHARACTER FROM FALLING TOO FAST ! if motion.y <= 5: ! motion.y += GRAVITY
delta TARGET_FPS ! ! ##IF THE CHARACTER HITS ON GROUND ! if $RayCast2D_Left.is_colliding() || $RayCast2D_Right.is_colliding(): ! OFFLEDGE_TIMER = 6 ! snap = Vector2.DOWN ! if x_input == 0: ! motion.x = lerp(motion.x, 0, FRICTION delta) ! ##MAKING CHARACTER TO STOP ONCE THE motion.x value GETS LOW-ENOUGH MOMENTUM ! if motion.x < 0.5 && (motion.x) > -0.5: ! motion.x = 0 ! #IF JUMP BUTTON IS PRESSED WHILE ON GROUND, CHARACTER WILL JUMP ACCORDING TO HOW LONG IT IS HELD ! if Input.is_action_just_pressed("ui_jump"): ! motion.y = -(JUMP_MODIFIER JUMP_FORCE) ! OFFLEDGE_TIMER = 0 ! elif Input.is_action_just_released("ui_jump"): ! OFFLEDGE_TIMER = 0 ! if motion.y < -(JUMP_MODIFIER (JUMP_FORCE/3)): ! motion.y = -(JUMP_MODIFIER (JUMP_FORCE/3)) ! else: ! OFFLEDGE_TIMER += -1 ! snap = Vector2.ZERO ! if Input.is_action_just_released("ui_jump"): ! OFFLEDGE_TIMER = 0 ! if motion.y < -(JUMP_MODIFIER (JUMP_FORCE/3)): ! motion.y = -(JUMP_MODIFIER (JUMP_FORCE/3)) ! ! if x_input == 0: ! motion.x = lerp(motion.x, 0, AIR_RESISTANCE delta) ! ! if OFFLEDGE_TIMER > 0 and Input.is_action_just_pressed("ui_jump"): ! motion.y = -(JUMP_MODIFIER * JUMP_FORCE) ! OFFLEDGE_TIMER = 0 ! if snap.y == 1: ! motion.y = move_and_slide_with_snap(motion, snap, Vector2.UP, true, 4, deg2rad(52)).y ! else: ! motion.y = move_and_slide_with_snap(motion, snap, Vector2.UP, true, 4, deg2rad(52)).y