KybernetikGames / animancer

Documentation for the Animancer Unity Plugin.
66 stars 8 forks source link

A more abstract way of controlling Mixer parameters #257

Closed KybernetikGames closed 1 month ago

KybernetikGames commented 1 year ago

Use Case

In simple cases, being able to directly control a strongly typed Parameter property on a Mixer is great. For example, you could make a LocomotionState script with a LinearMixerTransition to blend between Idle, Walk, and Run animations and it can simply set transition.State.Parameter = desiredSpeed;. It's easy to set up, easy to debug, and it just works.

But direct control means direct dependency, which is bad for flexibility. If you wanted a character in the above example to have a single locomotion animation or a 2D mixer, you would need to make a whole new state script to support that. Even though Animancer allows you to use Serialized References for Transition fields, if your script doesn't know that the transition is a mixer and what its parameters actually mean, then it won't be able to set them appropriately.

This lack of flexibility is particularly bad when you're trying to use Nested Mixers. Manually creating them in code isn't too bad, but hard coding mixer setups is generally a bad idea because it makes it hard for artists to work with. Defining mixers using Transitions is usually a much better workflow, but as you can see on that page the script still ends up needing a very hard coded understanding of the mixer structure in order to be able to control their parameters. That means the script will only work with a specific mixer structure it expects, but there is no indication of the expected structure when you look at it in the Inspector, leading to wasted development time on trial and error.

Alternatives

Normally I'd put alternatives at the end, but the rest of this post got super long, so here's a simple extension method which would let you set the parameter of any child mixers. It would only work as long as all child mixers of a given type share the same meaning for their parameters and you would still be hard coding significant assumptions into your script, but it would at least simplify your script a bit.

public static class MixerExtensions
{
    public static void SetChildParameters<TParameter>(this ManualMixerState parent, TParameter parameter)
    {
        var childCount = parent.ChildCount;
        for (int i = 0; i < childCount; i++)
            if (parent.GetChild(i) is MixerState<TParameter> mixer)
                mixer.Parameter = parameter;
    }
}

Solution

Something like the parameter system in Animator Controllers.

That's the general idea, but there are a few more specific considerations I haven't yet settled on.

Dynamic Definitions

Setting up a Blend Tree with a parameter is a 3 step process:

  1. Add a parameter to the Animator Controller.
  2. Go to the Blend Tree and select that parameter.
  3. Call animator.SetFloat("Parameter Name", value); in a script.

In line with the way Animancer lets you play animations without defining them beforehand, this system could cut that down to 2 steps:

  1. Go to the Mixer and select a key.
  2. Call animancer.SetParameter(key, value); in a script.

A parameter would simply get created for any given key the first time anything tries to get or set it.

It's not all upside though. See the Networking section below.

What's a Key?

Animator Controllers use strings as their keys, but Magic Strings are bad. They are actually quite good for simple situations, but scale very poorly as complexity increases.

Enums would take a lot of effort to implement in a way that lets users specify their own values and would still be unsafe because doing things like rearranging the enum values in code wouldn't sync with any serialized fields you've already set up.

Scriptable Objects might be the best option. A class which inherits from ScriptableObject could be referenced by the mixer transition in its key field and in your control script to form a reasonably safe and reliable connection. It wouldn't even necessarily need to be limited to that class, the field type could be the base UnityEngine.Object which would allow you to assign any ScriptableObject, MonoBehaviour, GameObject, or anything else that inherits from it.

The main downside of Scriptable Objects is the required verbosity. As mentioned above, strings are nice and simple:

[SerializeField] private AnimancerComponent _Animancer;

private void Update()
{
    _Animancer.SetParameter("MoveSpeed", xxx);
}

But to do that with a Scriptable Object, you need a bit more code and you also need to go and assign the reference in the Inspector:

[SerializeField] private AnimancerComponent _Animancer;
[SerializeField] private ParameterKey _MoveSpeedParameter;

private void Update()
{
    _Animancer.SetParameter(_MoveSpeedParameter, xxx);
}

The result is more robust, but it takes more effort to set up and also to learn how to use it in the first place.

