Play With Lua!

Pushing crates

without comments

Player movement is really hard to get right. It seems like it wouldn’t be, like it’s just as simple as “if the left arrow key is down, move to the left”, but it’s actually surprisingly difficult. Feel is really important. Try playing Mario 2: each character plays the same levels with the same enemies and the same rules, but because they have their movement tweaked differently they play totally differently. Luigi can jump the farthest but has a lot of inertia for changing direction mid-jump, for example. It’s something you can tweak indefinitely.

In this post, and probably the next one, I’m going to walk through one way to do it for a top-down (Zelda-like) game in Löve, that feels “right” to me. This isn’t going to touch on code organization at all, or how to start using Löve (there are other posts about that already), it’s just going to be an explanation of what I have figured out over the past couple weeks of poking at this. So, let’s start with some basics:

The environment

We’re going to need a sandbox to play around with this. What I’m going to assume is that the playing area is in an 800×600 window, divided (more or less) into 32×32 square tiles. I’ll have a couple crates the player can push around, too.

We’ll make the player be a circle 16 px in radius (so they fill up an entire square), and the crates will be squares 32 px on a side. Walls will also be made of, er, something, but walls are pretty trivial to model: they’re just areas you can’t move into, they’re the same as the edges of the screen as far as the physics of player movement is concerned. So we won’t have any for right now, in the interest of making this short and simple.

The first version of the engine looks like this:

function love.load()
   math.randomseed(os.time())
   love.physics.setMeter(32)
   love.graphics.setBackgroundColor(64, 120, 64)
 
   world = love.physics.newWorld(0, 0)
   crates = { makeCrate(world, 5, 5),
              makeCrate(world, 5, 6) }
   player = makePlayer(world)
end
 
function makeCrate(world, x, y)
   local b = love.physics.newBody(world, x*32 + 16, y*32 + 16, 'dynamic')
   local s = love.physics.newRectangleShape(0, 0, 32, 32)
   love.physics.newFixture(b, s)
   b:setFixedRotation(true)
   return {body=b, shape=s}
end
 
function makePlayer(world)
   local b = love.physics.newBody(world, 48, 48, 'dynamic')
   local s = love.physics.newCircleShape(16)
   love.physics.newFixture(b, s)
   b:setMass(1)
   return {body=b, shape=s}
end
 
function love.draw()
   local g = love.graphics
 
   -- Draw player
   g.setColor(160, 64, 64)
   g.circle('fill', player.body:getX(), player.body:getY(), 16)
 
   -- Draw crates
   g.setColor(180, 120, 90)
   for _, c in ipairs(crates) do
      g.rectangle('fill', c.body:getX()-16, c.body:getY()-16, 32, 32)
   end
end

It’s pretty basic. We create a couple crates (stored in a list) and a player, and a function that draws them. We make the crates fixed-rotation so that if the player shoves them off-center they still move like you’d expect, aligned to the grid. Because a lot of stuff we’re going to do with the physics engine depends on mass, we set the player’s mass to 1 to simplify the math.

From now on, we’re just going to change this in two places: we’ll make changes to makePlayer and makeCrate to tweak the properties of the physics bodies, and we’ll write love.update to change how the user controls things.

Why use a physics engine?

You may be thinking, why use a physics engine at all for this? Well, we get a lot of things for free. One is, we don’t have to worry about collisions at all, because the game will take care of it for us. With what we’re doing (a circle and axis-aligned squares) this isn’t such a big deal, but it means we can easily add other stuff (big rotating things! chains! whatever!) later without having to mess with the code at this level. More importantly, it means that collisions will feel right: if we take the naive approach and just stop the player when they collide with something, then a glancing bounce off a wall will stop them dead in their tracks just like running straight into a wall will. If we want to not do that, well, we’ll end up writing a physics engine to make it feel right. And this example is all about feeling right.

The naive way

So let’s do this the naive way first. When the user pushes an arrow key, a force is applied to the player:

function love.update(dt)
   local k = love.keyboard.isDown
   local dir = point(0, 0)
 
   local p = player
 
   if k('up') then dir = dir + point.up end
   if k('down') then dir = dir + point.down end
   if k('left') then dir = dir + point.left end
   if k('right') then dir = dir + point.right end
 
   local f = dir * 320
   player.body:applyForce(f.x, f.y)
   world:update(dt)
end

(I’m using the point class I talked about in my last post, because it’s convenient. To use it, put the file in your game’s directory and put require('point') at the top of your file.)

If you try this, you’ll see it’s kinda… floaty. You can push the crates around, all right, but you don’t feel like you’re controlling a guy moving around, you feel like you’re flying a guy doing a spacewalk. For one thing, once you’re going one direction, you won’t slow down naturally, you just keep going until you try to move backward. For another, you have no maximum speed.

Damping

We can fix the first part with some linear damping. Linear damping reduces the velocity by a little bit each tick. Specifically, the new velocity for a body (unless you have some force acting on it, like the arrow keys) is this:

new_vel = old_vel * ( 1 - damping * time_step )

So, if the damping is 0.2, then after one second the velocity is 80% of what it was, after two it’s 80% of that (or 64%), etc. Let’s start with a damping of 8, so that after 1/16th of a second it halves the velocity:

p.body:setLinearDamping(8)

