Saturday, March 13, 2010

Character Physics

When I first started using a physics library to do collision detection/response I realized that character control wasn't really just handed to you on a plate.  I looked at how one would go about adding normal platformer controls using a physics set up and was a bit overwhelmed.  So instead of attempt it I took an easy route and made my characters rolling circles.  Granted that works for some game types but sometimes you need something a little more interesting.  I've been working on adding some platform-y controls to one of my games and figured I'd share what I learned.

The following assumes you are creating controls for a 2D game with some physics library (I recommend Box2D or Chipmunk).

Body Setup
For most games it is important to be able to detect what portion of your character's body is receiving physics callbacks, for this reason we will need to set up a few shapes to attach to our body.

  • Torso
    • main collision shape (in my case a rectangle encompassing shoulders down to bottom of the feet)
  • Head
    • for movement purposes I just want a circle to help the character shrug off his cranial injuries (not get stuck on his head when falling)
  • Feet
    • a sensor shape simple to detect when the feet are touching something
  • Bounding
    • another sensor shape to help correct orientation when fallen over (not needed if fixed orientation)

From the picture we can see these four shapes.  I have colored the physically interacting shapes as green and the sensor shapes as blue.  It is important that the foot sensing shape go beyond the interactive portion of the character so that jump detection can work smoothly.  If you'd rather your character not be able to fall over, you can always lock the orientation and the bounding box I create here is no longer needed.  The next section will detail how I use this box to help orient my character.

Standing Up
If you can get away with locking your orientation you do not need to worry about re-orienting the character.  However, if you do want your character flipping about then please read on!

Without any orientation correction, it is inevitable that your controllable character will fall over and potentially start walking with his belly or get stuck forever.  Neither of these are all that desirable for most games so we will work on helping the character stand back up.  The general idea here is that we know what orientation the body should be in and what orientation the body is in and we want to adjust the orientation to the desired one without breaking the physics simulation.  Let's look at some code.

// desired angle
 float ang_desire = std::fmod( std::atan2(m_Gravity.y, m_Gravity.x), (float)Gosu::pi * 2.0f);
 //Current angle of body
 float ang = std::fmod(m_Body->GetAngle(), 2.f * (float)Gosu::pi);
 //find difference to upright (-gravity)
 float spin = ang_desire - ang;
 if (spin > Gosu::pi) { // Angle wrapping stuff
  spin = ang_desire - (ang + 2.0 * Gosu::pi);
 if (spin < -Gosu::pi) {
  spin = ang_desire - (ang - 2.0 * Gosu::pi);
 //apply torque to correct
 spin = spin * m_SpinAccel; // Get our force
 spin = Gosu::clamp(spin, -m_MaxSpinVel, m_MaxSpinVel); // Clamp to min/max

Here I wish to orient my character to my dynamic gravity vector, so I compute the angle and normalize it to be between 0 and 2*pi;.  I then find the difference angle and normalize that to be between -pi and pi.  Now I should have the closest difference angle to my desired orientation (no rotating over 180 degrees).  I then apply an acceleration magnitude to get a force out of the difference.  Here you may want to note that we want a time-sliced force so you might want to choose an acceleration and divide it by your tick speed.  Of course if you don't mind having your acceleration in terms of 1/20th of a second or whatever your update rate is, don't worry about it.  We then clamp this to some minimum and maximum force so that the character doesn't try and spin wildly about.  Finally we apply the force to our body through the ApplyTorque method.

This can be run constantly, or maybe you want to fire it off only when necessary.  Either approach is fine, it will just affect how much torque you want to apply.  I have a little of both running in my version.  As you run around the character will try and orient himself automatically, but if you do fall over the bounding sensor is used to detect such a situation and allow the player to prop themselves up.  This is done by checking if the character is not touching the ground (foot sensor) but the bounding sensor is touching something.  The player can then press jump to apply a small impulse to the character allowing the re-orientation to take control.

Walking is fairly simple compared to re-orientation.  We simply want to push our character in some direction. Now if we want to only walk while on the ground we need to implement callbacks and keep track of contacts between the character's foot and the ground.  I will probably cover this in a later post as doing this using Box2D's current stable interface is a bit involved.  For now we can just assume we know when the character is on the ground and move accordingly.

Sometimes walking just isn't fast enough and we need to get places faster but we don't have a car/airplane/trained ostrich.  So we make due with running.  For character control this usually just means a modifier key that signals your control code to up the velocity of the character.  Nothing too complicated here.

Now what would a platformer be without jumping?  That's right, something else.  Jumping itself is pretty straight forward, we just need to push the character upward when given the signal.  Now what if we want something like Mario where the longer the button is held the higher he jumps?  Okay now we're talking!

For this, we need to keep track of button states i.e. press/held/release etc..  Let's assume we know these things (I will probably write a post on it later) and head on over to logic town.  When the jump button is first pressed we apply a large impulse to the character upward, this acts as our initial thrust and baseline jump.  We then set off a timer so we don't just let the player hold down jump and jetpack all over the level.  I usually set mine to 1/2 of a second.  Now, as long as that timer is not zero (don't forget to decrement it) and the jump key is held down we continually add a smaller thrust to the character.  I used a ratio of 1:10 for initial to sustained propulsion which seems to work fine for me.  

All this is fine and dandy but sometimes we want more, like say the ability to jump further based on current running speed or spinning around in the air or dynamically changing the animation speed with the running speed.  Really this is where you have a chance to shine and make your movement stand out.  With the basics down you should be able to add a number of things to help deepen your movement.  

And just to see all of this in action, here is a little video of my implementation at work


Unknown said...

Nice work. Thanks for posting this. I'm working on a piece of code for doing torque-based orientation as well and your code has been very helpful.

Is there a way to get the orientation bit less 'wobbly'? The angle adjustment overshoots on each correction, less and less until if finally hits close to the desired angle. Does it do that fo you too? I may have messed something up though.

Is there an approach that can do torque-based orientation that looks as though you're setting the rotation explicitly? (Note, I don't want to set the rotation as it interferes with box2d collision detection, just want the appearance as such)

I've been watching the Khan Academy videos to learn about angular momentum and torque, etc. Not quite there yet though :)

Thanks alot!!

Unknown said...

If you're having trouble with too much wobble in your orientation, you might try using a PID controller.
It looks a little scary, but the idea is that you include the rate of change in error when calculating a response so you don't overshoot as much. For example, if your character is oriented too far left, the naive thing to do would be to apply force to turn it to the right. But if the character is already spinning to the right quickly (this would be the rate of change of the error) you may actually need to apply force to the left to stop if from overshooting in a few seconds. I dunno if this makes sense, but hopefully it'll point you in the right direction.