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 AnimationClip
s, 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 |
---|---|
The script has a The bones are separate from the settings because the job can't use |
![]() |
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 TransformStreamHandle
s for each of the Transform
s 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 AnimationClip
s.

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.