Effortless Eye Movement: Continued


Using Unity Version 2019.4

If you've been following along with the first part of this tutorial series, you should have a basic eye movement and targeting system in place. If not, check it out and meet us back here!

Let's recap what we should have in place:

  • Our character is set up with an eye movement monobehaviour, which uses either eye bone transforms or UV texture offsets.

  • Your character has an eye rig parented to the head transform.

  • Our character has an animator controller that contains a Default state, with no behaviour, and a LookAtTarget state which uses a LookBehaviour script.

The Problem

There are two things we want to do to our eye movement system at this point: 1) Smooth out the transitions to and from a look state, and 2) Extend our system to allow our character to randomly look around.

The approach will differ greatly depending on the method you've chosen, but both will make use of coroutines.

About Coroutines

The Unity Manual does a pretty good job of explaining coroutines:

"[A coroutine] can be used as a way to spread an effect over a period of time...it is also a useful optimization. Many tasks in a game need to be carried out periodically and the most obvious way to do this is to include them in the Update function. However, this function will typically be called many times per second. When a task doesn’t need to be repeated quite so frequently, you can put it in a coroutine to get an update regularly but not every single frame."

Think of a coroutine like a "to do" list. Rather than execute all of your commands in a single frame update like a normal function, a coroutine allows you to go through your list of commands over time, repeat them on a loop until a condition is met, or even wait before executing a subsequent command.

So with that in mind, let's get to the tutorial part of this tutorial.

The Solution

Since this tutorial promises to cover two different ways to tackle eye movement, we'll need to split the tutorial into two parts. Choose the part that's right for you depending on the eye movement system you went with.

Eye bone transforms

The first things we’ll fix up are our transitions to and from looking. You may have noticed that, although the character follows the target fluidly when they’re in the “LookAtTarget” state, they don’t transition smoothly into and out of it. There are a number of ways to do this, but we’ll use the float parameters within animator.SetLookWeight.

There are three things we need to do to make this happen:

1. Add new parameters We need to create new parameters for every float we’re using in the LookAt interface. I’m only using the head weight and eye weight, so I’ll need two. If you’re using the body weight, you’ll want to add a third. Add these float parameters to your animator.

2. Update the animator with clips: We need to create three new animation clips. These clips will only contain the new float parameters just created. I’ve called these three clips “StartLooking”, “StopLooking”, and “Looking”. Drag your “StartLooking” and “StopLooking” clips to the animator controller as new states. Make note of the values you chose in the animator behaviour. That will be your “max” weight. Open the clips and add the new floats from the animator. Then switch from Dopesheet to Curves and edit the two existing keys. In “StartLooking”, my first key will have a value of 0 for both floats. My end key will have a value of 0.35 (for my max eye weight) and 0.15 (for my max head weight). Repeat this for your “StopLooking” clip with the reversed value (max weight to 0) for both floats. You can add or manipulate the keyframes as you see fit to speed up, slow down or ease the transitions and make it look as natural as possible.

The “Looking” clip will be added to any of the states that uses the behaviour. For now, that’s just our “LookAtTarget” state. Click on that state and drag the “Looking” clip to the Motion field. In this new clip, add the floats and just set both keyframes to your max value. Delete your existing transitions. At this point, you are going to ALWAYS transition to your “StartLooking” with the transition condition and then automatically, with no condition, to “LookAtTarget”. Likewise, you will always transition to your “StopLooking” state, with the DefaultEye condition, before automatically transitioning to the DefaultEye state with no condition. Here’s how mine looks:

3. Update your LookBehaviour code Now that your animator is set up, it’s time to go back into our LookBehaviour code. In the OnStateIK call, add new lines for the float parameters you've just added:

NOTE: If you’ve called your parameters something different, just make sure you replace my float names with your own. You could expose the float names as public string parameters if you wish.

Save the code and go back to your scene. Now when you enter playmode you’ll see a smooth transition into and out of targeting.

This is usable as it is now, but I promised one more behaviour: Randomly looking around.

Again, there are a few ways you could do this. You could, for instance, find the current rotation/position of the character and generate a random point to look at, based on that transform. But we’re going to take an easier route and extend our eye rig.

First, add a child to the eye rig that will contain a number of look targets. You can have any number of look target children, but I’ve gone with six. Name them something like “LookTargetx” where x is a consecutive number. Then duplicate the DefaultLookTarget and drag it under the EyeRig parent. Call this child MovingTarget. We'll use these targets in our EyeMovement script, so let's open it up and start writing our coroutine.