That takes care of slowing down, but it still doesn’t feel right. What we want is to not dampen the velocity when we’re moving, and to limit the velocity to some maximum. So, we need to keep track of whether there are any keys down:

if k('up') then dir = dir + point.up ; kd = kd + 1 end
if k('down') then dir = dir + point.down ; kd = kd + 1 end
if k('left') then dir = dir + point.left ; kd = kd + 1 end
if k('right') then dir = dir + point.right ; kd = kd + 1 end

Then, damping looks like this:

if kd > 0 then
   p.body:setLinearDamping(0)
else
   p.body:setLinearDamping(8)
end

Now, the velocity limit. Pretty straightforward:

function methods:max_speed(body, spd)
   local x, y = body:getLinearVelocity()
   if x*x + y*y > spd*spd then
      local a = math.atan2(y,x)
      body:setLinearVelocity(spd * math.cos(a),
                             spd * math.sin(a))
   end
end

If the speed is greater than a maximum (we’ll use 320, the same speed we reach in a second of movement) we normalize the vector that maximum.

Aligning movement to the grid

Because the map is on a square grid, most of the time the player will want to move along that grid. So, let’s apply a little extra “damping” to the component of the velocity that’s not in line with where they’re moving.

So, if they only have one key down, we do this:

if kd == 1 then
   dampenSidewaysVelocity(player.body, dir, dt)
end

The function it calls is here:

function dampenSidewaysVelocity(body, dir, dt)
   local a = 1 - 4 * dt
   if a > 1.0 then a = 1.0 elseif a < 0 then a = 0 end
   local v = point(body:getLinearVelocity())
 
   if dir.y == 0 then v.y = v.y * a end
   if dir.x == 0 then v.x = v.x * a end
 
   body:setLinearVelocity(v.x, v.y)
end

This is equivalent to a linear damping of 4, but only on the part tangential to movement. To make cornering a little sharper, increase that value; to make it looser, decrease it.

Crates

Now, the player feels pretty good, we want to make the crates act right. The goal here is that pushing a crate should feel like pushing something heavy, crates should be easy to position on grid points, and you should only be able to push one crate at a time.

So, basic things: let’s make crates have a mass of 5 and a linear damping of 4. In makeCrates:

b:setMass(5)
b:setLinearDamping(4)

Now shoving them around feels like it takes effort, and they don’t keep moving after you stop shoving them. So that’s the first goal taken care of.

Position crates on the grid

If a crate isn’t aligned to the grid, we want to give it a little nudge to make it line up to where it should be:

function nudgeToSquare(body, sq, acc)
   local y = body:getY() - 16
   local ty = sq.y * 32
   local f = acc * (ty - y)
   body:applyForce(0, f)
 
   local x = body:getX() - 16
   local tx = sq.x * 32
   local f = acc * (tx - x)
   body:applyForce(f, 0)
end

We take a body and a target (32×32) grid square, and an attraction factor, and use Hooke’s law to make a force to spring it back to that target square. Call this on all the crates, in love.update:

for _, c in ipairs(crates) do
   local sq = point(
      math.floor(c.body:getX() / 32),
      math.floor(c.body:getY() / 32))
   nudgeToSquare(c.body, sq, 20)
end

Keep the player from pushing two crates

Actually, if you try this, it’s already working, but let’s prove it.

Now, if we move the crates 16 units (half a square), they’ll start to spring to the next square over. According to Hooke’s law, the amount of energy required to do that is:

energy = 1/2 * k * x^2

k is the spring constant for two crates, which is 40 (twice the value we passed into nudgeToSquare) and x is how far we want them moved (16). So, total is 5120.

Next, we need to know the maximum momentum that a player can impart to two crates. This is the mass (1) times the max speed (320). Because of conservation of momentum, when a player with a momentum of 320 hits two crates, the total mass-eleven (player and two crates) glob has the same momentum, so it moves away at 29.

The energy of a thing with mass 11 moving at speed 29 is given by:

energy = 1/2 * mass * speed^2

So 4625, which is less than the required 5120.

But wait! The player isn’t just going to hit it and bounce, they will keep pushing forward. But, their pushing forward will be counteracted by the linear damping of the crates. By the time the crates have deflected to the edge of their square, their linear damping has killed their velocity, so there’s now a player imparting a force of 320 to move from rest a body massing 11 (so 3520), which isn’t enough to counteract the force of the spring (40 * 16 at full deflection) on a body massing 10 (two crates, so 6400).

But, one crate, the picture is a little better: The player musters 320 force to move himself plus one crate, so 320 * 6 = 1920, and he’s fighting a spring pulling back at 20 * 16 * 5 = 1600. So the crate gets nudged over the bump on to the next square.

Try it out!

Sorry, that last part was a little hand-wave-y. Really, the important thing to know is what all the different numbers do, and their relationships, so you can tweak them to get the effect you want. I think if you play with the complete example code you’ll agree it feels good for a Zelda-like adventure game, but if the player is supposed to be a tank, say, or a fish, you’d want the movement to feel different. It’s really important for immersion, and aside from basic things like “prove they can’t move two crates”, it’s best to just tweak the variables until it feels like you want.

Written by randrews

August 7th, 2012 at 11:21 pm

Posted in Uncategorized