godot: isOnFloor is intermittently false when moving against the floor normal

Godot version: 3.0, C# with Mono

OS/device including version: Windows 10, GTX 1080

Issue description: KinematicBody2D.IsOnFloor() is intermittently false when moving against the floor normal

Steps to reproduce:

  • Press the Spacebar. The character will refuse to jump because he’s not on the floor.

Minimal reproduction project:

peridot.zip

I’ve already set the FLOOR_NORMAL when using MoveAndSlide, so there’s no reason as to why this should be happening.

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Comments: 37 (15 by maintainers)

Most upvoted comments

I recently ran into this issue as well. Seems like is_on_floor() doesn’t work correctly unless a collision occurs with the floor. So I had to apply a velocity.y = 5 to get it to return true.

Since I had this problem recently (and this issue seems to get some traffic,) I figured I’d spend a few days looking into it. The bad news is, I don’t see an easy fix in the engine. The good news is, it’s very easy to fix in your code.

The short solution is simple, per @justinluck. Instead of having a movement loop like this:

if is_on_floor():
	if Input.is_action_just_pressed("ui_up"):
		motion.y = -JUMP_SPEED
	else:
		motion.y = 0 
else:
	motion.y += GRAVITY

motion = move_and_slide(motion, UP)

Use a loop like this:

motion.y += GRAVITY
if Input.is_action_just_pressed("ui_up"):
		motion.y = -JUMP_SPEED

motion = move_and_slide(motion, UP)

This will make it so you’ll always have some downwards velocity on the ground, causing a collision with the floor and ensuring is_on_floor() stays True.

That’s enough to fix the bug. For the curious, I’ve attached a longer explanation below.


The key to this bug is that objects start every motion by pushing themselves out of collision distance of all other objects. However, this only happens when the first object is moving. That object is then moved along its intended path as much as possible before it actually overlaps with another object.

Say you’re using the first loop above (the bad one). On the first frame of your game, is_on_floor() is false by default, so gravity will be applied, pushing you as close to the floor as it can (well within collision distance). As long as you don’t start moving, you won’t be kicked out, so is_on_floor() will remain true.

Once you start moving, though, the engine snaps awake and kicks you out of the floor. Here’s the kicker: if your motion is perfectly horizontal, well, you won’t move downwards. This leaves you just out of collision distance with the floor, causing is_on_floor() to return False for that frame. Seeing this, your script quite naturally adds gravity for the next frame, giving you a downward velocity and pushing you close to the floor again, so is_on_floor() returns True. And now that you’re on the floor, the script doesn’t add gravity, so in the next frame you get kicked out and move perfectly horizontally again, causing is_on_floor() to remain False… and so on.

I’ve honestly got no idea how this would be fixed (and it probably shouldn’t be). One idea is to only kick a collider out of a body if it isn’t moving parallel to said body . I’ve tested this, and although it removes the issue in a vacuum, it comes back when you’re colliding with another object (i.e. running into a wall.)

You have to call ModeAndSlide() BEFORE IsOnFloor() or IsOnWall().

What happens is probably the following: the MoveAndSlide() functions set the internal attribute isOnFloor. Since you use MoveAndSlide() at the end of your function, the values are sometimes still usable for the next _process() call. But sometimes, a physics frame goes between two _process() calls which resets the values to 0.