Days 18 & 19: Animation

It’s time to bring our world and our characters to life. In this mega-lesson, we dive into the wonderful world of animation, specifically how animation is implemented in our engine. We are going to look at a large number of animation types, so I have posted individual workshops for each stage of this. Links inside!

To follow along with these tutorials, you should download the UGE_Animation package from the course Box folder for today’s lesson. You may notice that the sprites in this package have already been set up and sliced, but you are welcome to attempt to re-slice them if you would like the practice.

YouTube Links:

  1. Basic Sprite Animation
  2. Animated Tilemaps
  3. Keyframe Animation
  4. Moving on Command
  5. Enemy Animation
  6. Player Animation

Part 1: Simple Sprite Animation

Part 1: “Coins!” (Simple Sprite Animation) [YouTube]

“Moving” sprites share a great deal in common with the old cartoons that your parents (and grandparents) used to watch. The characters and objects that appeared to move were most often generated from a series of individual images, swapped out with one another. These images were painted onto “cels” (short for “celluloid”) which were transparent sheets. These sheets would be placed over a background image and photographed to create a single frame of animation. The “movement” these individual images evoked was due to the similarity of one image to the next, each offset only slightly from the other. Artists would create this smooth motion by defining “keyframes”, or drawings that would represent the start and end of a particular movement, and then generate drawings in between that would approximate the adjustment of items in the first drawing to items of the last, and these frames were known as “in-betweens”.

In our modern software, we rely on similar approaches to create pre-defined animations. For simple sprite animation, we replace the paintings with digital images (and use transparent pixels to replicate the effect of the transparent celluloid sheet). This creates a discrete progression, where each frame of motion is represented with its own distinct sprite. The other method – which we will use most often for animating objects themselves – allow us to define “keyframes” for our properties, and then the engine will “interpolate” the frames that occur in between.

First, we are going to start simple, with an animated sprite. These are very easy to generate – if we have a series of images (or a series of sprites generated through a sprite sheet), we can select these from our Asset Window and drag them into our scene. When you do this, you will be asked to choose a location and name for your “anim” file. This file is an AnimationClip, and it defines the specifics of this particular animation. You will see this along with an Animation Controller file in your Asset folder, likely with the same name as your new GameObject in the scene. Like in the last lesson, your GameObject will have a Sprite Renderer component, but now it will be accompanied by the Animator component.

By dragging the individual frames of this coin sheet to the screen, we have created a fancy coin animation.

Easy, right?

In the video, I also link up the Coin prefab to a script that will “collect” the coin by destroying the object instance and playing a noise from a “Sound Manager” object that I created for the scene. In this case, I use a Trigger instead of a Collider so that our player will continue to move smoothly through the collider.

Part 2: Tile Animation

Part 2: “Animated Tilemaps” [YouTube]

IMPORTANT UPDATE: Unity 2021.3 now fully supports the “2D Tilemap Extras” package, and it is automatically included as part of the 2D Template. You can disregard the steps to install the “preview package” and skip to the 3:30 mark to follow the instructions.

So what about adding animation to our Tiles? That process, although the underlying principles are nearly identical, is slightly more complicated due to the way that Unity defines its tiles via Scriptable Objects.

Right-click in your Assets panel, and select Create > 2D > Tiles > Animated Tile. This will create a new object that is your animated tile definition.

Select this object. In the Inspector, you will see an area indicating that you can drag tiles to define the animation. Drag your sprite frames into this, and then arrange the individual entries into the proper order (if they are not already so).

Now you have a tile that you can bring into your Tilemap, either by dragging it directly from the Asset Folder onto your Grid, or by adding it to your Tile Palette (again, dragging the tile into your grid object). If this tile is something you expect to use frequently, such as our water animation, I recommend placing it in your Tile Palette so that you can “paint” it into your grid where needed.

Part 3: Keyframe Animation (Moving Platforms)

Part 3: “Moving Platforms (Keyframe Animation)” [YouTube]

Now it is time to make some moving platforms. We start with an “up and down” movement. To do this, we will use the Animation window (not to be confused with the Animator window) to create a keyframed animation.

