Godot: 3D KinematicBody slides with move_and_collide

Created on 26 Apr 2018  路  20Comments  路  Source: godotengine/godot

Godot version:
3.0.2

OS/device including version:
Windows 10

Issue description:
KinematicBody slides slowly along a slope when applying constant downwards movement using move_and_collide(). Expected it to come to a complete stop after first collision with the slope.

This issue was not reproducible with KinematicBody2D. Safe Margin is set to 0.001.

Steps to reproduce:

  • Create KinematicBody with a CollisionShape (either BoxShape or CapsuleShape).
  • Add a script to the KinematicBody:
extends KinematicBody

func _physics_process(delta):
    move_and_collide(Vector3(0, -1, 0))

Create a slope using StaticBody with a CollisionShape.

Minimal reproduction project:
move_and_collide_sliding.zip

bug physics

Most helpful comment

Workaround:

var pos = transform.origin
var col = move_and_collide(movevec * delta)
if col:
    transform.origin = pos + movevec.normalized() * col.get_travel()

IMHO, move_and_collide() should be doing it this way already (and is what I would expect from the description in the docs). Otherwise why also have move_and_slide()? (;

All 20 comments

This is also happening in 2D, the kinematic body slides down the slope when using move_and_collide. It can be reduced to a minimum setting to almost zero the safe margin in the kinematic body. But it's sliding anyway, only slower.

I made a minimum project with this behaviour, just make sure Debug/Visible Collision Shapes is enabled.

I've run into this problem while trying to create a character controller that also handles stair stepping. Using move_and_collide() as a sort of trace and down-stepper. Only now they slide down slopes at speeds determined through the safe margin :(

Same here. Any workarounds? I'm using 3.0.5

Setting the kinematic body safe margin to a very very small value should mitigate the sliding.

Sorry for bumping an old issue, but I can still reproduce this even with margin set to 0.001 (lowest value which the editor allows.)

Making the "move the object outside of the solid world" step optional might go a long way towards making this less of a problem. The "pop outside of anything you're touching" step should only happen once a frame, but people using move_and_collide might want to use it multiple times per frame.

If you're trying to write your own movement solver (which is absolutely imperative for some genres!) then you're going to be using move_and_collide until you're nearly touching things multiple times a frame, inside the safety margin, but that doesn't mean you want to pop out of them yet.

Popping out of the world can also sometimes be done better by the movement solver, at least in simple cases like "hey, I'm standing on a slope, I only want to pop out of it in a straight vertical line".

It would be nice if, instead of moving to a point where the collision needs to push the KinematicBody out, move_and_collide() moved all the way up to, and stopped at, the point where a collision would happen. Like, move_and_collide(), not move_and_penetrate_then_push_away_from_the_normal(). :)

Workaround:

var pos = transform.origin
var col = move_and_collide(movevec * delta)
if col:
    transform.origin = pos + movevec.normalized() * col.get_travel()

IMHO, move_and_collide() should be doing it this way already (and is what I would expect from the description in the docs). Otherwise why also have move_and_slide()? (;

I couldn't get this workaround to work. The travel is a vector3, so multiplying it by the move direction just got crazy results and teleported my character into walls and such. I tried adding the pre-move transform and travel together, but that just had the same result as not using the workaround (still had the slow slide down hills). I also tried to get the length of the travel and multiply that by the move direction, but that ended up making the character fall through the ground.

Looking at the source, it seems the travel combines the movement and recover motion, so I don't think using this value can result in anything much different from the initial behavior.

I tried messing around with the remainder, since that doesn't take the depenetration into account, but that resulted in the character slowly sinking into the ground.

After a few hours digging around in the physics code, I eventually decided to do an ugly hack:

If the horizontal velocity is 0, and the ground touched has a walkable slope, I set the horizontal component of the position to the position prior to moving.

This specifically fixes the case of not moving while on a slope causing the character to slide down, but does not fix the fundamental issue of the margin nudging things around to unexpected locations (instead of the move_and_collide() just stopping at the desired collision point).

The bulllet code is broken into 3 phases (see SpaceBullet::test_body_motion()):
1) depenetration using margin
2) sweep without margin
3) depenetration (again) with margin -- collision info is returned from this, which is why it's sometimes wonky and not the actual surface normal.

I tried disabling both of the depenetrations and actually got much better results for the most part. Unexpected sliding and nudging were gone, and the normals seemed to be correct (most notably on non-uniformly scaled collision, which can get really wonky, especially with small margins). Unfortunately, this resulted in the character sticking frequently when moving parallel to surfaces (such as walking on the ground). Fixing this would likely require nudging the kinematic body slightly, bringing us back to square 1. I wonder how other engines handle this.

Other engines are basically just very careful about exactly when they do depenetration, and sometimes "how" as well. move_and_collide would make a lot more sense if there were some way to control when and how depenetration happens, like only doing it once per frame even if you call move_and_collide multiple times per frame, or making it try to go straight up/down before falling back to using collision normals.

Thanks @raincomplex for the solution! It was enough to get me the rest of the way there, which I accomplished by rounding off the starting position and movement vector to a fairly precise grid via Vector2.snapped(Vector2(0.000001, 0.000001)).

it seems to me that the issue is SpaceBullet::recover_from_penetration
instead of moving the collider back the path it came from, it calculates the recover motion vector with the normal of the collision.
see here:
https://github.com/godotengine/godot/blob/c4daac279b8ec6f4893056ba6717624f701ab970/modules/bullet/space_bullet.cpp#L1284

With that, when hitting a collider with an angle, when the recovering happens it basically is reflected from the collision object, moving back along the collision normal.
This happens every frame, hence the object appears to be sliding along the slope.

recover_from_penetration is used not just after tracing but also before tracing, intended purpose being pushing the object outside of anything it's already overlapping (depenetration). The code you're pointing out is correct in that context (depenetration).

I tried rewriting the test_body_motion function to not use the margin or recover from penetration and have it PRETTY CLOSE to working the way I would expect it to, but there are still a few oddities/edge cases that I need to work around. This is what I have so far:

```bool SpaceBullet::test_body_motion(RigidBodyBullet *p_body, const Transform &p_from, const Vector3 &p_motion, bool p_infinite_inertia, PhysicsServer::MotionResult *r_result, bool p_exclude_raycast_shapes) {

btTransform body_transform;
G_TO_B(p_from, body_transform);
UNSCALE_BT_BASIS(body_transform);
btTransform start_transform(body_transform);
btVector3 motion;
G_TO_B(p_motion, motion);
bool has_collision = false;
bool needs_iteration = true;
bool needs_unstuck = false;
int iterations_left = 3; // Max of 3 iterations
real_t unstuck_margin = 0.001; // TODO: use margin settings?
btVector3 unstuck_offset(0.0, 0.0, 0.0);
real_t allowed_penetration = 0.0; // dynamicsWorld->getDispatchInfo().m_allowedCcdPenetration

next_iteration:
// Do a sweep. If something hits without movement, back out along normal and try again.
while (needs_iteration) {
needs_iteration = false;
const int shape_count(p_body->get_shape_count());

    for (int shIndex = 0; shIndex < shape_count; ++shIndex) {
        if (p_body->is_shape_disabled(shIndex)) {
            continue;
        }

        if (!p_body->get_bt_shape(shIndex)->isConvex()) {
            // Skip no convex shape
            continue;
        }

        if (p_exclude_raycast_shapes && p_body->get_bt_shape(shIndex)->getShapeType() == CUSTOM_CONVEX_SHAPE_TYPE) {
            // Skip rayshape in order to implement custom separation process
            continue;
        }

        btConvexShape *convex_shape_test(static_cast<btConvexShape *>(p_body->get_bt_shape(shIndex)));

        if (needs_unstuck) {
            // I thought maybe if we moved OUT of collision, it wouldn't have an immediate hit, but apparently it does, so we'll have to unsafely back out :|

if 1

            btTransform shape_unstuck_from = start_transform * p_body->get_kinematic_utilities()->shapes[shIndex].transform;
            btTransform shape_unstuck_to(shape_unstuck_from);
            shape_unstuck_to.getOrigin() += unstuck_offset;
            GodotKinClosestConvexResultCallback btResult(shape_unstuck_from.getOrigin(), shape_unstuck_to.getOrigin(), p_body, p_infinite_inertia);
            btResult.m_collisionFilterGroup = p_body->get_collision_layer();
            btResult.m_collisionFilterMask = p_body->get_collision_mask();
            dynamicsWorld->convexSweepTest(convex_shape_test, shape_unstuck_from, shape_unstuck_to, btResult, allowed_penetration);
            body_transform.getOrigin() = start_transform.getOrigin() + btResult.m_closestHitFraction * unstuck_offset;

else

            //shape_world_from.getOrigin() += unstuck_offset;
            body_transform.getOrigin() += unstuck_offset;

endif

        }

        btTransform shape_world_from = body_transform * p_body->get_kinematic_utilities()->shapes[shIndex].transform;
        btTransform shape_world_to(shape_world_from);
        shape_world_to.getOrigin() += motion;
        GodotKinClosestConvexResultCallback btResult(shape_world_from.getOrigin(), shape_world_to.getOrigin(), p_body, p_infinite_inertia);
        btResult.m_collisionFilterGroup = p_body->get_collision_layer();
        btResult.m_collisionFilterMask = p_body->get_collision_mask();

        dynamicsWorld->convexSweepTest(convex_shape_test, shape_world_from, shape_world_to, btResult, allowed_penetration);

        if (btResult.hasHit()) {
            // If we get stuck immediately moving close to parallel to a surface, back up a little bit and try again.
            if (btResult.m_closestHitFraction == 0.0 && iterations_left > 0 && motion.normalized().dot(btResult.m_hitNormalWorld) > -0.01) {
                // Stuck immediately.  Try to move out a bit.
                --iterations_left;
                needs_iteration = true;
                needs_unstuck = true;
                unstuck_offset = btResult.m_hitNormalWorld * unstuck_margin;
                goto next_iteration;
            } else {
                /// Since for each sweep test I fix the motion of new shapes in base the recover result,
                /// if another shape will hit something it means that has a deepest penetration respect the previous shape
                motion *= btResult.m_closestHitFraction;
                /// jitspoe - fix case where collision happens but we don't get any results returned.
                has_collision = true;

                if (r_result) {
                    const btRigidBody *btRigid = static_cast<const btRigidBody *>(btResult.m_hitCollisionObject);
                    CollisionObjectBullet *collisionObject = static_cast<CollisionObjectBullet *>(btRigid->getUserPointer());

                    B_TO_G(motion, r_result->remainder); // is the remaining movements
                    r_result->remainder = p_motion - r_result->remainder;

                    B_TO_G(btResult.m_hitPointWorld, r_result->collision_point);
                    B_TO_G(btResult.m_hitNormalWorld, r_result->collision_normal);
                    //B_TO_G(btRigid->getVelocityInLocalPoint(r_recover_result.pointWorld - btRigid->getWorldTransform().getOrigin()), r_result->collider_velocity); // It calculates velocity at point and assign it using special function Bullet_to_Godot
                    r_result->collider = collisionObject->get_self();
                    r_result->collider_id = collisionObject->get_instance_id();
                    //r_result->collider_shape = r_recover_result.other_compound_shape_index;
                    //r_result->collision_local_shape = r_recover_result.local_shape_most_recovered;
                }
            }
        }
        // TODO: Move back by unstuck offset.
    }

    body_transform.getOrigin() += motion;

//next_iteration:
}

if (needs_unstuck) { // Move back by unstuck amount to stop stuff from floating.
    btTransform correct_unstuck_to(body_transform);
    correct_unstuck_to.getOrigin() -= unstuck_offset;
    GodotKinClosestConvexResultCallback btResult(body_transform.getOrigin(), correct_unstuck_to.getOrigin(), p_body, p_infinite_inertia);
    btResult.m_collisionFilterGroup = p_body->get_collision_layer();
    btResult.m_collisionFilterMask = p_body->get_collision_mask();
    int shIndex = 0;// TODO: Handle multiple shapes in one object.
    btConvexShape *convex_shape_test(static_cast<btConvexShape *>(p_body->get_bt_shape(shIndex)));
    dynamicsWorld->convexSweepTest(convex_shape_test, body_transform, correct_unstuck_to, btResult, allowed_penetration);
    body_transform.getOrigin() -= btResult.m_closestHitFraction * unstuck_offset;
}

if (r_result) {

    if (!has_collision) {
        r_result->remainder = Vector3();
    }

    motion = body_transform.getOrigin() - start_transform.getOrigin();
    B_TO_G(motion, r_result->motion);
}

return has_collision;

}
```

