๐Ÿ“‘Animator Profiles

In this section you will learn about new layer system.

Legacy system

In previous versions of the FPS Animation Framework, we had a fixed amount of animation layers, specified directly in the FPSAnimator component:

The problem with this design is obvious - scalability. What if we want to add an unarmed state? Or maybe swimming? What if we want a unique animation feature for this specific weapon?

As you can see, these questions arise a very important question of how far we can extend the system, and how fast we can implement custom animation features in the future.

The new Scriptable Animation System completely eliminates this problem by introducing a dynamic linking system for animation features.

Dynamic Linking

This concept is incredibly simple - we want to dynamically link animation features. To achieve this, the new system uses Animator Profiles:

An Animator Profile is a Scriptable Object, which contains Animator Layer Settings. You can create different Animator Profiles not just for weapons or items, but also gameplay actions.

Example: imagine you want to add a ladder climbing feature. Obviously we won't need Ads or Recoil animation layers. So you can create a new profile, which will include the features you want for this specific gameplay scenario.

And here's a code example:

FPSController.cs
private void EquipWeapon()
{
    ...
    _fpsAnimator.LinkAnimatorProfile(gun.gameObject);
    ...
}

Here we try to dynamically link an Animator Profile from the currently equipped weapon. For items and weapons there's a special component called FPS Animator Entity:

As you can see, this component just contains an Animator Profile for this weapon or item.

Tip: the Scriptable Animation System no longer depends on the weapons, instead it relies on Animator Profiles to execute animation features in runtime!

Now let's see how new Animator Layers work!

Animator Layers

Animator Layer now consists of 2 classes:

  • Animator Layer Settings: contains the data only, can instantiate a Layer State.

  • Animator Layer State: executes the logic, holds a reference to the Layer Settings.

The Animator Layer Settings is a Scriptable Object, which not only contains the data, but can also instatiate a Layer State object. As we already know, we operate in Animator Profiles, which include the Animator Layer Settings.

When we update an Animator Profile, we need to instatiate new Layer States, so the system iterates over all Layer Settings and calls the common CreateState() method:

FPSBoneController.cs
foreach (var setting in _activeProfile.settings)
{
    var state = setting.CreateState();
    ...
    _layerStates.Add(state);
}

The reason why we need Animator Layer States is because Scriptable Objects do not actually have a state, and we just can't use them to run any logic or store runtime data.

To sum up everything above - the greates difference with the previous version is this:

  1. We store the data in the Scriptable Object, not the Animator Layer itself.

  2. We execute the logic in the Layer State exclusively.

This approach allows us to modify the animation features' behaviour in runtime, without restarting the editor session. You can also swap Animator Layers and Animator Profiles on the fly, which will save you a lot of time.

In the next section we will learn more about the new Input System, and how we can use it to establish communication between different entities in the framework.

Cache and Blending

Animator Profiles have a very useful feature which allows a smooth transition, when linking a new Animator Profile.

Caching and Blending occurs when a new Animator Profile is linked, and the order of operations is this:

Caching:

  1. Cache all the bone data from the supported layers - layers that do not exist in the next profile, and have Linked Dynamically set to false.

  2. If layer has Linked Dynamically is set to true, call the OnLayerLinked method and use Animator Layer from the next profile as a parameter.

  3. Dispose those layers which were cached.

Blending:

  1. Cache all the bones that are affected by caching.

  2. Apply current animation features as usual.

  3. Override pose with cached data.

Animator Layers support caching and blending only if they have these methods implemented:

YourAnimatorLayerState.cs
public override void RegisterBones(ref HashSet<int> registeredBones)
{
    // Add indexes of ALL bones to the Hash Set.
}

public override void CachePoses(ref List<KPose> cachedPoses)
{
    // Add KPoses for all cached bones.
}
Why is it not handled automatically?

Performance. In order to keep a stable framerate, caching the whole pose can be expensive. By specifying what bones we want to affect by blending, we effectively save us from unnecessary computations.

Let's take a look at the implementation of the Attach Hand Layer:

AttachHandLayerState.cs
public override void RegisterBones(ref HashSet<int> registeredBones)
{
    // Register the whole left arm, as it is crucial for IK.
    
    registeredBones.Add(_settings.handBone.index - 2);
    registeredBones.Add(_settings.handBone.index - 1);
    registeredBones.Add(_settings.handBone.index);
}

public override void CachePoses(ref List<KPose> cachedPoses)
{
    // Cache KPoses for all left arm bones.

    Transform mid = _handBone.parent;
    Transform root = mid.parent;

    cachedPoses.Add(new KPose()
    {
        element = new KRigElement(_settings.handBone.index - 2, ""),
        modifyMode = EModifyMode.Replace,
        pose = new KTransform(_owner.transform).GetRelativeTransform(new KTransform(root), false),
        space = ESpaceType.ComponentSpace
    });

    cachedPoses.Add(new KPose()
    {
        element = new KRigElement(_settings.handBone.index - 1, ""),
        modifyMode = EModifyMode.Replace,
        pose = new KTransform(_owner.transform).GetRelativeTransform(new KTransform(mid), false),
        space = ESpaceType.ComponentSpace
    });

    cachedPoses.Add(new KPose()
    {
        element = _settings.handBone,
        modifyMode = EModifyMode.Replace,
        pose = new KTransform(_owner.transform).GetRelativeTransform(new KTransform(_handBone), false),
        space = ESpaceType.ComponentSpace
    });
}

Tip: another great benefit of this sytem is that it cache won't be overriding active animation pose. This means, that it will be applied as if the layer was still active. This will ensure that the base pose is preserved, and the cache is properly applied on top of everything else.

Last updated