03 – Creating AnimationLayers

Introduction

Now that we have finished with initial setup way we can start our animation logic. This tutorial will focus on reading values from the Player entity to set which animation should play. We will be using AnimationLayers and an AnimationController to update our animations.

Adding a Run Button

Our Player character has most of the behavior we would need for movement that could be found in Super Mario World except for one thing – a run button. We’ll add a simple implementation for a run button so that we can use it in our animation logic.

To do this, modify the Player.cs file as shown in the following snippet.

 

public partial class Player
{
    public IPressableInput RunInput { get; set; }

    private void CustomInitialize()
    {
        RunInput = InputManager.Keyboard.GetKey(Microsoft.Xna.Framework.Input.Keys.R);
    }

Note that in a real game, the RunInput would be based on the input device, but we’re going to always bind it to the R button to keep the code simple.

Adding the AnimationController

The AnimationController class is used to control which animation is displayed by our Player’s SpriteInstance. We will create an AnimationController in code and add AnimationLayers in order of priority. The first layers created will only apply if the later layers return null. Typically the first layer added is an idle layer, as idle animations only show if Player entities are not performing any other action.

The AnimationController and AnimationLayers are all created in the CustomInitialize of our Player. To add the AnimationController, modify the Player code as shown in the following snippet:

 

public partial class Player
{
    AnimationController animationController;

    public IPressableInput RunInput { get; set; }

    private void CustomInitialize()
    {
        RunInput = InputManager.Keyboard.GetKey(Microsoft.Xna.Framework.Input.Keys.R);

        animationController = new AnimationController(SpriteInstance);

        var idleLayer = new AnimationLayer();
        idleLayer.EveryFrameAction = () =>
        {
            return "CharacterIdle" + DirectionFacing;
        };
        animationController.Layers.Add(idleLayer);

        var lookUpLayer = new AnimationLayer();
        lookUpLayer.EveryFrameAction = () =>
        {
            if(this.VerticalInput.Value > 0)
            {
                return "CharacterLookUp" + DirectionFacing;
            }
            return null;
        };
        animationController.Layers.Add(lookUpLayer);

        var walkLayer = new AnimationLayer();
        walkLayer.EveryFrameAction = () =>
        {
            if(this.Velocity.X != 0)
            {
                return "CharacterWalk" + DirectionFacing;
            }
            return null;
        };
        animationController.Layers.Add(walkLayer);

        var runLayer = new AnimationLayer();
        runLayer.EveryFrameAction = () =>
        {
            if(this.XVelocity != 0 && RunInput.IsDown)
            {
                return "CharacterRun" + DirectionFacing;
            }
            return null;
        };
        animationController.Layers.Add(runLayer);

        var skidLayer = new AnimationLayer();
        skidLayer.EveryFrameAction = () =>
        {
            if(this.XVelocity != 0 && this.HorizontalInput.Value != 0 && 
                Math.Sign(XVelocity) != Math.Sign(this.HorizontalInput.Value) &&
                this.RunInput.IsDown)
            {
                return "CharacterSkid" + DirectionFacing;
            }
            return null;
        };
        animationController.Layers.Add(skidLayer);

        var duckLayer = new AnimationLayer();
        duckLayer.EveryFrameAction = () =>
        {
            if(this.VerticalInput.Value < 0) { return "CharacterDuck" + DirectionFacing; } return null; }; animationController.Layers.Add(duckLayer); var fallLayer = new AnimationLayer(); fallLayer.EveryFrameAction = () =>
        {
            if(this.IsOnGround == false)
            {
                return "CharacterFall" + DirectionFacing;
            }
            return null;
        };
        animationController.Layers.Add(fallLayer);

        var jumpLayer = new AnimationLayer();
        jumpLayer.EveryFrameAction = () =>
        {
            if(this.IsOnGround == false && YVelocity > 0)
            {
                return "CharacterJump" + DirectionFacing;
            }
            return null;
        };
        animationController.Layers.Add(jumpLayer);

        var runJump = new AnimationLayer();
        runJump.EveryFrameAction = () =>
        {
            if (this.IsOnGround == false && RunInput.IsDown)
            {
                return "CharacterRunJump" + DirectionFacing;
            }
            return null;
        };
        animationController.Layers.Add(runJump);
    }


    private void CustomActivity()
    {
        animationController.Activity();
    }

While that may seem like a lot of code, once we run the game we see that this code has added a considerable amount of animation logic to our character.

AnimationLayer Logic

As shown in the code above, most of the code needed for creating an AnimationController is the logic for when to display an animation. AnimationLayers are added to an AnimationController in order of priority from low->high priority, so the code near the bottom has the highest priority. If an AnimationLayer’s EveryFrameAction returns a non-null value, then that animation is played. Otherwise, the AnimationController checks the previous AnimationLayer. The logic may continue to cascade down the list of AnimationLayers until one of the EveryFrameAction methods returns a non-null value. Notice that the first layer (which is the last one checked) always returns a value.

To simplify the code, the .achx file we are using contains animations which all end with the word “Left” or “Right”. Therefore, we can append the DirectionFacing to avoid additional if statements. The DirectionFacing property is an enumeration provided and maintained by the generated platformer code. It exists because we set our Player to be a Platformer earlier when we used the Glue Wizard.

Since the last AnimationLayer has the most priority, it can be helpful to look at the code bottom-up. The last animation checks two conditions:

this.IsOnGround == false && RunInput.IsDown

This animation will only be displayed if the user is in the air and is holding the run button. It is the most specific situation defined by the AnimationLayers and will always play if those conditions are met. Of course, if we were to develop this game, we may add higher-priority animations. For example, a higher-priority animation may play if the Player were to collide with an enemy.

As we continue to move up through the AnimationLayer definitions, we see animation logic which will only play if the AnimationLayers below do not return an animation. For example, the duckLayer animation will only play if all AnimationLayers below do not return an animation. By the time the duckLayer AnimationLayer is checked, we logically know that the player cannot be in the air. Therefore, the duckLayer and all AnimationLayers above it can all assume that the Player is on the ground. By the time the idleLayer is checked, all of the other AnimationLayers have been checked, so we know that the player is on the ground and is not holding the horizontal input.

While this approach may seem backwards from a typical if/else if series of checks, it does have a number of advantages:

  1. AnimationLayer definition can be moved to different pieces of code rather than requiring one large if/else if block. This gives you more freedom in organizing your code.
  2. AnimationLayer definition can be performed in generated code. While we do not take advantage of generated code for defining our AnimationLayers, this feature is planned for future versions of Glue.
  3. Standard AnimationLayers could be defined and reused in a project or even between projects.

Conclusion

So far our game works fairly well. The animation looks great and matches the original game closely. Next, we’ll look at creating multiple sets of movement variables to simulate the movement style in Super Mario World.