Torque 2D/GenreTutorials/PlatformerAnimation
From TDN
[edit] IntroductionThe 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. |
|
[edit] Creating AnimationsI 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.
|
|
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. |
|
[edit] Adding DatablocksUsing 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. |
|
[edit] Animating the PlayerNow 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 = 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.
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.
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.
|




