Location: Assets/Plugins/Animancer/Examples/10 Animation Jobs/03 Lean
Recommended After: Two Bone IK and Damping
Learning Outcomes: in this example you will learn:
How to dynamically lean a character by rotating their spine.
How to write a custom Animation Job.
This example demonstrates how you can use an Animation Job to dynamically rotate a character's spine to the side with a slider. The effect can be modified by changing the axis around which the rotation occurs:
- (0, 0, 0) leans over to the left/right
- (90, 0, 0) turns to the left/right
- (0, 90, 0) bends forward/back
This example is based on an implementation by ted-hou on GitHub.
Pro-Only Features are used in this example: Animation Jobs. Animancer Lite allows you to try out these features in the Unity Editor, but they're not available in runtime builds unless you purchase Animancer Pro.
Summary
- Jobs can't use reference types, but since
NativeArray
s behave like them, you can allocate aNativeArray
with a single element then use it to easily and efficiently communicate with a job. AnimancerUtilities.CreateNativeReference
allocates a single element array.- Don't forget that the array needs to be
Dispose
d when you're done with it (or added toanimancerComponent.Playable.Disposables
so Animancer will do it for you).
Overview
This example uses two scripts like the previous Animation Jobs examples, but this time we're writing the entire job from scratch so we can organise things to be a bit more reusable. Instead of using one script for a job struct and one for a component that initializes the job, this time we will be using three Types:
- The
SimpleLean
class inherits fromAnimancerJob
as a wrapper around the job to provide a clean API for other scripts to access the job. - The
Job
struct is nested inside theSimpleLean
class and contains the actualIAnimationJob
implementation. - The
SimpleLeanComponent
class is a regularMonoBehaviour
component which initializes aSimpleLean
for the purpose of this example.
This setup allows SimpleLean
and its Job
to be fairly reusable without tieing them to specific implementation details that only exist for the purpose of this demonstration. For example, SimpleLeanComponent
has a reference to the Transform
of the Axis object and sends its forward vector to the job every Update
in case you rotate it, but that's not likely to be necessary in a real project since you would probably just use a fixed axis for a specific purpose.
Simple Lean Component
As mentioned above, the SimpleLeanComponent
is a very simple script designed for this specific example scene to allow the real implementation of SimpleLean
to be potentially reused in other circumstances.
It has an array of Transform
s so you can use the Inspector to pick which bones you want it to control.
public sealed class SimpleLeanComponent : MonoBehaviour
{
[SerializeField] private AnimancerComponent _Animancer;
[SerializeField] private Transform[] _Bones;
For this example, it's set to control the Chest and Spine.
On startup, it asserts that at least one bone has been assigned, converts the Transform[]
into a NativeArray<TransformStreamHandle>
for the job to use, and creates a SimpleLean
with it.
private SimpleLean _Lean;
private void Awake()
{
Debug.Assert(_Bones.Length > 0, "No bones are assigned.", this);
var boneHandles = AnimancerUtilities.ConvertToTransformStreamHandles(_Bones, _Animancer.Animator);
_Lean = new SimpleLean(_Animancer.Playable, _Axis.forward, boneHandles);
}
It has a public Angle
property for a UI Slider to control the SimpleLean.Angle
.
public float Angle
{
get => _Lean.Angle;
set => _Lean.Angle = value;
}
And a reference to the Transform
of an object to define the SimpleLean.Axis
around which the rotation will be performed.
[SerializeField]
private Transform _Axis;
private void Update()
{
_Lean.Axis = _Axis.forward;
}
}
Initialization
SimpleLean
inherits from AnimancerJob
which is a simple base class that makes it a bit easier to implement wrappers for Animation Jobs in Animancer. The benefits of this inheritance will be noted when they are relevant.
public sealed class SimpleLean : AnimancerJob<SimpleLean.Job>, IDisposable
{
The constructor initializes the Job and passes it the required details as we have seen in the previous Animation Jobs examples.
public SimpleLean(AnimancerPlayable animancer, Vector3 axis, NativeArray<TransformStreamHandle> leanBones)
{
var animator = animancer.Component.Animator;
_Job = new Job
{
root = animator.BindStreamTransform(animator.transform),
bones = leanBones,
axis = axis,
angle = AnimancerUtilities.CreateNativeReference<float>(),// See the Angle section.
};
The CreatePlayable
method is inherited from AnimancerJob
. It calls AnimancerPlayable.InsertOutputJob
like in the earlier examples, but it also stores the returned AnimationScriptPlayable
for the Cleanup process which also requires this object to be registered in the AnimancerPlayable.Disposables
list.
CreatePlayable(animancer);
animancer.Disposables.Add(this);
}
Axis
Since the _Job
field (inherited from AnimancerJob
) stores the job created in the constructor, we can just get
the current axis
directly from it.
public Vector3 Axis
{
get => _Job.axis;
But since the job is a Value Type, simply modifying the values stored in the _Job
field will not actually modify the values of the job inside the AnimationScriptPlayable
in the PlayableGraph
so after we make any modifications we need to use AnimationScriptPlayable.SetJobData
to give it the new data. That method is not very efficient because it needs to copy the value of every field in the Job
struct even though we are only actually changing one of them. In a real project, this sort of effect is not likely to need to change the axis which the bones are rotated around so that overhead is not a big deal for this.
set
{
if (_Job.axis == value)
return;
_Job.axis = value;
_Playable.SetJobData(_Job);
}
}
Angle
Unlike the Axis, the whole point of this system is to be able to dynamically change the Angle
of the lean so we want this property to be as efficient as possible. Even though jobs are not allowed to use reference types, we can exploit the fact that NativeArray
s actually behave like reference types. By allocating a NativeArray
with a size of 1 we will be able to efficiently access its value both inside and outside the job.
Inside the SimpleLean.Job
struct, we declare the angle
field as a NativeArray
.
public struct Job : IAnimationJob
{
...
public NativeArray<float> angle;
...
}
Then when we initialize the Job
in the SimpleLean
constructor, we use AnimancerUtilities.CreateNativeReference
to allocate a single element array for it.
public SimpleLean(AnimancerPlayable animancer, Vector3 axis, NativeArray<TransformStreamHandle> leanBones)
{
...
_Job = new Job
{
...
angle = AnimancerUtilities.CreateNativeReference<float>(),
};
...
}
And that allows the Angle
property to set the value without needing to use the AnimationScriptPlayable.SetJobData
method to copy the entire Job
.
public float Angle
{
get => _Job.angle[0];
set => _Job.angle[0] = value;
}
Cleanup
As explained in the Damping example, when you're done with a NativeArray
you must call Dispose
on it to avoid leaking its memory. We could add the arrays directly to the AnimancerPlayable.Disposables
list, but it might be useful to be able to remove the SimpleLean
system separately from the destruction of the rest of the character and we don't want to let it cause an error if it tries to Dispose
an array we already disposed.
We start by implementing the IDisposable
interface with our own Dispose
method.
public sealed class SimpleLean : AnimancerJob<SimpleLean.Job>, IDisposable
{
public SimpleLean(...)
{
...
animancer.Disposables.Add(this);
}
public void Dispose()
{
if (_Job.angle.IsCreated)
_Job.angle.Dispose();
if (_Job.bones.IsCreated)
_Job.bones.Dispose();
}
That would take care of the automatic disposal, but if another script calls that Dispose
method the Job
would stop working without actually being removed from the PlayableGraph
so it would still be executing and causing errors. So instead we can instead use an Explicit Interface Implementation and make the Dispose
method private.
void IDisposable.Dispose() => Dispose();
private void Dispose()
{
if (_Job.angle.IsCreated)
_Job.angle.Dispose();
if (_Job.bones.IsCreated)
_Job.bones.Dispose();
}
Then we override the AnimancerJob.Destroy
method to call the Dispose
method as well.
public override void Destroy()
{
Dispose();
base.Destroy();
}
So now the AnimancerPlayable
will call our Dispose
method when the whole graph is being destroyed and other scripts can call that Destroy
method which will both Dispose
the job and remove it from the graph.
Job
The actual Job
implementation is fairly simple. We already initialized the fields in the SimpleLean
constructor.
public struct Job : IAnimationJob
{
public TransformStreamHandle root;
public NativeArray<TransformStreamHandle> bones;
public Vector3 axis;
public NativeArray<float> angle;
The IAnimationJob
interface contains two methods which are both called every time the PlayableGraph
is updated (generally once per frame). We don't care about ProcessRootMotion
here and want to do all our math in ProcessAnimation
.
public void ProcessRootMotion(AnimationStream stream) { }
public void ProcessAnimation(AnimationStream stream)
{
We start by calculating the angle
that each bone will be rotated by dividing the specified total Angle by the number of bones. For this example we are controlling the character's Chest and Spine bones, so each will receive half of the total angle.
var angle = this.angle[0] / bones.Length;
Since the Axis is easier for other scripts to specify in local space, so we want to rotate that axis
according to the character's root
.
var worldAxis = root.GetRotation(stream) * axis;
Then we can use that angle
and worldAxis
to calculate the rotation Quaternion
we want to add to each bone.
var offset = Quaternion.AngleAxis(angle, worldAxis);
And finally, we just add that offset
to each of the bones
.
for (int i = bones.Length - 1; i >= 0; i--)
{
var bone = bones[i];
bone.SetRotation(stream, offset * bone.GetRotation(stream));
}
}
}
}
Note that "adding" rotations actually uses the multiply operator (*
) due to the way Quaternion
math works.
Conclusion
This technique could be used to make a character lean to the side while running around a corner, or turn and bend according to the direction they want to look.
It could also be adapted to widen the character's legs and arms to accomodate skinny or bulky characters without needing separate animations for each body type.