Slow Rush Studios logo,
    depicting an apprehensive-looking snail rushing forward

Slow Rush Studios

◂  Choosing an Engine
News index
Pixel Perfect Rendering  ▸

Making Platforming Feel Good

Contents

This week, I added physics-based platforming support - so now you can control a little box who can move around, jump and push some balls around.

Making movement feel good is really tricky! For example, real life physics would lead you to think that a jump makes you travel in a parabola, so that the time taken to reach the peak of your jump is the same as the time to fall from the peak to the ground... but in a game, that feels terrible!

Physics support

To jump from platform to platform, it helps to be able to know when your character is standing on a platform - which means detecting collisions and responding to them.

I did already have "pixel physics", where pixels can collide and come to a stop, but I wanted the player to be a bit bigger than 1 by 1 pixel.

The traditional platformer approach is to implement collision handling directly: for each box-shaped platform, use its origin and its dimensions to determine whether it intersects with your player box. If all your boxes are axis-aligned then this is some straightforward additions and subtractions, but usually it gets more complicated if you want to have support for sloped objects.

(In my case, support for slopes would have come for free because they're represented as individual pixels already!)

However, I want to have rigid body physics too, so players can push larger objects around, so I cut to the chase and integrated the Rapier physics library.

Weeee, bouncy! Having integrated physics means these balls bounce around properly.

Worlds, scales, and cameras

An annoying wrinkle is that the physics library is tuned so that "1 unit" is 1 metre or 1 kg, and you can't customize that. So when I naively implemented a 20 pixel ball as being 20m wide, well, turns out that if you drop a 20m ball from a height of 200m, it's going to look like it moves veeerrrry slowly initially.

To fix this I needed to manually convert between the two coordinate systems: the screen (bottom right pixel = screen resolution) and the physics game world, where everything is about 10x smaller (10 pixels = 1m).

No, scratch that; I needed to convert between 3 coordinate systems: the two above, plus the game world. Otherwise the window would act as a fixed frame (the camera can't "pan"), rather than since as an actual window through which you can see the game world.

So I implemented that, made the camera movable, and added debug drawing of the physics objects:

A movable camera! Just ignore the water being spawned when I mess with the physics debug draw options - you didn't see anything :D

I'm sure there is a clever way to do the world -> screen transform using the graphics card (something something opengl transform), but all my attempts were "pixel imperfect", meaning that a single logical pixel would be rendered as more than one physical pixel. So I'll punt that to later.

Movement, jumping, and making it feel decent

With the addition of physics and a camera that could hypothetically follow a player character around the world, I just needed to implement the character. Rapier provides a sample controller that lets the character push objects and stops them from falling through walls, and all I had to do was provide a velocity vector of "where the character would like to move to". So how hard could it be, right?

  // first cut of movement logic
  game.player.vel.x = player_input_x * max_speed;
  if player_input_jump_started {
      game.player.vel.y = -12.0;
  }
  game.player.vel += GRAVITY * dt;

Well, it worked, but it felt terrible:

First cut of movement logic

This led me down a rabbit hole of tricks that platformers use to make themselves feel not-terrible.

The first fix was non-linear gravity: firstly, when the player has reached the peak of the jump, increase the player's gravity dramatically so they hit the ground again sooner. Secondly, if the player is still jumping upwards but has released the jump button already, also boost the gravity; this lets players jump lower.

  // Non-linear gravity, based on whether player is jumping or not
  let gravity_mult = if player.vel.y > 0.0 {
      // player has reached peak of jump -> time to fall faster
      fall_multiplier
  } else if player.vel.y < 0.0 && !jump_key_down {
      // player is falling and not holding the jump key -> also fall faster
      low_jump_multiplier
  } else {
      1.0
  };
  player.vel.y += 9.81 * gravity_mult * dt;
Demo of non-linear gravity

You might notice that I also implemented some acceleration-over-time there.

The next tricks were more subtle. Based on a few breakdowns of Celeste and an "Ultimate" controller for Unity, I implemented:

  // jump buffering and coyote timer implementation
  if player.jump_buffer_timer.is_running()
      && (player.on_ground || player.coyote_available_timer.is_running()) {
      player.jump_buffer_timer = Countdown::stopped();
      player.coyote_available_timer = Countdown::stopped();
      player.vel.y = -jump_force;
  }
  // (assuming the timers are started on jump press & leaving ground)

Here's what it looks like:

Jumping buffering (gold dot) and coyote time (blue dot) in action

There are a few more tricks, such as clamping the maximum fall speed to avoid losing control when falling, but they're less interesting. I'll need to keep tweaking all this stuff of course - but at least now it feels pretty nice.

Camera, meet player

Obviously the camera now needed to follow the player, but making the camera directly centre on the player 100% of the time felt a bit too jarring.

First I tried moving the camera a small distance to its destination each time which was a little better, but still not great. I wanted something like Unity's Cinemachine, which implements (among other things) smooth camera movement.

I got lost in maths a little bit, but eventually I found the Getting There In Style GDC talk (recording), which clued me into PD controllers; a PD controller lets me move the camera as if it were on a spring attached to the player. Which I probably would already known if I'd studied a real engineering degree, but let's not get into that.

Playable web build‎

If you manage to make it onto all 7 floating platforms without cheating, you're a hero!

Click to focus, then play with keyboard and mouse. No mobile support! Give feedback.

Shiny new controls for you:

If you fall off the edge, just refresh the page! And if you see lots of gold blobs appearing while holding jump, that's just your OS's key-repeat kicking in - on Web, we don't (can't?) distinguish between that and physical new key presses.

Also, there's a totally intentional thing right now where if you hold jump and hit your head on a platform, you can keep moving upwards and (if you're careful) eventually make it onto that same platform. Sweet, right?

Also, here are your existing controls from last week (which still work):

The pixel physics are not yet interacting with the rigid body physics, so e.g. sand will happily overlap with a circle you spawn.. maybe I'll tackle that next week!

◂  Choosing an Engine
News index
Pixel Perfect Rendering  ▸