Before we begin, it is important to note that when you run an animation clip that animates the property of an object, such as position, those values will override everything else.  You can make an animated platform, define a prefab, and then copy it all over the board, but when you hit “play” they will all move back to the same position as the original because that it what the animation clip demands.   There are two methods to solve this. One is to use the Apply Root Motion checkbox in the Animation component of the object, which will treat this animation as an offset of the transform of the object it is attached to. The other method is to animate them relative to a parent object so that they inherit the world position of the parent and move only in relation to that. In a way, it’s doing the same thing, except giving you a little more control over the placement and movement.

For this reason, it’s always best to build these prefabs by placing all objects at (0,0,0), building your and animating there, then defining your prefab from that object.

NOTE: It is also important to note that any transformation will be applied to animated and their children. This means that our “scale” value can cause objects to move differently than expected or defined. Best to leave “scale” alone if you can, especially if you will be parenting an object like an enemy or player that have movement controls of their own.

First, we create a new platform by creating an empty game object that will serve as our platform parent. We place some child sprites adjacent to one another to define our platform, and then add a Box Collider 2D component to the empty parent object, editing the boundaries to fit the children.

With our parent object selected, open the Animation Window (Window > Animation > Animation) and click the “Create” button to generate a new clip.  Give your clip a name.

Once you do this, you will see the Dopesheet, showing the individual frames.

We will want to animate the x-position of this platform, so we click Add Property > Transform > Position > +

Now your dopesheet will show your Transform channels, and you will see diamonds at the start and end of your clip – these are the keyframes.  These are the positions in the timeline when a value is defined.  In between these frames, the game engine will interpolate between the values.

Since we want our animation to loop, it is important that the values for the keyframes be the same at the start and end of the clip.  By adding the property as we did, Unity automatically creates the start and end position.

Next we want to set a new keyframe – the “right” position of the platform.  First we move our time position (indicated by the thin white vertical line on the dopesheet) to the mid-point, here at 0:30.  Then we hit the Record button (the red circle in the upper left).  This will turn the timeline header from blue to red, indicating that recording is on.  Any changes made to the object now will automatically be keyframed into the position on the timeline.

With recording on, select the Move tool and position your gizmo object to the right-most position of your path. (The platform child will follow with you).  You will see that a new set of keyframes are generated.

Turn recording off, then hit the Play button and you can watch your platform move up and down.  If you want to see how the motion path is defined, use the Curves view, found at the bottom left.  Here you can see each property color coded and mapped out over time, and each keyframe is a bezier point with handles that can be edited similar to applications like Illustrator or Photoshop.

Now try it out in your world.  If you are happy with the result, drag the top parent object to the Assets window to convert it into a prefab. Then you can move your instance to whatever location you prefer.  Run your hero character over and see what happens.

You’ve probably noticed that our hero character isn’t behaving as one would hope.  Instead of sticking to the platform, he’s bouncing as we move up and down. And if you created a side-to-side platform, he slides right off.

The problem here is that by creating a slippery rigidbody, we have given up the friction that keep us connected to the objects that we stand on. Also, our platform’s movement is predetermined and falling faster than gravity would initially imply. Our character keeps landing, then falling, catching up and landing again. Thankfully, there is an easy answer to this.

We have already defined the motion of these objects. If we want our player object to move in the exact same way as the platform it is standing on, we can simply make it a child a child of the platform. Rigidbody physics will still be applied in the FixedUpdate, and pull our player down or hold its momentum, but now its position will also adjust with the frames of the animation itself.

Now, unless we want our entire game to play out on this one platform, we need to let the player hop from object to object, meaning we need to let it change parent objects.  Basically we need to let a platform “adopt” the player, and then let the player “emancipate” itself by breaking the lineage.  This is performed using a simple assignment of parentage that we run in a script attached to our platforms.

private void OnCollisionEnter2D(Collision2D collision)
{
    if (collision.gameObject.tag == "Player")
    {
        collision.transform.parent = transform;
    }
}
private void OnCollisionExit2D(Collision2D collision)
{
    if (collision.gameObject.tag == "Player")
    {
        collision.transform.parent = null;
    }
}

Now our character will become a child of the platform when it touches it, and un-child itself once it leaves the surface, through running, jumping, getting pushed off, etc.

