Wednesday, September 28, 2011

Character Animation Logic

Character animation logic is a surprisingly complicated programming problem. Every single condition for making a character animate the correct way, in the correct context, needs to be explicitly defined by the programmer. Without a clear methodology, this kind of code quickly spirals out of control, and becomes a major pain during the game development process.

Today, I will walk through how I implemented Character Animation Logic in Bullet Time Ninja.


The code included with this article comes from an earlier build of Bullet Time Ninja, when I was still using the red ninja.


I chose to share this code instead of a recent build because the code is a lot more readable.

Understanding the Problem
There are two things we immediately know about our ninja:
  • The ninja has multiple animations
  • The ninja performs exactly one animation at a time
This means that the player has multiple states, and at any point in time will have a current state. We can define a bunch of constants to represent each state, and use exactly one variable to hold the ninja's current state. In traditional Computer Science, this is called a finite state machine. If we threw all the states onto a chart, it would look like this:




Of course, having a bunch of states without any way to transition between them is pretty useless. Let's add that detail to our chart:




The key concept this chart illustrates is this: starting from my current state, does the game require that I transition to any other state? Lets take a look at the implementation details.

Implementation in Code
First we have the state variable, and all of its possible states:

 private var state:String;

 public static const STANDING:String   = "standing";
 public static const RUNNING:String   = "running";
 public static const JUMPING:String   = "jumping";
 public static const WALL_KICK_LEFT:String  = "wall kick left";
 public static const WALL_KICK_RIGHT:String  = "wall kick right";
 public static const AIR_KICK_UP:String   = "air kick up";
 public static const AIR_KICK_DOWN:String  = "air kick down";
 public static const AIR_KICK_LEFT:String  = "air kick left";
 public static const AIR_KICK_RIGHT:String  = "air kick right";
 public static const DYING:String   = "dying";

This code is Flixel-specific. Since each state is a String, I can use the state as a String key that maps to a particular set of animation frames. (see FlxSprite.addAnimation())

 // load the player graphic and add animations
 loadGraphic(ImgSprite, true, true, 16, 16, false);
 addAnimation(STANDING, [0], 15, true); 
 addAnimation(RUNNING, [1, 2, 3, 4, 5, 6], 15, true); 
 addAnimation(JUMPING, [8], 15, true); 
 addAnimation(WALL_KICK_LEFT, [10], 15, true);
 addAnimation(WALL_KICK_RIGHT, [11], 15, true);
 
 addAnimation(AIR_KICK_UP, [9], 15, true);
 addAnimation(AIR_KICK_DOWN, [9], 15, true);
 addAnimation(AIR_KICK_LEFT, [12], 15, true);
 addAnimation(AIR_KICK_RIGHT, [12], 15, true);

Then we have the update logic:

// in my update() loop
// from my current state, see if I transition to any other state
switch(state)
{
 case STANDING:
  if(!shouldStand())
   state = RUNNING;
  if(!onFloor)
   state = JUMPING;
  break;
 case RUNNING:
  if(shouldStand())
   state = STANDING;
  if(!onFloor)
   state = JUMPING;
  break;
 case AIR_KICK_UP:
 case AIR_KICK_DOWN:
 case AIR_KICK_LEFT:
 case AIR_KICK_RIGHT:
 case JUMPING:
  if(onFloor)
  {
   if(shouldStand())
    state = STANDING;
   else
    state = RUNNING;
  }
  else if(onWall)
  {   
   if(facing == FlxSprite.LEFT)
    state = WALL_KICK_LEFT;
   else
    state = WALL_KICK_RIGHT;       
  }
  break;
 case WALL_KICK_LEFT:
 case WALL_KICK_RIGHT:
  if(onFloor)
  {
   if(shouldStand())
    state = STANDING;
   else
    state = RUNNING;
  }
  else if(!onWall)
  {
   state = JUMPING;
  }
  break;
 case DYING:
  
  break;
}

// play animation based on state
play(state);

A couple things to note here:
  • This code is written to be scalable. There are simpler ways to write state transition code if your character only has only 2 or 3 states (basically drop the switch statement). However, once your character has more than 3 states, your code quickly becomes unreadable, and by extension risks subtle logic errors.
  • The switch() statement occurs once per update(). If there happens to be a funny edge case where the character transitions between two different states repeatedly, you would see the character blinking rapidly when playing the game.


Conclusions
Lastly, some coding suggestions:
  1. Keep your state transition logic completely separate from any other logic in your game. The code is already complicated enough as is.
    1. Read any variable values however you like
    2. The only variable you should set is your "state" variable
  2. If the conditions inside your if() statements are complicated, put those conditions in a function that returns a Boolean result. Your future self will thank you for the clarity.

Your partner in science,
Greg


1 comment: