2D Animation, Part 4 – Code Explanation

Previously on the site we hosted a guest series on 2D animation in Mecanim written by game artist Jonathan Ferriter. In Part 3 of the series, Jon showed us how take our 2D animations and implement them in the Unity engine using the two scripts provided in the post and titled Jump.cs and Move.cs.

In that third video, Jon sort of skims through the makeup of these scripts and doesn’t take the time to explain how they actually work, so I wanted to write this post in order to discuss them further. While writing this post I’ve gone through both scripts, learned how they work, and written comments in them so you might better understand their systems. You can download the updated scripts from this link.

Unity_Logo_small  Guest_JonFerriter_2DAnimation

Aside: I had a hard time tracking down the origin of these two scripts, and I can tell they’ve just been thrown haphazardly around the internet as the ‘quick solution’. So there might be a much better way to do all of this. Of course they work, but consider this a word of caution.

Move.cs

Let’s start with the basics, shall we? Every player character needs a movement system, and the Move.cs script is where ours is. The first part of the script gives us a few important variables.


public float speed = 1.0f;    //character movement speed
public string axisName = "Horizontal";    //stores movement axis name so we don't have to type the string later

public Animator anim;    //for our animator controller

We need the first two variables to set the character’s speed and to move it along the horizontal axis. We don’t actually need to store the name of horizontal axis, and I wouldn’t normally think of doing so. Doing it this way just gives us the added benefit of only having to type the full string out once. The variable can then be used wherever you’d like.

After that, we create a variable for the animator controller just like in the 3D examples in other posts. In the Start() function we make sure to store the animator controller in this new variable.


void Start () {
    //stores the animator controller of our character
    anim = gameObject.GetComponent<Animator>();
}

Next we have the main part of our code, the Update() function. The first thing we do is get the player’s input on the horizontal axis (a value between -1 and 1) and set the Speed parameter in your animator controller equal to it. This will help us determine if a movement animation should be playing.


//set the character's speed parameter in the animator controller based on horizontal axis input
anim.SetFloat("Speed", Mathf.Abs(Input.GetAxis(axisName)));

We’re using the absolute value of our input here to make sure that the speed of the character is always 0 or greater. Using the unmodified axis input would get us a negative value when the player tries to move left (negative) on the axis.

Aside: I’m honestly wondering why the original scripter did it this way. It’s certainly possible to get full positive/negative input on the movement axis and then still act appropriately. My guess is that whoever wrote this script simply may not have understood how to code it this way. And since this is what we have, we’ll go with it. If you’d like me to write up an alternate version of this script, let me know in the comments.

The next part of our Update() is a conditional block used to make our character face left or right when walking or idling, whichever direction it should be facing at the time. Without this functionality the robot would just look to the right all the time, no matter which direction you were moving it. The code solves this here by flipping the character’s direction based on the input value from the horizontal axis.


//if horizontal axis input is negative (moving left)...
if(Input.GetAxis(axisName) < 0)
{
    //set character's local x scale to -1 so it faces its reverse direction (left, not the default right)
    //otherwise the character will always walk to the right (its forward) even when moving left (its backward)
    Vector3 newScale = transform.localScale;
    newScale.x = -1.0f;
    transform.localScale = newScale;
}
//if horizontal axis input is positive (moving right)...
else if (Input.GetAxis(axisName) > 0)
{
    //set character's local x scale to 1 so it faces its forward direction (the default right)
    Vector3 newScale = transform.localScale;
    newScale.x = 1.0f;
    transform.localScale = newScale;
}

So how exactly does this work? First, we check and see if the input on the horizontal axis is negative/left-moving (the top code block) or positive/right-moving (the bottom code block). Let’s say we’re moving left/negative for this explanation. Obviously, we want the character to face left in this case… but wait. IT’S A 2D SPRITE! HOW DO WE ROTATE IT?!?

Simple. We don’t. We actually scale it inversely on the X axis with the three lines of code inside the conditional statement. Instead of ending up with a rotating character, you end up with something more like this:

spriteGif2

Flipping the scale value like this gives us a similar visual effect to a character rotating in 3D space, and the player doesn’t think twice about it.

The last step in our movement code is to apply everything we’ve done previously to the character’s actual position in the game world, a.k.a. the actual movement. We do this using this final line of code:


//set character position by adding the change in movement to the old position
//(add the right axis * the axis input [gets left or right direction] * the movement speed * deltaTime [for smooth framerate])
transform.position += transform.right*Input.GetAxis(axisName)*speed*Time.deltaTime;

We take all of the variables we’ve stored previously (character speed, the horizontal axis input), multiply them by the character’s X axis so they move on the correct axis (this is the transform.right bit), and then multiply them all again by deltaTime to ensure a stable rate of motion on all systems.

Aside: What a mouthful.

That’s the Move.cs script in a nutshell. Let’s head on over and see what the Jump.cs script has in store for us, shall we?

Jump.cs

The jump code provided by Jon in his tutorial is a bit more complicated than the movement code. This is pretty normal, though jumping is such a standard component of video games that you’d think someone would have already built a standardized jump system that can be used across the board. Oh well.

In any case, I just want to let you know this script was a little confusing to pull apart since there were no comments in it when I began reading it over. I hope my explanation of everything is okay, but if you catch me making a mistake please let me know in the comments.

So let’s jump on in. Right off the bat we have a ton of variables being declared at the top of our script.


