TGB/PlatformerStarterKit/Animation Tutorial

From TDN

If you have not yet read over the framework documentation for the PSK, I suggest you have a quick read now! Since there is some ambiguity in how to add different animations to an Actor, I will cover one of the most common changes that you may make, adding a 'shoot' animation.

Since our Actor, at the moment, has three main states (standing, running, jumping), we will need to incorporate three different types of shooting animations:

  • Shoot
  • ShootRun
  • ShootJump

Before we get into the code, I want to recap on how the Animation Finite State Machine (FSM) works.

Contents

Animation Finite State Machine

When an FSM is executed, it runs through a method based on the current state, which in turn determines the next state. The Actor Behavior itself shows a few examples of how to override the FSM state manually - jumping for example. The reason we process things in this manner is so that we always load the animation sequence for the Actor. The 'state' of the Actor is also a very important tool for use within your Actor Behavior itself.

In most cases, you could probably get away with having one large function which determines the next animation for the Actor. In this case, you would have to have a large series of nested 'if' statements which determine the subsequent animation. This is basically what the FSM does, but the current state helps you transition into the new state!

In our case, we're going to go with the 'override' method to load our new state. I prefer to put input events into this category, as they don't really 'flow' on from some of the other states. If you added, say a 'crouch' state, you would place it inside the FSM as you normally would.

The State Code

Firstly, we need the FSM to be able to identify that there is three new states. Place this code inside your PlayerMethods.cs file:

function PlayerClass::initAnimationStates( %this )
{
    // Register Animation States.
    %this.registerAnimationState( "shoot", "ShootAnimation", "ShootSound" );
    %this.registerAnimationState( "shootRun", "ShootRunAnimation", "ShootSound" );
    %this.registerAnimationState( "shootJump", "ShootJumpAnimation", "ShootSound" );
}

The initAnimationStates method is the location you must use to register your animation states. Calls to registerAnimationState in other locations cannot be guaranteed to work correctly! The 3rd parameter specifies an optional sound to play when the state is executed. Now for the states themselves:

////////////////////////////////////////////////////////////////////////////////
/// SHOOT STATE
////////////////////////////////////////////////////////////////////////////////

function PlayerClass::executeShootAnimationState(%this)
{
    if ( !%this.Alive )
    {
        return "die";
    }
    
    if ( %this.Climbing )
    {
        if ( %this.LinearVelocity.Y < 0 )
        {
            return "climbUp";
        }
        else if ( %this.LinearVelocity.Y > 0 )
        {
            return "climbDown";
        }
        
        return "climbIdle";
    }
    
    if ( !%this.OnGround )
    {
        if ( %this.Attack )
        {
            return "shootJump";
        }
        else
        {
            if ( %this.LinearVelocity.Y > 0 )
            {
                if ( %this.Gliding )
                {
                    return "glide";
                }
                
                return "fall";
            }
        }
    }
    
    %moveSpeed         = %this.Speed.X;
    %inheritedVelocity = %this.InheritedVelocity.X;
    %groundVelocity    = %this.GroundObject.LinearVelocity.X;
    if ( %moveSpeed == 0 && %inheritedVelocity != %groundVelocity )
    {
        return "slide";
    }
    
    if ( %this.Attack )
    {
        if ( %this.OnGround )
        {
            if (%this.Direction.X != 0)
            {
                return "shootRun";
            }
        }
        else
        {
            return "shootJump";
        }
    }
    else
    {
        if (%this.Direction.X != 0)
        {
            return "run";
        }
        
        return "idle";
    }
    
    return NULL;
}

////////////////////////////////////////////////////////////////////////////////
/// SHOOT RUN STATE
////////////////////////////////////////////////////////////////////////////////

function PlayerClass::executeShootRunAnimationState(%this)
{
    if ( !%this.Alive )
    {
        return "die";
    }
    
    if ( %this.Climbing )
    {
        if ( %this.LinearVelocity.Y < 0 )
        {
            return "climbUp";
        }
        else if ( %this.LinearVelocity.Y > 0 )
        {
            return "climbDown";
        }
        
        return "climbIdle";
    }
    
    if ( !%this.OnGround )
    {
        if ( %this.Attack )
        {
            return "shootJump";
        }
        else
        {
            if ( %this.LinearVelocity.Y > 0 )
            {
                if ( %this.Gliding )
                {
                    return "glide";
                }
                
                return "fall";
            }
        }
    }
    
    if ( %this.Attack )
    {
        if ( %this.OnGround )
        {
            if ( %this.Direction.X == 0 )
            {
                return "shoot";
            }
        }
        else
        {
            return "shootJump";
        }
    }
    else
    {
        if ( %this.Direction.X == 0 )
        {
            return "idle";
        }
        else
        {
            return "run";
        }
    }

    return NULL;
}

////////////////////////////////////////////////////////////////////////////////
/// SHOOT JUMP STATE
////////////////////////////////////////////////////////////////////////////////

function PlayerClass::executeShootJumpAnimationState(%this)
{
    if ( !%this.Alive )
    {
        return "die";
    }
    
    if ( %this.Climbing )
    {
        if ( %this.LinearVelocity.Y < 0 )
        {
            return "climbUp";
        }
        else if ( %this.LinearVelocity.Y > 0 )
        {
            return "climbDown";
        }
        
        return "climbIdle";
    }
    
    if ( %this.OnGround )
    {
        if ( %this.Attack )
        {
            if ( %this.Direction.X != 0 )
            {
                return "shootRun";
            }
            
            return "shoot";
        }
        else
        {
            if ( %this.Direction.X != 0 )
            {
                return "run";
            }
            
            return "idle";
        }
    }
    else
    {
        if ( !%this.Attack )
        {
            if ( %this.Owner.LinearVelocity.Y < 0 )
            {
                return "jump";
            }
            else
            {
                return "fall";
            }
        }
    }
    
    return NULL;
}

