Torque 2D/GenreTutorials/PlatformerAnimation

From TDN

Contents

Introduction


The actual code necessary to get an animation to load and play is very simple. Yet, this can be one of the most difficult parts of creating a game to get right. The rules defining what animations to play at what time can be very complex and the code can get unwieldy very quickly. The approach I am going to use, and the approach I recommend, is to use a state machine. A state machine consists of a series of states, and rules defining when a state should be changed. I will go into more detail when we get into the actual implementation. First, though, we need to create and load some animations.





Creating Animations


I can't help you with the actual drawing portion of creating animations, because I know nothing about it, but I can give you some tips that will make things easier. The most important thing to remember is that all movement of your character is handled by the code. So, in a jumping sequence, you don't want the character to actually move upward, and in a running sequence, no horizontal movement should occur. You shouldn't break this rule unless you have a very good reason to. You may want to draw the animations with movement, just remember to line them up when you're creating the final image. For the most part, everything should be centered horizontally, and lined up at the feet (or ground contact point) vertically.

The engine requires that all frames of an animation sequence be in a single image file. Usually, you will take that a step farther and include all of the frames of all of the animation sequences for a particular object and place them in a single image file. At this point, sizing can become an issue. First of all, every single frame of animation for a particular object, even those in different sequences, should be the same size. To make things easier, I recommend that size be a power of two. Normally, this will be 128x128 or 256x256, though it need not be square.

The final image that holds all of your frames does need to be sized to a power of two in both directions. Once again, it doesn't have to be square. If your frames are a power of two size like I recommended, lining them up in the final image should be simple. Just grab all your frames and put them in a grid. To determine your grid size, take your number of frames and round it up to the next power of two (13 becomes 16, 17 becomes 32, 64 stays at 64). Then, take that number and divide it into two factors (so, 16 would be 4x4 or 8x2, 32 would be 8x4, 64 would be 8x8 or 16x4). Those two numbers are now your grid size. Put your frames in the grid, save it, and you should be set.

As I alluded to before, I am a horrible artist. Luckily, I was able to get help from Philip Mansfield in creating some nice character art. All of the animations I am using in these tutorials are a part of his free mannequin man animation pack which you can get here. Here is the final image that I will be using for the rest of the tutorials. If You want to use animations of your own, the concepts will still apply, but you will have to modify your code slightly.

Image:platformerPlayerAnimation.png

Right click on the image and select "Save Image As..." or something similar. Save it as player.png in the T2D/data/images directory of this project.





Adding Datablocks


Using animation datablocks, we actually tell the engine about our animations. We also have to redo the playerImageMap datablock to use the cell image mode instead of full. So, replace the playerImageMap datablock in datablocks.cs with all of this:

new t2dImageMapDatablock(playerImageMap)
{
   imageMode = "cell";
   cellWidth = 128;
   cellHeight = 128;
   inset = 5;
   imageName = "~/data/images/player";
};

new t2dAnimationDatablock(playerStandAnimation)
{
   imageMap = playerImageMap;
   animationFrames = "0 1 2 3";
   animationTime = 0.4;
   animationCycle = true;
   randomStart = false;
};

new t2dAnimationDatablock(playerStandRunAnimation)
{
   imageMap = playerImageMap;
   animationFrames = "4 5 6";
   animationTime = 0.1;
   animationCycle = false;
   randomStart = false;
};

new t2dAnimationDatablock(playerRunAnimation)
{
   imageMap = playerImageMap;
   animationFrames = "7 8 9 10 11 12 13 14 15 16 17 18 19 20 21";
   animationTime = 0.6;
   animationCycle = true;
   randomStart = false;
};

new t2dAnimationDatablock(playerRunStandAnimation)
{
   imageMap = playerImageMap;
   animationFrames = "22 23 24 25";
   animationTime = 0.2;
   animationCycle = false;
   randomStart = false;
};

new t2dAnimationDatablock(playerStandJumpAnimation)
{
   imageMap = playerImageMap;
   animationFrames = "26 27 28";
   animationTime = 0.1;
   animationCycle = false;
   randomStart = false;
};

new t2dAnimationDatablock(playerStandJumpFallAnimation)
{
   imageMap = playerImageMap;
   animationFrames = "29 30";
   animationTime = 0.1;
   animationCycle = false;
   randomStart = false;
};

new t2dAnimationDatablock(playerRunJumpAnimation)
{
   imageMap = playerImageMap;
   animationFrames = "31 32";
   animationTime = 0.1;
   animationCycle = false;
   randomStart = false;
};

new t2dAnimationDatablock(playerRunJumpFallAnimation)
{
   imageMap = playerImageMap;
   animationFrames = "33 34 35";
   animationTime = 0.1;
   animationCycle = false;
   randomStart = false;
};

new t2dAnimationDatablock(playerFallAnimation)
{
   imageMap = playerImageMap;
   animationFrames = "36"; // to rid the console of errors I would suggest using animationFrames = "36 36";
   animationTime = 0.0;  // and here change to animationTime = 0.1;       (just what I noticed)
   animationCycle = false;
   randomStart = false;
};

new t2dAnimationDatablock(playerFallStandAnimation)
{
   imageMap = playerImageMap;
   animationFrames = "37 38";
   animationTime = 0.1;
   animationCycle = false;
   randomStart = false;
};

new t2dAnimationDatablock(playerFallRunAnimation)
{
   imageMap = playerImageMap;
   animationFrames = "39 40 41";
   animationTime = 0.1;
   animationCycle = false;
   randomStart = false;
};

new t2dAnimationDatablock(playerStandFallAnimation)
{
   imageMap = playerImageMap;
   animationFrames = "38 37";
   animationTime = 0.1;
   animationCycle = false;
   randomStart = false;
};

new t2dAnimationDatablock(playerRunFallAnimation)
{
   imageMap = playerImageMap;
   animationFrames = "41 40 39";
   animationTime = 0.1;
   animationCycle = false;
   randomStart = false;
};

Here is a quick rundown of the fields in the animation datablocks. imageMap is the image that the animation should grab frames from – in this case our playerImageMap. animationFrames is the actual frames to use for the animation. The frame number starts in the upper left and goes across, then down. The very first frame is frame 0, not frame 1. Animation time is the amount of time, in seconds, that 1 cycle of the animation should take. This number, especially for the run animation, you will need to tweak until it looks right. animationCycle is set to true if you want the animation to loop, or false if you only want to play it once. randomStart tells the engine to start the animation at a random frame. If it’s false, the animation will start at the first frame.






Animating the Player


Now for the fun stuff. The design and implementation of the state machine. Like I said before, a state machine consists of a series of states and rules. We are going to need a state for each animation sequence and rules that define when to change the playing animation. Here is a rundown of all our states and the rules governing each one.

playerStandState

The player is motionless on a platform.

  • If the player is no longer on a surface, go to playerStandFallState.
  • If the user is trying to jump, go to playerStandJumpState.
  • If the player is trying to move, go to playerStandRunState.



playerStandRunState

The player is just starting to run.

  • If the player is no longer on a surface, go to playerRunFallState.
  • If the user is trying to jump, go to playerRunJumpState.
  • If the animation is done, go to playerRunState.



playerRunState

The player is running on a platform.

  • If the player is no longer on a surface, go to playerRunFallState.
  • If the user is trying to jump, go to playerRunJumpState.
  • If the user stopped moving, go to playerRunStandState.



playerRunStandState

The player is stopping running.

  • If the player is no longer on a surface, go to playerRunFallState.
  • If the user is trying to jump, go to playerStandJumpState.
  • If the user is trying to move, go to playerRunState.
  • If the animation is done, go to playerStandState.



playerStandJumpState

The player is jumping from a standing position.

  • If the animation is done, go to playerStandJumpFallState and jump.



playerRunJumpState

The player is jumping from a run.

  • If the animation is done, go to playerRunJumpFallState and jump.



playerStandJumpFallState

The player is in the air after a standing jump.

  • If the animation is done, go to playerFallState.



playerRunJumpFallState

The player is in the air after a running jump.

  • If the animation is done, go to playerFallState.



playerFallState

The player is falling.

  • If the player is now on a surface and moving, go to playerFallRunState.
  • If the player is now on a surface and not moving, go to playerFallStandState.



playerFallStandState

The player is landing and not moving.

  • If the animation is done, go to playerStandState.



playerFallRunState

The player is landing and moving.

  • If the animation is done, go to playerRunState.



playerStandFallState

The player was standing and is suddenly not on a surface.

  • If the animation is done, go to playerFallState.



playerRunFallState

The player ran off a ledge.

  • If the animation is done, go to playerFallState.



Figuring all of that out was actually the hardest part. Translating it into code is a snap. First we need to define our states in player.cs:

$playerStandState = new ScriptObject() { animation = playerStandAnimation; };
$playerStandRunState = new ScriptObject() { animation = playerStandRunAnimation; };
$playerRunState = new ScriptObject() { animation = playerRunAnimation; };
$playerRunStandState = new ScriptObject() { animation = playerRunStandAnimation; };
$playerStandJumpState = new ScriptObject() { animation = playerStandJumpAnimation; };
$playerRunJumpState = new ScriptObject() { animation = playerRunJumpAnimation; };
$playerStandJumpFallState = new ScriptObject() { animation = playerStandJumpFallAnimation; };
$playerRunJumpFallState = new ScriptObject() { animation = playerRunJumpFallAnimation; };
$playerStandFallState = new ScriptObject() { animation = playerStandFallAnimation; };
$playerRunFallState = new ScriptObject() { animation = playerRunFallAnimation; };
$playerFallState = new ScriptObject() { animation = playerFallAnimation; };
$playerFallStandState = new ScriptObject() { animation = playerFallStandAnimation; };
$playerFallRunState = new ScriptObject() { animation = playerFallRunAnimation; };

The reason I have created the states as script objects is so states can hold some corresponding information, in this case the animation they are associated with. This makes state changes much simpler. We could add other fields to this as well if we had any other state dependent properties for the player. One that comes to mind would be the collision polygon. If we were giving the player the ability to crouch, we would probably want to shrink the collision poly during that animation.

Now we need some functionality for setting the state of the player. Add this function to player.cs:

function setPlayerState(%state)
{
   $player.state = %state;
   $player.playAnimation(%state.animation);
}

This just sets the player's state variable and changes the animation to the one corresponding to the new state. To start the whole system, we need to give the player an initial state. Add this to the resetPlayer function in player.cs:

   setPlayerState($playerStandState);

Before I dump the implementation of the rules on you, we have a couple more things that need to be modified. First of all, our player is now going to be animated, so we need to change its object type. In the createPlayer function, change:

   $player = new t2dStaticSprite()
   {
      scenegraph = t2dscene;
   };
   $player.setImageMap(playerImageMap);

to

   $player = new t2dAnimatedSprite()
   {
      scenegraph = t2dscene;
   };

In the same function, remove this line:

   $player.setCollisionPolyPrimitive(8);

And add its replacement to the resetPlayer function:

   $player.setCollisionPolyCustom(4, "-0.2 -0.5 0.2 -0.5 0.2 1.0 -0.2 1.0");
   $player.setSize("20 20");

Instead of using an octagon to define the collision polygon, we are using a rectangle that encompasses the player’s body, but not all the fluff around it. If you want to see this new collision polygon, run the game, then open up the console (by pressing ‘~’) and type “$player.setDebugOn(5);” The player should now have a green rectangle around it. That is the rectangle that the engine will use to check for collisions.

In the collidePlayerPlatform function, add this to keep the player from sliding down platforms it should be allowed to walk on:

      if ($player.state == $playerStandState)
      {
         $player.setLinearVelocityY(0);
      }

And finally, a change to the updatePlayer function. Replace yours with this:

function updatePlayer()
{
   updatePlayerAnimation();
   %move = $moveRight - $moveLeft;
   
   if ($runSurface < 0)
   {
      %speed = $player.getLinearVelocityX();
      if (%move)
      {
         if (((%speed * %move) <= 0) || (mAbs(%speed) < $player.airSpeed))
            $player.setLinearVelocityX(%move * $player.airSpeed);
      }
   }
}

This is the same as it was before, except we added the call to updatePlayerAnimation and removed the jumping code. The jumping code is now handled by the state machine, because it is dependent on the animations for it to look right. The only thing left to do is define updatePlayerAnimation which is basically the state machine implementation. Because the code is a direct translation of the rules we set above, I'm just going to dump it on you for you to copy into your player.cs file:

function updatePlayerAnimation()
{
   // Grab the desired direction of movement.
   %move = $moveRight - $moveLeft;
   %jump = $jump;
   
   // The state machine. The current state of the player is determined, and
   // based on some rules that can change that state, the state is updated.
   switch ($player.state)
   {
      // Standing still.
      case $playerStandState:
         // If suddenly the player is not a surface, start falling.
         if ($runSurface < 0)
            setPlayerState($playerStandFallState);
            
         // If we want to jump, do it.
         else if (%jump)
            setPlayerState($playerStandJumpState);
         
         // And if we want to move, do that.
         else if (%move)
            setPlayerState($playerStandRunState);
      
      // Starting to run.
      case $playerStandRunState:
         // If the player ran off a cliff or some such, start falling.
         if ($runSurface < 0)
            setPlayerState($playerRunFallState);
            
         // If we want to jump, do it.
         else if (%jump)
            setPlayerState($playerRunJumpState);
         
         // And if this animation is done, the player is in a full fledged run.
         else if ($player.getIsAnimationFinished())
            setPlayerState($playerRunState);
      
      // Running.
      case $playerRunState:
      
         // Ran off an edge. We need to make sure this is an actual edge, and
         // not just the top of an incline or set of stairs.
         if ($runSurface < 0)
            {
               // Save the current y velocity so it can be reset.
               %yVel = $player.getLinearVelocityY();
               
               // Cast a collision downward to see if there is a platform close
               // by.
               $player.setLinearVelocityY(100);
               %collision = $player.castCollision(0.1);
               $player.setLinearVelocityY(%yVel);
               
               // No collision, so the player is actually at an edge.
               if (%collision $= "")
                  setPlayerState($playerRunFallState);
                  
               // There was a collision. Now make sure it was with a runnable
               // platform.
               else
               {
                  // Grab the object.
                  %obj = getWord(%collision, 0);
                  // Grab the surface normal.
                  %normal = getWords(%collision, 4, 5);
                  
                  // If the collision is not with a runnable platform, fall.
                  if ((%obj.getGroup() != $platformGroup) ||
                     (isRunSurface(%normal, $player.maxRunSurfaceAngle) < 1))
                  {
                     setPlayerState($playerRunFallState);
                  }
               }
            }
         
         // Jump.
         else if (%jump)
            setPlayerState($playerRunJumpState);
            
         // Stop moving.
         else if (!%move)
            setPlayerState($playerRunStandState);
      
      // Stop running.
      case $playerRunStandState:
         // Fall.
         if ($runSurface < 0)
            setPlayerState($playerRunFallState);
            
         // Jump.
         else if (%jump)
            setPlayerState($playerStandJumpState);
            
         // Start running again.
         else if (%move)
            setPlayerState($playerRunState);
            
         // Stop completely.
         else if ($player.getIsAnimationFinished())
            setPlayerState($playerStandState);
      
      // Standing jump.
      case $playerStandJumpState:
         // Once the standing jump animation is finished, the player actually
         // jumps.
         if ($player.getIsAnimationFinished())
         {
            setPlayerState($playerStandJumpFallState);
            playerJump();
         }
      
      // Running jump.
      case $playerRunJumpState:
         // Once the running jump animation is finished, the player actually
         // jumps.
         if ($player.getIsAnimationFinished())
         {
            setPlayerState($playerRunJumpFallState);
            playerJump();
         }
      
      // Fall from a standing jump.
      case $playerStandJumpFallState:
         if ($player.getIsAnimationFinished())
            setPlayerState($playerFallState);
      
      // Fall from a running jump.
      case $playerRunJumpFallState:
         if ($player.getIsAnimationFinished())
            setPlayerState($playerFallState);
      
      // Falling.
      case $playerFallState:
         // If suddenly the player hit the ground...
         if ($runSurface > 0)
         {
            // If it is moving, start running.
            if (%move)
               setPlayerState($playerFallRunState);
               
            // Otherwise, stand still.
            else
               setPlayerState($playerFallStandState);
         }
      
      // Standing still landing.
      case $playerFallStandState:
         if ($player.getIsAnimationFinished())
            setPlayerState($playerStandState);
      
      // Running landing.
      case $playerFallRunState:
         if ($player.getIsAnimationFinished())
            setPlayerState($playerRunState);
      
      // Standing still fall.
      case $playerStandFallState:
         if ($player.getIsAnimationFinished())
            setPlayerState($playerFallState);
      
      // Running fall.
      case $playerRunFallState:
         if ($player.getIsAnimationFinished())
            setPlayerState($playerFallState);
      
   }
   
   // Set the orientation of the player based on the direction it is moving.
   // If it is not moving, the direction will not change and thus will face
   // in the last direction of movement.
   if (%move < 0)
      $player.setFlip(true, false);
   else if (%move > 0)
      $player.setFlip(false, false);
}

There is a little bit more to this than I laid out in the rules. First, at the end of the function we update the direction the player is facing depending on the direction it is moving. Also, the run state is a little more complicated than you might expect. The code inside the "if ($runSurface < 0)" block is just making sure there is no platform close to underneath the player before it gets sent into the fall state. This is so the player doesn't 'fall' down stairs or inclines.

The nice thing about using a state machine is all of our state changing code is in one place. It is also very easy to expand. If you want to add a new animation sequence, which we will do in further tutorials, all you have to do is add the state, add the rules that change the player into and out of that state.

In the next tutorial we will be adding enemies and giving them some intelligence.