The old code was kind of sketchy in that it allowed the movement to penetrate a set amount during the sweep, then it recovered from the penetration, rather than simply sweeping and colliding:

dynamicsWorld->convexSweepTest(convex_shape_test, shape_world_from, shape_world_to, btResult, dynamicsWorld->getDispatchInfo().m_allowedCcdPenetration);

m_allowedCcdPenetration was 0.04, the default margin value. I was actually using a different margin value than that. Not sure if that makes things better or worse.

Things I've noticed with my changes: Issues with bad normals are MOSTLY fixed. For whatever reason, I still get some bad normal when moving quickly. Not sure why that would make any difference, but... shrug. Also some precision issues that seem worse than I would expect (ex: start colliding, back up, then move back down more than I moved up, but don't collide). Bullet doesn't always report a collision if you start inside of something. Sometimes catch the edge of two objects that are flush next to each other. This was something I was hoping to fix, but instead made worse. :|

Easiest way to repro bad normals is to take a box with collision and scale it a bunch non-uniformly (like make it super long). With the old logic, you'll get normals as though you're running on bumpy ground. With this code it's closer to what it should be.

Still need to iterate on this a bit more. Might be a while before I get back to it. Feel free to take my WIP code and tinker with it.

Bullet's convexSweepTest (or more specifically the btConvexCast::calcTimeOfImpact functions that it eventually depends on, see here) works by having distance support functions, which return a conservative approximation of distance, and moving closer to the target body until the distance support function is smaller than a certain value (0.0001 by default in 32-bit bullet) or it loops too many iterations (32 in 32-bit bullet).

So you can't rely on it to give particularly precise, or stable, results, especially with wildly different speeds. Also, cylinders and meshes are more likely to give bad normals than other shapes in bullet.

Oof. What's a guy have to do to get a nice, clean sweep of a shape that just does some exact math for a convex intersection point? I'm mostly doing box to box collision tests. Is that too much to ask for? I don't want these mushy physics for my platformer. :(

Just tried it in GodotPhysics and Bullet. They both seem to do the slide thing. Just wanted to point that out. (edited)

I was responding to consistency concerns in jitspoe's post.

CC @madmiraal

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Spooner picture Spooner  路  3Comments

n-pigeon picture n-pigeon  路  3Comments

RebelliousX picture RebelliousX  路  3Comments

gonzo191 picture gonzo191  路  3Comments

Zylann picture Zylann  路  3Comments