NOTE

This article is part of a series:

Introduction

In this series of articles, I’ll use PICO-8 to implement different collision detection methods, in a basic, straightforward, and interactable way.

Not a tutorial

Most of the methods that I plan to showcase already have several free tutorials readily available for you to read and watch. My plan is to succinctly show how a method works, what are the issues and downsides, and how to solve them if possible.

I’ll assume the reader has basic knowledge of game programming and concepts.

PICO-8 API

PICO-8 uses a special flavor of Lua, with some specific API functions. So, the examples will obviously use these functions (e.g. flr() instead of math.floor()), but I’ll try to refrain using P8 idioms (e.g. +=) that don’t exist in standard Lua.

Axis-Aligned Bounding Boxes

Or AABB, is a fancy wording to say “rectangles on a grid”. AABB collision detection is the most basic way to check if 2 rectangles (or points, or circles) collide, and it’s also the easiest to get wrong.

Let’s try this minimal demo, we’ll see what’s wrong with it right below. Use the left/right arrows to move, and the up arrow to (repeatedly) jump. Press ctrl+r if you’re stuck.

The code is available here.

How does it work?

For each frame, depending on our direction, we check the next position of the left/right/up/bottom corners. If that next position collides, we stop the entity. Simple, right? Nope.

Wait, it’s actually buggy?

If you’ve played a bit with my demo, you probably noticed a few issues.

#1: If you jump with a block above you, the jump is stopped before you reach the block

Since your entity is blocked before the collision happens, if you’re moving faster than 1px/frame, your entity will be blocked when there’s still a gap between it and the wall.

#2: There is sometimes a weird “lag” that happens right before the entity touches on the floor

What actually happens is that the entity is falling too fast, and so is blocked, just like in the problem above. Then the gravity is applied again, and it starts to fall again, but slower.

#3: You maybe managed to pass through the floor when falling from high enough.

If you’re falling really too fast (more than 8px/frame, the size of a block) and are a bit unlucky, the collision detection won’t “see” the floor, and your entity will clip through it.

#4: You maybe managed to get stuck inside, or jump through a block.

Since we’re independently checking the horizontal and vertical collisions, it’s possible to clip through a block when moving diagonally.

Important

The first three problems all have the same stem: AABB collisions don’t work well if you’re moving at more than 1px/frame. The fourth issue can be avoided with a specific diagonal check.

So, aside from bandaids like a careful level design and a velocity cap, how to solve this properly?

Collision Response

Like an annoying Souls-like boss, you realize with horror that collision detection was actually the easy first phase. Now comes the dreaded collision response. Ok maybe not dreaded, but we have to think a bit harder.

So far, with our naive implementation, we mostly avoided the response. We detect if an entity will collide with a wall, and we just stop the entity to prevent the collision from happening. It’s a response, but clearly not the best.

An easy solution is to move the entity against the wall before stopping it, instead of just stopping it before it collides.

The updated code is available here.

What did we change?

First, we make sure to cap the velocity to 7px/frame when falling. That should prevent most clipping issues. Then, if we detect a collision when moving, we “teleport” the entity right against the block it’s about to collide with.

-- collision left
if self.dx < 0 and (isColliding(self.x + self.dx, self.y)
  or isColliding(self.x + self.dx, self.y + 7)) then
  -- move the player against the left wall,
  -- if our x coord isn't already a multiple of 8
  if self.x % 8 ~= 8 then
    self.x = flr(self.x / 8) * 8
  end
  self.dx = 0
end

We also added a simple diagonal check to avoid this particular edge case.

So while it doesn’t solve all problems and can be a bit limited, this collision code is now an adequate starting point for most games.

Further improvements

For the sake of simplicity, in these examples, everything has a size of 8 pixels. The logic will still be the same if your entities have a different width and height, and you’ll just have to adapt the values.