Need an easy solution to top-down block pushing being buggy

Godot Version

4.4.stable

Question

I have this top-down game in which you have to push boxes around to do things. The problem is on occasion the boxes will just stop being pushable or get jammed in the tileset. This shouldn’t happen because the boxes snap to the grid as soon as they stop moving.

In the video the first tile pushed to the right goes through just fine, but the second one pushed down and then to the right struggles, despite having the exact same Y coordinate. (I have double-checked this by printing the coordinates in console) So why do boxes sometimes fit through spaces, and other times they don’t?

Code for the Box Just makes it snap in place when it slows down.
extends RigidBody2D

func _physics_process(_delta):
if abs(linear_velocity.x) <= 1 and abs(linear_velocity.y) <= 1:
position = position.snapped(Vector2.ONE*4)

Not the full player script, but the part that makes them able to push the box within a physics process.
for i in get_slide_collision_count():
var c = get_slide_collision(i)
if c.get_collider() is RigidBody2D:
c.get_collider().apply_central_impulse(-c.get_normal() * push_force)

And before it is mentioned, I have tested this on Rapier Physics engine. It has the exact same issue, and Rapier is causing some errors in 4.4, so I’d rather avoid switching to it until it is updated. I’d really just like a dirty and simple workaround for this.

OOC, does the player character also have the same y coordinate in both instances? In the first push (successful box push) it looks like the character is flush with the wall above, but in the second push, it looks like there is a gap. I wonder if this causes the central_impulse to be slightly in the up direction, causing the box to hit the wall.

This might have been part of the issue. I have added the snapping script to the player

Like this

if abs(velocity.x) <= 0.1 and abs(velocity.y) <= 0.1:
position = position.snapped(Vector2.ONE*4)

however, on seemingly random occasions it just doesn’t work? Just an example but my player’s position stops at x 107.9339 when it theoretically should snap back to a rounded coordinate.

Not at my computer at the moment, so this will be brief but: maybe try setting the impulse direction to a straight north/south/east/west direction. Choose the direction based on the dot product between each of these unit vectors and your collision direction vector

That sounds promising. I unfortunately am not sure exactly how to do that, so if you could provide some code later, I’d really appreciate it.

I think ideally I’d like the player to be able to move freely while the boxes stay snapped to a grid but loosen from the grid only on the axis they’re being pushed on and snap back to it when it stops.

I haven’t dabbled with this physics engine, but checking the codes for RigidBody2D, Vector2, and KinematicCollision2D, I’d check what your get_normal is in the instances where stuff isn’t working. Theoretically you should get a normal in a straight direction if everything is a box and isn’t able to rotate.
I’d also check the rotation, in case there’s some slight weird stuff going on when the box gets pushed down along the wall. Depending on space/sizing, even a 0.000001 rotation or offset will mathematically prevent the box from moving if it’s rotated at all. Likewise with positions not quite matching up.

I feel like 2D Physics Engines are probably overkill for games like this, especially for things that need to precisely fit in an exact measurement.

Edit: If the normal isn’t an exact up/down/left/right, you could round that off when checked.

I was thinking something along the lines of:

if c.get_collider() is RigidBody2D:
    var collision_direction = -c.get_normal()
    var impulse_direction = Vector2.ZERO
    # Check whether collision is from the left/right or top/bottom
    if abs(collision_direction.x) > abs(collision_direction.y):
        # Collision direction is left/right
        if collision_direction.x < 0:
            impulse_direction = Vector2(-1, 0)
        else:
            impulse_direction = Vector2(1, 0)
    else:
        # Collision direction is up/down
        if collision_direction.y < 0:
            impulse_direction = Vector2(0,-1)
        else:
            impulse_direction = Vector2(0, 1)
    c.get_collider().apply_central_impulse(impulse_direction * push_force)

I might be off by a sign somewhere, but this was the rough idea I had. The comment about dot products was just a slightly simpler way to accomplish the same thing.

1 Like

Thank you so much! This seems to work fairly well.