public string jumpButton = "Fire1";    //stores the string name of our jump button for later use
public float jumpPower = 10.0f;        //the power of a jump, affects jump height
public Animator anim;                  //our animator controller
public float minJumpDelay = 0.5f;      //short delay to make sure jump happens properly
public Transform groundCheck;          //stores the game object that checks if character is on the ground
private float jumpTime = 0.0f;         // a timer for jump cooldown
public bool grounded = false;          //checks if character is on the ground
private bool jumped = false;           //checks if character has jumped (jump button pressed)
private bool canJump = true;           //checks if character can jump now

Just like in our move script, we first store the name of our jump button in a string variable that we’ll use to check the jump button later. A few variables in we have a variable for our animator controller again as well, and further down is the groundCheck variable. As you maybe saw in Jon’s video, the Transform that is stored in the groundCheck variable is a small invisible sphere that Jon attached to the bottom of his robot. If this object is colliding with another object in the scene that is on the Ground layer, the character is on the ground and groundCheck will be true. This variable is used to determine if the character is on the ground and if jumping is possible.

All of the other variables are directly related to jumping. Most of them are pretty straightforward, but I want to hit two of them before we move ahead. The minJumpDelay and jumpTime floats work hand in hand to create what is essentially jump cooldown timer. The minJumpDelay is the minimum amount of time that must take place between jumps. This ensures that the player can’t rapidly press the jump button and initiate a jump sequence more than once while still grounded. The jumpTime float takes the value from minJumpDelay and uses that value every time a jump occurs to check and see if the required delay time has passed. When the timer is up, the player can jump again (as long as other conditions are true). You’ll see all of this as I go through the code, but I thought it would be a lot easier to just explain it to you beforehand.

Aside: It took me quite a while to figure out what these two variables were being used for. I blame their names. Lesson in code: always be clear with your variable names.

Just like in the movement script, our Start() function is used to store our animator controller in the anim variable.


void Start ()
{
    //stores the animator controller of our character
    anim = gameObject.GetComponent<Animator>();
}

The Update() function is where things start to get a little tricky. There’s a lot going on here, and I’ll do my best to clear things up. Our first line of code is responsible for checking to see if the player character is grounded (on the ground).


//cast a line (as opposed to a ray) to see if the player is touching anything on the "ground" layer, returns TRUE or FALSE
grounded = Physics2D.Linecast(transform.position, groundCheck.position, 1 << LayerMask.NameToLayer("Ground"));

This is done by shooting a line straight downward from the center of our robot and seeing if it is intersecting with anything on the Ground layer. If the line intersects with any ground then grounded is set to true. This is done using Physics2D (as opposed to 3D) in order to keep the process simple and save on memory.

This next line of code relates directly to the jumping.


jumpTime -= Time.deltaTime;    //continually subtract from the jump time

The value of Time.deltaTime is subtracted from jumpTime every frame. This is one of the parts that does what I mentioned earlier with the jump delay and is where the jumpTime variable acts as a timer. This is connected to the first conditional statement in our Update() as well.


//set the jump time to 0 if it is negative
if(jumpTime < 0)
{
    jumpTime = 0;
}

Here we make sure our jumpTime variable is never below 0. There is no need for it to be below 0, so we just constrain it here.

At this point in the code we come to the actual jumping functionality. Before our character can jump, two things must be true. The player must have pushed the jump button, and the player must currently have the ability to jump.


if(Input.GetButtonDown(jumpButton) && canJump == true)
{
    ...

If that is the case, we initiate the jumping system.


    ...
    canJump = false;    //can't jump now
    jumped = true;      //has jumped
    grounded = false;   //is not on the ground
    anim.SetTrigger("Jump");    //fire the "Jump" trigger parameter in the animator controller to play jump animation
    rigidbody2D.AddForce(transform.up*jumpPower);    //add upward force to the character based on the jumpPower variable
    jumpTime = minJumpDelay;    //slight delay on the jump to make sure it goes through
}

The code begins by setting the canJump variable to false to make sure that no jumping is possible after the initial jump. We set jumped to true so that our game knows we made a jump (used later), and we set grounded to false. This takes care of all of those ‘administrative variables’ in our code, and now we are prepared to make the jump.

First, we fire the Jump parameter in our animator controller so that our jump animation plays. Since Jump is a trigger-type parameter we don’t need to set it to any values like we would a bool or int parameter. Once it is fired the animator controller will transition automatically into a jumping animation state, and other condition checks will bring it back to an idle or moving state. Next, a 2D force is added to the rigidbody to push the character up into the air with the desired strength set in jumpPower, and the jumpTime is set to the minJumpDelay value to start the cooldown timer.

Aside: It’s very important to note that this code is using physics to make our character jump. This is only one way to make a character jump, and while it is rather simple it does have some drawbacks. For starters, physics are basically uncontrollable. Once you add a jump force to the character, there’s no way to ensure that the force will act on the character in the exact same way every time. It’s just something to think about when you’re developing your games.

Last in our jump script is another conditional statement that resets our jump variables when the character is able to jump again.


//if character is on the ground AND the jump time is less than/equal to 0 AND character has jumped...
if(grounded && jumpTime <=0 && jumped)
{
    canJump = true;             //can now jump
    jumped = false;             //no longer in jump
    anim.SetTrigger("Land");    //fire the "Land" trigger parameter in the animator controller to play landing animation
}

If our character passes the grounded check at the top of our Update(), the jumpTime has reset to 0, and the player has jumped, we make it so our player can jump again and we fire off the landing animation in our animator controller.

Ta-da! The Jump.cs script explained! I hope I’ve been able to impart a fundamental understanding of how these two scripts work and that it wasn’t too painful to comprehend. Writing about code is not always the easiest thing to do, and I’d love to hear in the comments how I managed this time through.

Thanks for sticking around, Warriors! There’s some cool stuff coming up this month, and I’m working out some more neat things for down the line. Stay tuned!



Leave a comment