teaching machines

CS 491 Lecture 16 – Animation States

March 31, 2016 by . Filed under gamedev3, lectures, spring 2016.

TODO

Lab

Today we start a little game called Roundup, where a round character goes around a scene and picks things up. We will animate this character with many different animation clips, and we will go from one animation to another with Unity’s Animator state machine:

Screen Shot 2016-03-31 at 7.47.07 AM

Floor

Let’s start by adding a floor. To keep it simple, let’s just use a Plane. Scale it by (10, 1, 10).

Now add a texture to help make movement across the floor more obvious. I used this one:

grid

Import your image. Select it in the project assets. Make sure its Texture Type is Texture. (If the game started in 2D mode, it will import as a Sprite instead, which is not useful for texturing a plane.) Drop the texture onto your plane object. Unity will automatically create a material.

Examine the now-textured plane in the Scene Editor. It probably doesn’t scale well across the floor. Select the material and set the Tiling to (20, 20). This means Unity will fit 20 repetitions of the texture on the surface along both texture axes. Select the texture and make sure Wrap is set to Repeat. How does the floor look now?

I think it’s too fuzzy. Select the texture asset and set it’s Filter mode to Point sampling. Bilinear sampling interpolates colors between pixels, which smooths out edges.

Character

Now let’s make a character that moves across this floor. Because we are going to be animating its limbs, we will need these independently moving parts to be separate objects. So, start by just adding an empty and rename it Avatar or Player or Slar.

Our ultimate goal is to make something like this:

Screen Shot 2016-03-31 at 8.57.49 AM

Body

Add a sphere as a child object of the avatar. Rename it Body. Center it on the avatar object. Recall that a child object’s position is relative to its parent. What then do you want the position to be if you want it centered on the parent?

Scale it a bit on the y-axis to give it some bodyish elongation.

Head

Add another sphere as a child object to the avatar. Rename it Head. Center it on the avatar object and then raise it above the body. Scale for cuteness.

Legs

Don’t make any legs. We’ve don’t kneed them.

Left Shoulder

That leaves the arms, right? We shouldn’t add these quite yet. Why? Because later on we are going to animate them to swing back and forth when the player walks. If we try to rotate the arms directly, they will rotate about their own origin, which for all Unity-generated objects is in the center of the geometry. But our arms don’t rotate around their center of mass. They rotate around the shoulder joint.

So, let’s first add a shoulder. Create an empty child of the avatar, rename it Left Shoulder, center it on the avatar, and then shift it on the x- and y-axes to where the shoulder should appear. This should will be the arm’s parent and will thereby serve as its pivot point.

This nesting is common practice in animation.

Left Arm

Add a Capsule object as a child of the Left Shoulder. Scale it so it looks like an arm. Rotate it so it lies tangent to the body.

Right Should and Arm

Duplicate the left shoulder and arm to make the right should and arm. Rename them appropriately. Alter their transforms to mirror their left counterparts.

Physics

Finally add a Rigidbody and Capsule Collider to your overall avatar. Also, disable or remove all the colliders from the child objects. We’ll let the overall Capsule Collider do all the collision work.

What’s your avatar’s name?

Moving and Turning

Add to your avatar a PlayerController script. In it, define an instance variable for its Rigidbody, define it in Start, and add a FixedUpdate method. Declare a public float named speed and set it to a non-zero value. Add this code to FixedUpdate to allow the player to move and turn using the cursor keys:

float turnOomph = Input.GetAxis("Horizontal");
float forwardOomph = Input.GetAxis("Vertical");

Vector3 velocity = forwardOomph * transform.forward * speed;

// Preserve any lingering y-velocity from jumps.                                                                                                                              
velocity.y = rigidbody.velocity.y;
rigidbody.velocity = velocity;
rigidbody.MoveRotation(rigidbody.rotation * Quaternion.AngleAxis(turnOomph, Vector3.up));

Playtest. Can you move across the floor?

Third Person Camera

Make the camera a child of the avatar. Center it and then position it behind and above the avatar.

Playtest. Does the camera follow the avatar?

Idle

We’re now ready to add our first animation. Whichever clip we add first becomes the default animation, so we might as well start with the idle animation. Follow the animation workflow to create a clip named Idle for the overall avatar object. The avatar is the only object we will be animating today.