Part 4: Animation on Demand

Part 4: “Moving on Command’ [YouTube]

Next we want to create a side-to-side animation to build a platform that will “ferry” us over a great expanse.  Like before, we want to set up the animation clip using the Animation window, but this time we the platform to remain stationary until we land on it, which will be the cue for it to start moving.

We build the same structure – an empty object, containing an empty gizmo object that contains a platform prefab. Our gizmo gets a box collider set to trigger, and an Animator component.

We build the animation clip, editing the X-value to move from 0 to a new position and ending there.   This way I have created a start and end keyframe, rather than a looping sequence.   I adjust the control points by selecting and dragging them until the movement takes the amount of time I wish it too.    I also disable looping in the animation clip that I have created by unchecking Loop Time.

Since we want our object to stay in place, we need to give it some other animation to run until we are ready to trigger our ferry animation.  In order to do this, we will create a new, empty Animation Clip. With our object selected, go to the Animation Window and select the drop-down with our current clip name. Select the “Create New Clip…” option, name it, and just don’t put anything in it. This will be our “idle” state.

You may have noticed that when we create an animation clip for an object, it also adds the Animator component, just as we had with our sprite.  If you open the Animator window and select your platform, you will see an animation state set with our movement clip and our idle clip. Our movement clip is going to be the default clip (highlighted in orange) because we just created it.

Right-click the idle state and select the “Set As Layer Default State” option. Now our Idle clip turns orange, and the arrow from Entry moves directly to it.

Next, we need to set up a transition to move us from the Idle to Movement state. Right click on the Idle state, select “Make Transition” and move your mouse over the movement clip and click it to establish the line.

Note the arrow, indicating which direction of the transition. Transitions have a single direction, and if we wish to return we must create the return transition.

Before we move on, we also want to set up a Parameter, which is a condition that the Animator will use to determine when it is time ot move to the next clip. With the Parameters tab selected, we press the “+” button and add a new Trigger. (A trigger is kind of like a function call – it is only active at the moment it is called. The other types – float, int, and bool, compare their values throughout playback. ) Here we create a Trigger called “StartMoving”.

When we select our transition you will see details about it appear in the Inspector. There are a few important settings for Sprite animation – you will want to uncheck the Has Exit Time checkbox, and set the Transition Duration to 0.0 (this is found under the Settings dropdown). Blending is great for 3D animation, where we need to interpolate between one set of movements and another to make our transition smooth, but in sprite frame animation there is no such thing as “blending” and so we want to use these settings to instantly jump to the next clip.

At the bottom of the Inspector you will see the Conditions panel, which defines the conditions that must be met in order for this transition to take place. Right now we only have one option – the StartMoving trigger.