You might be wondering:

Why don't we have both?

And the answer is: we could at runtime, but not in the Inspector.

string and UnityEngine.Object both inherit from object (the alias of System.Object) so using it as the key type would allow you to use both (or literally anything else because all types in C# inherit from object). That's exactly how Animancer's State Keys work.

But Unity's serialization system doesn't support inheritance. Actually it does ... in 2 different ways ... neither of which would help us here:

  1. If the field is UnityEngine.Object or something inheriting from it, then it will let you assign anything inheriting from the field type. But string doesn't, so that's a no-go.
  2. If you give the field a [SerializeReference] attribute then it will support inheritance and you can assign anything to it which inherits from the field type ... except for things which inherit from UnityEngine.Object.

A potential workaround is to use a [SerializeReference] object field to hold either a string or a custom serializable class which can contain a reference to the UnityEngine.Object you want, but that adds a lot of complexity to the implementation and usage of the system for the main use case we want (Scriptable Objects) just to allow a lazy use case that doesn't scale well (strings).

Another potential workaround is to just have two fields and use whichever is present at runtime, but that's ugly. Wasted disk space, wasted memory, and wasted load time for everyone regardless of which option you choose. It wouldn't be the end of the world, but I'm not a fan.

Networking

If the parameter system was serializable, it could be very helpful for synchronizing things over a network. Not just for mixers, you could give it whatever important values you want and get them back on the other side.

The main downside of supporting networkability is that it necessitates significant restrictions to the way the system can be used. This may actually be the primary reason for many of the restrictions in Animator Controllers which led me to create Animancer in the first place.

The most significant restriction is that if we allow Dynamic Definitions of parameters as described above, then when sending the parameter data we can't just send the values and assume the other side will know which value is for which parameter. So we would need to include a parameter identifier with each value. "Identifier" is basically synonymous with "Key", which brings us back to the question of What's a Key because now we need something that works with whatever serialization system the user's networking system has and it's very important to minimize the amount of data we require (in addition to still needing to work with Unity's serialization system).

One solution would be to give up on the idea of letting you use anything you want as a key. Require all keys to be a specific ScriptableObject type with a unique ID int that can't be changed at runtime.

But I think a better solution would be to give up on Dynamic Definitions if you want networking. It should be possible to still allow them by default, but have a way of specifying all the parameters a character has upfront (like Animator Controllers do) and require you to use that if you want to serialize the parameters.

Feedback

I haven't started work on this idea and probably won't any time soon, but I'd greatly appreciate any suggestions. Even if you don't have much to say, the more interest it gets, the more likely I am to work on it.

FlorianBarnier commented 1 year ago

Great writeup! I'm not a fan of setting parameters using the alternative you mentioned (which is pretty much what I currently have), so I'd definitely like this feature.

In my opinion the approach using a Scriptable Object as a key seems to be the best. It's also not much more verbose that using strings, since if you wanted to do things properly with a string you'd probably serialize it too. I understand why that could make it a bit harder for newcomers to learn though, however if the current way of setting mixer.Parameter still exists then I feel like it wouldn't be that much of a problem.

As I mentioned in the Unity Forums thread, it would be great if the same idea was to be applied to events too. That would make working with lots of events on nested mixers much easier, as well as allowing multiple events to share the same key.

KybernetikGames commented 1 year ago

Thanks for the feedback.

it would be great if the same idea was to be applied to events too

Would you mind posting a new Feature Request and giving a general overview of how you imagine such a system could work? Not the low level implementation details, just a high level conceptual workflow you'd like to achieve. A description of your use case would also be helpful.

FlorianBarnier commented 1 year ago

Sure, will do!

KybernetikGames commented 7 months ago

I've now implemented this feature in the upcoming Animancer v8.0.

Here's a summary of how it works:

Mixers are currently the only thing that uses parameters, but I'm also looking at implementing Transition Sets which might be able to benefit from having transition conditions based on parameters.

KybernetikGames commented 3 months ago

Animancer v8.0 is now available for Alpha Testing and includes this feature..

KybernetikGames commented 1 month ago

Animancer v8.0 is now fully released.