a simple, accurate platformer physics system in pico-8

I picked up pico-8 recently, and started out by seeing if I could make a platformer collision system on my own. I ended up getting further than I expected, implementing many notoriously difficult features like rigid box pushing, carrying objects, slopes, conveyors, etc.
A couple people have asked for an explanation of the system, so here you go!

Demo:
Z: enable debug stepping
X: change levels
Z+X on same frame: toggle 3d mode

Code

If you would just like to poke around in the project yourself, you can view the code above or download the cart here:

Download torc_platformer.p8

(CC0, do whatever you want with it! Build off it for your own games, whatever. You can credit me or not, I dont mind either way :)

Preamble

This project was entirely made out of self interest, and is also my first time working with pico-8. I didn't do much in the way of optimizations or following best practices. I also didn't follow any preexisting work, it's possible I'm directly recreating an existing approach, I have no idea. This is just what I naturally ended up with as i solved things. I welcome anyone with more experience to build off this work!

That said, here's an explanation of my platformer physics system :)

Summary

This does not use the conventional collision/response solver system. There's no global solver of any kind, nothing tries to shift itself out of collision with anything else. I don't have a good name for the approach but I've been calling it something like “push-forwarding” in my head.

With this approach, any time an object wants to move, it checks if it will overlap anything. If it overlaps a static object, it cancels the move. If it overlaps a movable object, it then checks if that object can move (and so on). If all checks in the chain pass, all objects move forward by the original amount, otherwise none of them move. This outright prevents any object from overlapping anything.

Push check animation

Of course, if an object tries to move more than one pixel in a frame, it would look like it stops before reaching the edge.

Stopping before wall

So the last piece of the puzzle is chunked delta movement. Where the conventional system increases accuracy by increasing iteration count in the global solver, this approach's equivalent is reducing the movement delta. When an object wants to move 4px in a frame, the system chunks that into four 1px movements, and at the end it rounds the final position to the nearest pixel

1px moves

Movement is separated into each axis. This allows you to e.g. run along the ground even though gravity would otherwise cause you to collide with it.

An object landing on another object stores that as its “ground object” and applies its delta movement every frame, creating the effect of carrying objects (or riding on platforms). Objects are processed from the bottom up, which prevents jittering/swaying.

This by default produces the effect where e.g. running right on top of a left-moving platform makes you move “slower”

Ground objects are seen as “static” to the object above, to prevent pushing back down against the ground object. This is less realistic but feels better, letting you e.g. jump at full height even with a box on your head.

Slopes take horizontal movement and apply an equivalent vertical movement, which allows pushing boxes up slopes. This also happens from vertical to horizontal, but only under certain conditions (e.g. player ducking to push downward)

This inherently creates the (sometimes desired) effect of keeping horizontal speed when running up a slope.

Slope movement

Water simply adds upwards velocity to objects inside it.

More detail

Movement

Push check

(check if an actor can move in a given direction, and if so push objects in the way)

Checking actor collision before tile collision is less performant but necessary due to slopes. Since the slope check adds another push check, it may see a valid move as invalid because it did not move actors out of the way yet.
Alternatively, the additional push check could be delegated after the other checks, but I opted for simplicity.

Slopes

One-way platforms

Only return a solid collision under the following circumstances:

Caveats

And that's it!
Much of the code in the demo can be trimmed out if you don't need e.g. water or slopes or whatever.
Feel free to shoot me any questions on my bluesky or my email, I'll happily update this page with any further info people may want.

<3