I want my coroutine to do a few things:

  1. I want to switch my characters active looking target to the MovingTarget child.

  2. I want it to pick a random target from the six look target children I just created.

  3. I want to move the MovingTarget child to that position so my characters eyes follow it.

  4. I want to look at that target for a random amount of time.

  5. I want this to repeat for as long as my character is randomly looking around but stop as soon as I leave the state.

  6. When I leave the state, I want my MovingTarget child to move back to the default position

You might be wondering why I’m moving a target around instead of just looking at the new targets. I could do it that way, but just like in our target looking state, the character’s head and eyes would snap into position. I want it to move fluidly, so I’ll have it look at a moving target so it will follow that target around.

This is how we’ll do it: First, we’ll check to see if the character is in the randomly looking state. This bool will be turned on when we enter the state and turned off when we exit. (We’ll add this to our behaviour script in a moment.) If we are in that state, we’ll set up a counter and duration to effectively control how long we want the eye movements to take. I want my character’s eye movement speed to be random, so I’m going to set my movement duration to a random range between 0.15f and 0.5f.

Next, we’ll pick a random target from the look children we created and make sure it’s not the target we’re already looking at. Then we’ll find the current position of the Moving Target child and lerp it to the new target position. After that, we wait for a random number of seconds before we run through the process again!

So with all of that in mind, add the following parameters to your EyeMovement code:

  • public Coroutine lookRandomly: Coroutines can be complicated because you can start them in multiple ways. We’re setting up a parameter to cache the coroutine because that will also allow us to directly stop it.

  • public bool isRandomlyLooking: this bool will be used to check that we’re in our RandomLook state.

  • public float duration: The duration is a private float. It’s value will be determined in our code.

  • public Transform[ ] randomTargets: This is an array for you to add your Look Target children (do not include the Moving Target child in this array)

  • public Transform movingTarget: This is the Moving Target child.

  • public Vector3 nextTarget: The look target your character will be looking at next.

And here's the coroutine you'll need to add:

As far as I know, you can't call coroutines directly from a state machine behaviour, so I’m also going to add a function to start and stop the coroutine based on the isRandomlyLooking bool

Now we should add the RandomlyLooking state to our animator. Since we want to smoothly transition into and out of this state, we'll bracket it with our Start and Stop clips, just like we did for the LookAtTarget state. Set them up exactly the same way, but rename them to indicate the state they are starting and stopping. Remember to add the "Looking" clip, and the behaviour script to the RandomlyLooking state. Here's how my final animator looks:

Finally, add a new trigger parameter to transition into the RandomlyLooking state.

One of the things you might have noticed about my setup is that I always pass through the default state, rather than allowing the random or targeted eye states to be called from Any State. I’ve consciously done this. At first I had planned to expand the look behaviour to enable this, but I decided against it. The character will always need to pass between the default state before randomly looking or targeting an object to maintain the fluidity of the movements. I don’t want to snap directly into a random looking state, and likewise, I don’t want to snap directly to looking at a target. By requiring the character to pass through the default state, I ensure that the start and stop states are always passed through. You can amend this in your own code if you choose to do so.

Go to play mode and try out your randomly looking state. It should look something like this:

Here is our final code for the Eye Movement system:

And our final code for the Look Behaviour:

Texture UV Offset

If you've chosen the UV offset system, you'll want to continue the tutorial here.

Let’s address the smooth movement for our UV offsets. With how it’s set up now, when we stop looking at a target, the offset zeros out automatically. That’s because, in the OnStateExit function of the look behaviour, we’ve simply zeroed out the look offset. What we want to do instead is change the offset value back to zero over time so we have a smooth transition.

Unlike with the bone transform method, we aren't going to do this by smoothing out the floats. Instead, we'll use a coroutine.

We’ll only need one new parameter for this coroutine, which is a private float. The float will handle the duration of time it takes to move the eyes back to their default position. This is the code for the coroutine:

The coroutine works by getting the current position of our active look target, along with the current rotation of the eye rig. Then, using a set duration, it will move to our default position and rotation. At the same time, it will lerp the offset values from their current value zero.

This is simple to do because we call it in the exit function of the state, so we don’t have to worry about the offsets in the update function. However, to look at the target smoothly is a bit more complex and, because it isn’t as noticeable as snapping back to the default position, we can safely ignore it.