Now if we play our game, our platform remains stationary. This is because we now need to pass a command to the Animator to fire the trigger that we defined, thus moving us through the clip. (If you have the object selected and the Animator window open, you can do this by clicking the button next to the StartMoving parameter.

In our Platform script, we need to set up a reference to the Animator component. We define a variable:

public Animator animator;

… and then we get the component in the start menu…

animator = GetComponent<Animator>();

… and finally we set our command to launch. If we use the existing collider, then we risk our platform taking off even if our hero just glances the side of it. So instead, I have created a second Box Collider 2D, defined is as “is Trigger” and placed it above and towards the middle of the platform so that it does not accidentally take off without us.

private void OnTriggerEnter2D(Collider2D collision)
{
    if (collision.gameObject.tag == "Player")
    {
        if (isTriggeredByPlayer)
        {
            animator.SetTrigger("StartMoving");
        }
    }
}
The “movement” trigger above our platform.

When the player enters our platform, the SetTrigger( ) command is called, passes our trigger name as the function parameter, and that triggers the animation to run. Because we have turned off the looping action in the animation clip, it will simply hold here.

Part 5: Animating Properties

Part 5: “Enemy Animation” [YouTube]

Next we want to make our enemy move. Our enemies here are relatively simple, and they only need to have two states – walking, and dying. Our first step is to create our walking animation, which we will do from the sprites included in the sprite sheet.

Since our enemy object already exists, and the prefab has been placed into multiple places, it would be easier for us to add the sprite animation to the existing object. We open the prefab, select the enemy object, and open our Animation Window. You should see a message indicating that there is no Animator or Animation Clip associated yet, and a button to let you create those. Click that button, and give your walking clip a name (and location, if you’re trying to keep your file structure neat).

Now this will create an empty Animation Clip. From here, the sprite animation is easy to create. We can simply grab the sprites from the Asset Folder and drag them directly into the Dopesheet on the Animation Window. This will create a sprite animation.

If you play this, you will notice that it moves very fast. That is because Unity by default creates animation clips to run at a sample rate of 60, meaning it holds 60 timing frames per second. Since we are dealing with sprites, turn this setting down to 12, which is the default setting for sprite animation.

Next, create a new clip that will be our “dying” clip. Here, we are going to use the Animation Clip to manage a few properties for us, saving us the trouble of having to make these happen through programming. Our intended behavior is that when an enemy is hit, it will stop moving, blink rapidly, then disappear. During this time, it’s colliders should also be turned off so that the player cannot be affected by accident.

Inside of our empty dying clip, I add new properties, specifically the “enabled” property for both colliders. This will start them with a keyframe at the first and last frame, which I will set to be unchecked. Next I will add the Sprite Renderer > Enabled property, and set keyframes every few frames to cycle this property on and off quickly to create a blinking movement.

Next, I set up the clips in my Animator to move from Walking to Dying only when a particular trigger is called (I’ve called mine “Dying”).

Lastly, I make some modifications in my script by setting the rigidbody to isKinematic so that it will no longer move, setting my speed value to 0, calling the SetTrigger( ) function from the Animator, and putting a 1 second delay on the Destroy( ) call so that the death animation has time to run.

Part 6: Player Animation

Part 6: “Player Animation” [YouTube]

The principles behind our player animation are very similar to those of our Enemy object, except now we have many more clips to move between. For this section, I highly recommend watching the video walkthrough, as a writeup will not do the process justice. However, here is a brief summary of the various animation states we attach to the player object.

We set up parameters to assist with the various conditions that we will want to cause a transition to occur. These include:

  • speed: a float value that will determine if our character should walk, run, or stand idle.
  • isOnGround: a boolean that we will use to determine if our player is mid-air or grounded.
  • Death: a trigger that we will call as events to move us between states

Above is our finished controller, with transitions in place. Here is how I have them configured:

  1. Entry: This step is automatic. Anytime the animation controller is enabled, or if another animation passes through the “Exit” state we will reset to here. Immediately moves to the default state, “player_idle” which holds our idle animation.
  2. Idle/Walk: uses the “speed” float as a condition to move between states. If we are Idle and speed is > 0.1 we will move to “player_walk”. If the current state is Walk and speed is < 0.1 we will move back to “player_idle”.
  3. Walk/Run: Similar to Idle/Walk, transition occurs when the speed crosses a value of 0.9
  4. Jump: All three of these transitions will use the “grounded” boolean, which we will populate from the “isGrounded” property of our character controller. A value of “false” will trigger this transition.
  5. Exit from Jump: Rather than creating multiple return paths and testing each path for speed values inside a range, we end our jump by exiting the animation when “isOnGround” equals “true”. This sends us back to the “Entry” state, which moves us to Idle and then evaluates based on our current speed to arrive at the correct position.
  6. Death: Because we may die at any time, we build the transition to the “Death” animation from the “Any State”, and set the condition to the “Death” trigger.
  7. Exit from Death: We did not use this path this year, but in past years I have also set a trigger “PlayerRevive” to act as a singular event that can move us out of the “Death” state. By going to Exit, we re-enter the flow at the “Entry” point and our animation starts again. However, this only makes sense if we are going to have our player object continue to exist. Since we are likely going to reset by destroying and instantiating, or by simply reloading a scene, this is extraneous and so we did not use it this year.

Animations can be looped by selecting the individual animation in the Asset Window and checking the Loop Time box in the Inspector. Our “Death” animation does NOT loop, as we do not want to die over and over again.

As always, I recommend turning off the Has Exit Time checkbox and setting the Transition Duration to 0 to assure instant transition from one clip to the next.

Next we need to connect our “jump” state. We already defined our Animator component, so we can now just use the SetBool( ) command to update our “isOnGround” status. We set our state to “false” when a jump first occurs:

if (Input.GetButtonDown("Jump"))
{
    jump = true;
    animator.SetBool("isOnGround", false);
}

The return is a little more tricky, because it is the CharacterController2D component that is determining whether or not we have hit ground. Thankfully, they anticipated this and have a Unity Event pre-defined to run when the character “lands” on the ground. We create a public function in our PlayerMovement script…

public void PlayerLanded()
{
    animator.SetBool("isOnGround", true);
}

… and then associate that function to be called when the landing event occurs by setting it up in the On Land Event () panel in the CharacterController2D component.

Now when we land, our grounded boolean forces our animation to exit, at which point it jumps back to the entry point, and everything starts again.

If you find that your Jumping animation seems to stop almost as soon as it starts, this may be due to the CharacterController identifying a “ground” hit right away. Sometimes this can be adjusted by moving the GroundCheck object that is a child of your player object, but other times it may still register a hit. This can be due to the fact that CharacterController2D uses FixedUpdate( ) to do its ground check, and this happens so fast that your character has not had a chance to get far enough off of the ground. Changing this command to a traditional Update( ) will often solve this, and will not affect the script, as this groundcheck is the only thing that occurs within here.



PlayerMovement.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    private CharacterController2D controller;
    public float speed;

    private float horizontalMove = 0.0f;
    private bool jump = false;

    private Animator animator;

    // Start is called before the first frame update
    void Start()
    {
        controller = GetComponent<CharacterController2D>();
        animator = GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        horizontalMove = Input.GetAxis("Horizontal") * speed;

        if (Input.GetButtonDown("Jump"))
        {
            jump = true;
            // set the jump
            animator.SetBool("isOnGround", false);
        }

        // set the animator speed 
        float ourSpeed = Input.GetAxis("Horizontal");
        animator.SetFloat("speed", Mathf.Abs(ourSpeed));

    }

    private void FixedUpdate()
    {
        controller.Move(horizontalMove * Time.fixedDeltaTime, false, jump);
        jump = false;
    }

    public void PlayerLanded()
    {
        // land the player
        animator.SetBool("isOnGround", true);
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "Enemy")
        {
            // player has died
            GetComponent<Rigidbody2D>().isKinematic = true;
            animator.SetTrigger("Death");
        }
    }
}

