Day 20: Animation (part 1)

It’s time to bring our world to life. Starting today and continuing into tomorrow, we will dive into the wonderful world of animation, specifically how animation is implemented in our engine.

For these lessons, you should download the UGE_Animation package from the course Box folder, found in Day 20 of the course resources. 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.

Part 1: Simple Sprite Animation

“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

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)

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

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 today’s class, we simply connected this to OnCollisionEnter2D, as we were running out of time. Tomorrow we will look at why you may want to use a trigger, as this writeup is showing.

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.


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");
            }
        }
    }
}