Record keyframes for three properties: the left shoulder rotation, the right shoulder rotation, and the head’s position.

For the shoulders, record just one keyframe at time 0 with the shoulders in their default rotation. Click the diamond-plus button to add a keyframe for the current rotation values. (Autorecording of keyframes requires a change of values.)

For the head, let’s make it be still for a while and then bob up and down. Scrub to 0 and record a keyframe with it at its default position. Scrub to 2:00 and do the same. Scrub to 2:30 and record a keyframe with it elevated a bit. Scrub to 3:00 and restore it to its default location (by copying and pasting an earlier keyframe).

Playtest. Do you see the head drop a little bit before it goes up? Check out the Curves view and see if you can figure out why that happens.

Right-click on the second keyframe in the Curves view and select Left Tangent / Linear to disable the smoothing.

Open Window / Animator and arrange your windows so you can see both your playing game and this window. Select the avatar and play. Do you see Unity walk through this animation state machine? What does the blue bar below the Idle state tell you?

Walk

Now let’s add a second animation that will get triggered when the avatar moves. With the avatar selected, click on Idle at the top left of the Animation window and select Create New Clip. Name this clip Walk. We will swing the avatar’s arms forward and backward using the shoulder pivot points.

Record a three-key sandwich. For the first key, set the left shoulder’s x-rotation so the left arm pivots forward. Pivot the right arm backward. For the second key, invert these so that left arm is back and the right arm is forward. Copy the first key to make the third key. Flatten keyframes in the Curves view.

Playtest with avatar’s animation state machine visible. Nothing different should happen. We are still stuck in the Idle state. Let’s fix that.

Our vehicle for changing animation states is what Unity calls animation parameters. Click on the Parameters tab in the Animator window and create a float parameter named Speed. When this speed goes above 0, we want to transition into the Walk animation. Right-click on the Idle state and click Make Transition. Connect the transition to Walk. Click on the transition arc/edge, and make sure Has Exit Time is unchecked. (We don’t want to automatically time out of this animation.) Click the plus sign in the conditions section and add a rule for transitioning when Speed is greater than 0.

We’ve configured the state machine to transition. Now we just need to communicate the avatar’s speed to it. In your PlayerController, define a reference to the Animator component just like you did the Rigidbody. Then in FixedUpdate, just after the velocity declaration but before we adjust its y-component, add this line:

animator.SetFloat("Speed", velocity.magnitude);

If we include the y-velocity, the animator will make us walk when we are jumping.

Playtest. Do we transition out of Idle when we move forward?

Even after we stop, we keep swinging our arms. Let’s also add an exit transition. Right-click on Walk and Make Transition back to Idle. Uncheck Has Exit Time and add a condition when Speed drops below a small positive threshold. (Because of precision issues with floating point numbers, you can’t and shouldn’t compare a float for equality with 0.)

Playtest. Do our arms start and stop?

Jump Up

Let’s allow our character to jump. Add a public float named jumpForce. Assign it an appropriate value in the Inspector. In Update, add this code to support jumping:

if (Input.GetButtonDown("Jump")) {
  rigidbody.AddForce(jumpForce * Vector3.up);
}

Now let’s add an animation for jumping upward. Select the avatar and create a new clip in the Animation window. Name it JumpUp. Add just one keyframe with both shoulders rotated about their z-axis to press them downward into the body. (When we jump, our limbs press inward as our body narrows. Like this morning, when you bounded out of bed.)

Add a float parameter to the animator named Upness. Set this parameter at the end of FixedUpdate as the rigidbody’s y-velocity.

In the Animator window, we want to add a transition to the JumpUp state. Since we have both Idle and Walk, must we make two transitions into JumpUp? No, thankfully not. We’ll use the meta Any State to factor out the transition. Right-click on Any State and Make Transition to JumpUp. Make sure Has Exit Time is unchecked and add a condition for Upness being above some small positive threshold. 0 is probably not a good threshold, as collisions can sometime causes upward bounces that probably shouldn’t trigger the jump animation.

Add an exit transition back to Idle for when Upness goes below some small positive threshold.

Playtest. When you jump, do your arms tuck inward?

Jump Down

After we reach the apex of our jump, let’s flail the avatar’s arms upward. Add a new animation clip named JumpDown. Add just one keyframe with both shoulders rotated about the z-axis to lift the arms into the air.