The Actor Code

We now need to load the state somehow! We're going to override the JumpUp and JumpDown methods to check if we should be using the new "shootJump" state:
Note: We are still within the PlayerMethods.cs file

function PlayerClass::JumpUp( %this )
{
    Parent::JumpUp( %this );
    
    if ( !%this.OnGround && %this.Attack )
    {
        %this.setAnimationState( "shootJump" );
    }
}

function PlayerClass::JumpDown( %this )
{
    Parent::JumpDown( %this );
    
    if ( !%this.OnGround && %this.Attack )
    {
        %this.setAnimationState( "shootJump" );
    }
}

Underneath that function, you need to add a new 'attack' method:

function PlayerClass::attack(%this)
{
    // Handle the animation
    if ( %this.OnGround )
    {
        if ( %this.Direction.X != 0 )
        {
            %this.setAnimationState( "shootRun" );
        }
        else
        {
            %this.setAnimationState( "shoot" );
        }
    }
    else
    {
        %this.setAnimationState( "shootJump" );
    }
}

Finally, you need to get the controller to tell the Actor that we intend to shoot. Since the controller came with an 'attack' key, we don't need to register one, but ordinarily you would need to add an additional behavior field. Inside the ControllerBehavior.cs and inside 'keyDown' adjust this:

    // Attack key
    if (%keyDown $= "Attack")
    {
        // Note that the attack key is being held
        %this.Owner.Attack = true;
        
        // Attack!
        %this.Owner.attack();
    }

There is no need to do anything in the 'keyUp' function, so we're all done! Almost...

There are some issues you'll probably will want resolved.

Finishing touches on the animation

If you've gotten this far you've noticed that when you attack your character attacks it gets stuck at the last animation unless you move around or jump to reset the animation. It's because when we're attacking %this.attack is true and it stays true, we need to set it to false at some point so the character will reset itself back. We are going to edit within PlayerMethods.cs, within the animation state

function PlayerClass::executeShootAnimationState(%this)
{
    if ( !%this.Alive )
    {
        return "die";
    }

    %this.Attack = false;

So that is fixed, the character will attack and once the animation goes through it will reset, but the problem is if you let go of attack in the middle of the animation it will end abruptly. We need another fix here.

function PlayerClass::executeShootAnimationState(%this)
{
    if ( !%this.Alive )
    {
        return "die";
    }

    // Animation Finished?
    %puppet = %this.getAnimationPuppet();
    if ( !%puppet.getIsAnimationFinished() )  
    { 
        return NULL;
    }
  
    %this.Attack = false;

So what this does is, once the animation is called it will check to see if it's finished, if not it will continue through it even if you let go of attack. Another issue is if you attack you can still push Left or Right and the character's animation will flip.

function PlayerClass::executeShootAnimationState(%this)
{
    if ( !%this.Alive )
    {
        return "die";
    }

    // Animation Finished?
    %puppet = %this.getAnimationPuppet();
    if ( !%puppet.getIsAnimationFinished() )  
    {
        %this.UpdateDirection = 0;
        return NULL;
    }
  
    %this.Attack = false;
    %this.UpdateDirection = 1;

Here we turned off within the Animation the ability to update your direction, so the animation will be unable to flip. Then after the animation is done we turn back on the ability to flip so the character can move left or right and flip correctly.

The last issue you might have is when attacking you can move left or right and slide across the ground, we can turn that off as well.

function PlayerClass::executeShootAnimationState(%this)
{
    if ( !%this.Alive )
    {
        return "die";
    }

    // Animation Finished?
    %puppet = %this.getAnimationPuppet();
    if ( !%puppet.getIsAnimationFinished() )  
    {
        %this.UpdateDirection = 0;
	%this.LinearVelocity	= 0 SPC 0;
	%this.MoveSpeed = 0 SPC 0;

        return NULL;
    }
  
    %this.Attack = false;
    %this.UpdateDirection = 1;

Here we have killed LinearVelocity and we have killed MoveSpeed to be zero so once you attack you will stop on a dime.

Similar Animation

What if you have multiple playable characters and you want them all to share the same animation states? If you want to setup the code so multiple characters can share the same states make the following edit within PlayerMethods.cs within initAnimationStates

function PlayerClass::initAnimationStates( %this )
{
    %temp = %this.ActorType

    // Register Attack Animation States.
    %this.registerAnimationState( "shoot", %temp @ "ShootAnimation", "ShootSound" );

What if say you wanted the Dragon's Run animation to be the Drill's Idle Animation and you didn't want to create a new animation all together? That is possible too.

Within datablocks.cs (the one under the folder gamescripts) add the following lines within DragonActorTemplate:

    AnimationData     = "DragonAnimationData";

Now create a new section with the following:

datablock SimDataBlock( DragonAnimationData )
{
    RunAnimation      = "DrillIdleAnimation";
};

The benefit of this is you can create animation states without having to duplicate animations just with different names. The SimDataBlock section is not needed otherwise for creation of a character.