10-04 Job States

Location: Samples/10 Animation Jobs/04 Job States

Recommended After: Hit Impacts

Learning Outcomes: in this sample you will learn:

How to play a procedural animation in place of an AnimationClip.

Summary

This sample demonstrates how you can use an Animation Job State to implement a simple procedural animation which waves the character's limbs around.

In a real application, you could do things like loading animation data from a file imported at runtime or implement fully scripted animations without needing AnimationClips, but those ideas are well beyond the scope of this sample.

Overview

The structure of this sample is very simple: the PlayWavyBones component creates a WavyBonesAnimationJob, wraps it in an AnimationJobState, and plays it on the AnimancerComponent.

Fields

Code Inspector
public class PlayWavyBones : MonoBehaviour
{
    [SerializeField]
    private AnimancerComponent _Animancer;

    [SerializeField]
    private Transform[] _Bones;

    [SerializeField]
    private WavyBonesSettings _Settings;
[Serializable]
public struct WavyBonesSettings
{
    public Vector3 rootPosition;
    public Vector3 magnitudes;
    public Vector3 speeds;
}

The script has a Transform[] to assign all the bones that will be controlled by the job and a WavyBonesSettings to hold other details for the job to use.

The bones are separate from the settings because the job can't use Transforms directly, it needs to get TransformStreamHandles for them.

Initialization

The core of this system is the AnimationJobState<T> which is a type of AnimancerState where T is an IAnimancerStateJob (not a regular Unity IAnimationJob).

We want to be able to update the settings while the job is running, so we need a field to hold the state once we create it.

private AnimationJobState<WavyBonesAnimationJob> _JobState;

On startup, we create the Job Struct (WavyBonesAnimationJob) then give it to the constructor of the AnimationJobState.

protected virtual void OnEnable()
{
    if (_JobState == null)
    {
        WavyBonesAnimationJob job = new WavyBonesAnimationJob(
            _Animancer.Animator, 
            _Bones,
            _Settings);

        _JobState = new AnimationJobState<WavyBonesAnimationJob>(job);
    }

Then we can play the state like any other AnimancerState.

    _Animancer.Play(_JobState);
}

That's all we need for this sample, but we could use any of Animancer's other features like specifying a fade duration, setting its Time, adjusting its Speed, or adding Animancer Events to it.

Updating Settings

In order to allow you to tweak the settings in the Inspector while the job is running, we can use OnValidate to give the new settings to the job whenever they are changed. Since the job is a struct, we need to copy it out of the state, modify it, then copy it back in again.

protected virtual void OnValidate()
{
    if (!_JobState.IsValid())
        return;

    WavyBonesAnimationJob job = _JobState.Job;
    job.settings = _Settings;
    _JobState.Job = job;
}

If you need to modify the job like that frequently, the Angle section in the Hit Impacts sample explains how that can be done more efficiently.

Job Struct

This job implements IAnimancerStateJob rather than the regular Unity IAnimationJob so that it can be used in an AnimationJobState.

public struct WavyBonesAnimationJob :
    IAnimancerStateJob
{

It holds a TransformStreamHandle for each of the bones it will be controlling (and the root which will be explained below) as well as the WavyBonesSettings which we set up in the Inspector.

    public TransformStreamHandle rootTransform;
    public NativeArray<TransformStreamHandle> transforms;
    public WavyBonesSettings settings;

IAnimancerStateJob requires Length and IsLooping properties which don't matter much in this sample because the procedural animation isn't trying to match any specific loop timing, so we just say it has a 1 second Length and is looping for the Inspector to display something reasonable if the character is selected.

    public readonly float Length
        => 1;

    public readonly bool IsLooping
        => true;

In the constructor, we use animator.BindStreamTransform to get TransformStreamHandles for each of the Transforms like in the Hit Impacts sample.

    public WavyBonesAnimationJob(
        Animator animator,
        Transform[] transforms,
        WavyBonesSettings settings)
    {
        this.settings = settings;

        this.transforms = new NativeArray<TransformStreamHandle>(
            transforms.Length,
            Allocator.Persistent,
            NativeArrayOptions.UninitializedMemory);

        for (int i = 0; i < transforms.Length; i++)
        {
            this.transforms[i] = animator.BindStreamTransform(transforms[i]);
        }

        rootTransform = animator.BindStreamTransform(animator.transform.GetChild(0));
    }

IAnimancerStateJob also inherits from IDisposable and calls Dispose when the state is destroyed, so we need to use that to dispose of the NativeArray.

    readonly void IDisposable.Dispose()
    {
        transforms.Dispose();
    }

We aren't using root motion in this sample, so ProcessRootMotion is left empty.

    readonly void IAnimancerStateJob.ProcessRootMotion(AnimationStream stream, double time)
    {
    }

The core of the job is in ProcessAnimation where we implement the procedural animation.

We first set the root position to the value from the settings so that the character's hip bone doesn't default to (0, 0, 0) which would put the character halfway in the ground.

    public readonly void ProcessAnimation(AnimationStream stream, double time)
    {
        rootTransform.SetLocalPosition(stream, settings.rootPosition);

Then we use Math.Sin to rotate the bones around on each axis using the time passed in from the state and the magnitudes and speeds from the settings.

        for (int i = 0; i < transforms.Length; i++)
        {
            TransformStreamHandle transform = transforms[i];

            Vector3 euler = new(
                (float)(Math.Sin(time * settings.speeds.x) * settings.magnitudes.x),
                (float)(Math.Sin(time * settings.speeds.y) * settings.magnitudes.y),
                (float)(Math.Sin(time * settings.speeds.z) * settings.magnitudes.z));

            Quaternion rotation = Quaternion.Euler(euler);

            transform.SetLocalRotation(stream, rotation);
        }
    }
}

And that's it, this gives us a character waving their limbs around without the use of any AnimationClips.

Humanoid Rig Defaults

You may have noticed that the character's knees are bent rather than standing straight even though we didn't assign any of the leg bones to the _Bones array. That's because the character is using a Humanoid rig which causes a bone's pose to be defined by the Avatar's muscle limits if there is no animation data affecting it.