Our existing Upness parameter can be used to transition to this state. Make Transition from Any State to JumpDown, with a condition for Upness going negative beyond some threshold. Make Transition from JumpDown to Idle, with a condition for Upness going above some negative threshold close to 0.

Playtest. Do the arms fly up and down when you jump?

Lift

For our last animation we will lift a crate. Add a cube object at the top level of your hierarchy (not a child of anything) and scale it to look somewhat hefty.

When the player hits the L key while near to and facing a crate, we’ll have the avatar lift it up in the air. Let’s use raycasting to check for a crate along the avatar’s forward direction. Add a layer named Pickup and put the crate on this layer.

In Update, add this code, substituting an appropriate value for maxDistance:

if (Input.GetKeyDown(KeyCode.L)) {
  if (crate == null) {
    RaycastHit hit;
    float maxDistance = ?;
    if (Physics.Raycast(transform.position, transform.forward, out hit, maxDistance, 1 << LayerMask.NameToLayer("Pickup"))) {
     crate = hit.collider.gameObject;
     animator.SetBool("IsLifting", true);
   }
  } else {
    animator.SetBool("IsLifting", false);
  }
}

This code only lets us pick up one crate at a time. Add a private GameObject named crate. We will persist a reference to the crate that we pick up so we can do stuff with it later.

Add to the animator a bool parameter named IsLifting. Add an animation clip to the avatar named Lift. Add just one keyframe for the left and right shoulders, rotating them around their x-axis into the air in a hoisting motion. In the Animator, transition out of Idle into Lift when IsLifting goes true. Transition out of Lift into Idle when IsLifting goes false.

Playtest. Near a crate and hit L. Do the arms lift into the air?

Hopefully the arms work, but the crate hasn’t moved a muscle. We want to animate this too. It’s currently not a child of the avatar, so we either need to create a second animator for the crate or somehow change its parent relationship with the player hits L. Since we’d like to rotate the crate up around a pivot point within the avatar’s body anyway, let’s go with the latter option.

First create an empty within the avatar named Lift Pivot. When the player hits L, let’s make the crate a child of this pivot. Add a public Transform named liftPivot in PlayerController, and drop the Lift Pivot into this slot in the Inspector.

Now add these two public methods to be called when we hoist or drop a crate:

public void Attach() {
  // Turn off physics while the crate is being hoisted.
  crate.GetComponent<Rigidbody>().isKinematic = true;

  // We want the crate to be a child of our pivot point so
  // that we can rotate it above the avatar.
  crate.transform.parent = liftPivot;
}

public void Detach() {
  // Turn physics back on now that crate has been dropped.
  crate.GetComponent<Rigidbody>().isKinematic = false;

  // The crate is no longer ours. It has graduated.
  crate.transform.parent = null;
  crate = null;
}

Make sure you understand the code.

Now head to the Lift clip’s dopesheet in Animation window. We’ve already got a keyframe for the shoulders. Let’s add a couple of keyframes for the Lift Pivot. Scrub to 0 and add a keyframe for the Lift Pivot’s default rotation on the x-axis. Scrub to a short time later and add a keyframe with the Lift Pivot rotated 90 degrees upward. (The blue, forward axis should point upward.)

If you playtest now, you won’t see anything happen. The pivot will rotate, but the crate is not yet its child and therefore will not move. This parenting is done in the Attach method, which we have not called anywhere. We could call it at the same time that we set the IsLifting parameter to true. But you’ll find it less obvious where to call Detach. We want to call it after the lift animation is “undone” and we don’t have a hook for that.

Our solution to this is to add a script to the animation state machine. Select the Lift clip in the Animator window. In the Inspector click AddBehavior and create a new script named LiftController. This script has a bunch of callbacks that provide the hooks we need for animation state change events. In OnStateEnter, add a line like this:

animator.GetComponent<PlayerController>().Attach();

Add a similar line to detach the crate in OnStateExit.

Playtest. Can you pick up and drop a crate?

If you are very near the crate when you lift it, it may clobber your head. Fix this by adding to the Lift animation a couple of keyframes for the Lift Pivot’s y-position. At time 0, record its default y-position. At the time time where it’s rotated 90 degrees up, record an elevated y-position.