Closed FlorianBarnier closed 1 month ago
Well there goes my whole morning :)
Looks like there's a few different feature suggestions here so I've split them out to separate issues:
That leaves the main idea as:
animancer.SetCallback(key, method)
Regardless of whether the keys are strings or Scriptable Objects, the general idea would be to give the AnimancerComponent
(or really its root AnimancerPlayable
) a Dictionary<key, Action>
so that:
For example:
animancer.Events.SetCallback("Footstep", DoStuff);
then an artist can add a Footstep event to the Walk and Run animations inside a mixer, or in any level of a nested mixer, or a BoredIdle animation where the character kicks the ground, or a special ability animation that involves movement. That can all be done by an artist without the scripts ever needing to know where the Footstep event is actually being used.For debugging purposes, I'd probably want to give a log warning if an event can't find a callback but if you register a callback that happens to not be used I'd only show an indication in the Inspector wherever I end up listing all registered callbacks.
The main aspect I'm not certain about is the best way to determine when to connect an event to its registered callback:
Every named event automatically looks in the dictionary for a callback.
LookInTheDictionaryToGetTheRealCallbackAndInvokeIt
.
Add an "Auto Bind" toggle to each event in the Inspector.
If we switch to Scriptable Object keys instead of strings, the Scriptable Object could potentially contain the bool indicating whether its events should auto-bind, but that's probably still too fiddly for a potential tiny performance gain.
You need to call a bind method on every transition which will go through all events (recursively) to find anything with a name but no callback and set it to LookInTheDictionaryToGetTheRealCallbackAndInvokeIt
.
Having written all that out, I'm leaning towards automatic bindings.
It becomes even worse when trying to work with nested Mixers, where you now have to go through n levels of nested Mixers to find all the correct events. For example, if I wanted to have a character with a set of movement animations for multiple speeds (Idle, Walk, Run, Sprint) and multiple ground slopes (up, down, left, right), I would need a Linear Mixer (speed), containing multiple 2D Mixers (slope), each containing a set of ClipTransitionAssets. You can see how that quickly becomes quite a mess!
For events that are reused a lot and can happen multiple times in the animation like FootStepSound and Weapon vfx sound, I personally embedded the event using Unity's AnimationEvent and it works quite well for me. I wonder what's your use case is here ?
That's a very good point. Implementing this sort of system would take time away from other features I could be working on and most use cases can probably already be covered by Animation Events (even if they're a bit annoying to use).
I'm currently looking at possible features to work on for the next major version of Animancer and this idea has some merit, but I've realised a limitation that might actually make it worse than Unity's Animation Events: the lack of parameters.
In the Footstep Events Example:
int
parameter to indicate whether it's the left or right foot.UnityEvents
to call methods with a parameter to specify the AudioSource
of each foot in its event.But if you want to call animancer.SetCallback(name, method)
then the method
can't have parameters and there's nowhere to store them with the events anyway. You would need to have a different name
for each different parameter value which would be very inconvenient, especially with other parameter types. You'd be writing the real Method(value)
as well as Method0()
and Method1()
to call it with the right parameters, then you'd also need to do both animancer.SetCallback("Footstep0", Method0)
and animancer.SetCallback("Footstep1", Method1)
.
Unfortunately, I haven't been able to come up with any viable solutions to this issue and it likely won't be worth the effort of implementing if the result is going to be missing such a crucial part.
I've now implemented this feature in the upcoming Animancer v8.0.
Unfortunately, this is a breaking change. Any event callbacks set up in an older version of Animancer will be lost. Event names will also be lost due to changing them from raw strings to ScriptableObjects.
Here's a summary of how it works:
animancerComponent.Events
is a dictionary mapping event names to callbacks.[SerializeReference] IInvokable
instead of [SerializeField] UnityEvent
. This is why it's a breaking change.
IInvokable
in the Inspector thanks to the Polymorphic Drawer.Animancer.UnityEvent
simply inherits from UnityEvent
and implements IInvokable
for this purpose, allowing you to achieve the same thing as the current system.Animancer.UltEvent
is the same for UltEvents, which means you will be able to use them automatically by just having the UltEvents package in your project instead of needing to set up an assembly reference and compilation symbol like you do now. It also means using UltEvents for some things won't break all your existing UnityEvents.AnimancerEventParameter???
types (Object
, bool
, int
, etc.) which store a value in AnimancerEvent.CurrentParameter
during the event so it can be accessed by callbacks registered in code (in the event sequence or in the animancerComponent.Events
dictionary).[Serializable]
class that inherits from AnimancerEventParameter<T>
.As an example, the Footstep Events example looks like this in the current system:
private void OnEnable()
{
_Animancer.Play(_Walk);
}
public void PlaySound(AudioSource source) ...
UnityEvent
callback.PlaySound
will be used.In the new system it could still be set up like that, or it could look like this:
private void OnEnable()
{
_Animancer.Events.AddNew<AudioSource>("Footstep", PlaySound);
_Animancer.Play(_Walk);
}
public void PlaySound(AudioSource source) ...
AudioSource
as a parameter.Obviously there will be advantages and disadvantages to each approach, but I can see a lot of potential in the new system and it will be necessary if I manage to implement Transition Sets because the caller wouldn't be able to safely initialise transition events in the same way when those transitions aren't owned by the caller.
The system isn't fully implemented yet, but the general proof of concept is done so I can confirm it will be in the upcoming Animancer v8.0.
Really looking forward to these changes, thanks for this! Will the new AnimancerEventParameters allow us to avoid the closure allocations when binding to event callbacks? I find that we have to do something like the following a lot in our code base and because the callback doesn't pass back any context about the event, we always have to pay the closure allocation cost.
events.SetCallback(
i,
() => PlayFootstep(eventName)
);
Another feature I would love to have is an Action<NamedKey> AnyEvent
callback. I have a few systems that bind to every event in the animation requiring me to loop through each event in the sequence. Would be nice to be able to bind to a single callback that gets triggered when any event is triggered that passes along the NamedKey. It would be much easier to work with, and in most cases prevent the closure allocations since you would be providing the relevant context as to which event was triggered.
AnimancerEvent.CurrentEvent
and CurrentState
are static properties which give you access to the context of an event. The current system doesn't include the event name in that (because it wasn't used for anything at invocation time) so you would need to use AnimancerEvent.CurrentState.Events.IndexOf(AnimancerEvent.CurrentEvent)
then get the name with that index, but the new system does include the event name in the context so you'll be able to access it directly, without any involvement from parameters.
If you wanted a different value other than the name then you could use a parameter like the AudioSource
in the example I gave in my previous post.
I don't think an AnyEvent
callback would be useful in enough cases to be worth the (admittedly small) performance cost for everyone. But it should be pretty easy for you to add yourself, just add the event in AnimancerPlayable
and trigger it in AnimancerEvent.Invoke
via CurrentState.Root.AnyEvent
. I wonder if there's a way I could make modifications like that easier to do without needing you to modify Animancer's scripts directly.
Use Case
Working with animations that need lots of events can quickly become quite heavy. You have to search through the animation's events and compare their strings to subscribe to them, which runs into a lot of the same problems as the ones described in #257: it's not very performance-friendly and not very reliable/scalable.
Another issue with the current system is that two events cannot have the same name on an animation. What if you use events to trigger sounds, and have an animation that needs to play the same sound 3 times? Having
MyCoolSound1
,MyCoolSound2
andMyCoolSound3
isn't exactly the cleanest.It becomes even worse when trying to work with nested Mixers, where you now have to go through n levels of nested Mixers to find all the correct events. For example, if I wanted to have a character with a set of movement animations for multiple speeds (Idle, Walk, Run, Sprint) and multiple ground slopes (up, down, left, right), I would need a Linear Mixer (speed), containing multiple 2D Mixers (slope), each containing a set of ClipTransitionAssets. You can see how that quickly becomes quite a mess!
Alternatives
For now, the alternative is to go through all the animations or sub-mixers contained on the mixer you just started, go through all of their respective events and compare their names to get the ones you need.
Solution
Similarly to what's described in the Solution part of #257, an Animancer event could take a ScriptableObject as its key instead of a string, which means:
animancer.SetCallback(key SO, method)
, which would also make working with nested Mixers easier.animation.Events.SetCallback
, you would now be comparing references to ScriptableObjects instead of magic strings, which is both lighter and less error prone for the artists.string.Contains("MyCoolSound")
on each event name, you could just doanimation.Events.SetCallback(key SO, method)
.