I rebuilt my collision system three times. Here's the one that worked.

In which I learn that 'just use the physics engine' is good advice you only believe after ignoring it for two weeks.

The BFranse mark, used as a placeholder cover for this post.

The first version of my collision was a giant if statement. It worked, in the way that holding a door closed with your foot works. When I added a second enemy type, the foot wasn’t enough.

I rewrote it as a state machine. This was technically progress and emotionally a defeat, because state machines are what you build when you don’t yet know the shape of the problem.

What finally clicked

The trick (and this is embarrassingly the literal first paragraph of the Godot docs) is to let move_and_slide() do the actual moving, and treat your code as the part that decides, not the part that computes.

“If you’re computing collision normals by hand, ask yourself why. Then ask again, but kinder.”

Once I let the engine carry the math, my code shrank from four files to fourteen lines. Embarrassing in the moment. Liberating in retrospect.

The 14-line fix

func _physics_process(delta):
    var input := Vector2(
        Input.get_axis("left", "right"),
        Input.get_axis("up", "down"),
    )
    velocity = input.normalized() * SPEED
    move_and_slide()
    for i in get_slide_collision_count():
        var c := get_slide_collision(i)
        if c.get_collider() is Enemy:
            on_enemy_touch(c.get_collider())

That’s the whole thing. The first version was 287 lines.

Things I broke

  • Wall-jumps stopped working. Fix: a small coyote-time buffer so the player can still trigger a jump for 6 frames after losing wall contact.
  • Enemy knockback became too strong. Fix: lerp the velocity toward zero over 0.2s instead of zeroing it instantly.
  • One specific tile in the test scene phased through the floor. Fix: the tile’s collision shape was 1px shorter than the visual. I am not above blaming the tile.

Source code

Up on the godot-collision-notes repo. The branch attempt-three is the version above; attempt-one and attempt-two are kept for shame and posterity.