EnemyScript.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemyScript : MonoBehaviour
{
    public float speed;
    public bool faceLeft = true;

    private CharacterController2D controller;

    // Start is called before the first frame update
    void Start()
    {
        controller = GetComponent<CharacterController2D>();
    }

    void FixedUpdate()
    {
        float horizontalMove = speed * Time.fixedDeltaTime;
        if(faceLeft) { horizontalMove *= -1.0f; }
        controller.Move(horizontalMove, false, false);
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.tag == "TurnAround")
        {
            // hit the collider, turn around
            Debug.Log("Enemy hit a collider");
            faceLeft = !faceLeft;
        } else if (collision.gameObject.tag == "Player")
        {
            Debug.Log("Goodbye Cruel World");
            Destroy(this.gameObject, 1.0f);

            // turn kinematics on to stop physics
            Rigidbody2D rb = GetComponent<Rigidbody2D>();
            rb.isKinematic = true;

            // queue the death animation
            GetComponent<Animator>().SetTrigger("Dying");

            // stop motion
            speed = 0.0f;
        }
    }

}

PlatformScript.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlatformScript : MonoBehaviour
{
    public bool isTriggeredByPlayer = false;
    private Animator animator;

    private void Start()
    {
        if (isTriggeredByPlayer) { animator = GetComponent<Animator>(); }
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "Player")
        {
            collision.transform.parent = transform;

        }
    }

    private void OnCollisionExit2D(Collision2D collision)
    {
        if (collision.gameObject.tag == "Player")
        {
            collision.transform.parent = null;
        }
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.tag == "Player")
        {
            if (isTriggeredByPlayer)
            {
                animator.SetTrigger("StartMoving");
            }
        }
    }
}