PlayableGraph / AnimationScriptPlayable questions

Hello guys.

I have a few questions about PlayableGraph and other things.

1) Is there any good example, how to correctly implement custom MixerPlayable, that can mix other mixers?
Implementation in current sample mixers (Default Playables) is very primitive, and incorrect. By that, i mean, that Mixer evaluates inputs and applies result to object by himself. This leads to inability to chain multiple mixers together, for more complex mixing behaviors.
In my case, i want to create playable that controls Inverse Kinematics data. I have such hierarchy:

  • AnimationIkPlayable - Holds information about IK configuration (weights, positions, rotations, etc)
  • AnimationIkMixerPlayable - Extends AnimationIkPlayable, mixes all inputs and saves result in own instance (since it is derived from AnmationIkPlayable), so other mixers can read data from it
  • AnimationIkOutputPlayable - Gets IK information from attached input, and applies it to animation stream

And it almost works, but graph update order is not what i expecting. For example, if i attach them like this: Mixer1 → Mixer2 → Node they will be updated in this exact order, which induces X frames delay for each action (Mixer1 reads data from Mixer2, but Mixer2 is not yet updated and contains data from previous frame). I expect that each input for each mixer is updated before mixer update (thus creating reverse update order Node → Mixer1 → Mixer2). AnimationJob seems to have such behavior, so i wondering, if normal playables can be configured for it too
And that leaves me thinking - am i doing this all correctly but just missing a few flags here and there, or my implementation is completely wrong.

2) Can AnimationScriptPlayable additionaly have custom PlayableBehavior?
Currently, you can modify animation stream in AnimationJob, but you cannot react to Playable events (like PrepareFrame) and should update AnimationJob somewhere in Update() method. In my case, i’d wanted to evaluate inputs, and then update AnimationJob with new parameters. My current setup is awful, but it works. Basically, AnimationIkOutputPlayable references AnimationJob and when output updates, it sets new parameters for AnimationJob. Its very unnatural, since graph output changes one of the graph nodes. Is there are better solution for this?

Here is my messy and experimental code, if someone interested.
https://github.com/3DI70R/CharacterAnimator

1 Like

Bump
Here is test code that demonstrates issue that i have in first question.

using UnityEngine;
using UnityEngine.Playables;

public class GraphTest : MonoBehaviour
{
    public class TestBehavior : PlayableBehaviour
    {
        public string name;
        public int value;

        public override void ProcessFrame(Playable playable, FrameData info, 
            object playerData)
        {
            base.ProcessFrame(playable, info, playerData);

            if (playable.GetInputCount() > 0)
            {
                // just copy value from input for demonstration purposes
                value = ((ScriptPlayable<TestBehavior>) playable.GetInput(0))
                    .GetBehaviour().value;
            }
        }
    }
    
    private PlayableGraph graph;
    private ScriptPlayable<TestBehavior> firstPlayable;
    private ScriptPlayable<TestBehavior> lastPlayable;
    private int counter;
    
    private void Start()
    {
        graph = PlayableGraph.Create("Test Graph");
        var o = ScriptPlayableOutput.Create(graph, "output");
        
        firstPlayable = ScriptPlayable<TestBehavior>.Create(graph, 
            new TestBehavior { name = "Node 1" });
        var p2 = ScriptPlayable<TestBehavior>.Create(graph,
            new TestBehavior { name = "Node 2" });
        var p3 = ScriptPlayable<TestBehavior>.Create(graph, 
            new TestBehavior { name = "Node 3" });
        var p4 = ScriptPlayable<TestBehavior>.Create(graph, 
            new TestBehavior { name = "Node 4" });
        var p5 = ScriptPlayable<TestBehavior>.Create(graph, 
            new TestBehavior { name = "Node 5" });
        var p6 = ScriptPlayable<TestBehavior>.Create(graph, 
            new TestBehavior { name = "Node 6" });
        var p7 = ScriptPlayable<TestBehavior>.Create(graph, 
            new TestBehavior { name = "Node 7" });
        lastPlayable = ScriptPlayable<TestBehavior>.Create(graph, 
            new TestBehavior { name = "Source" });

        firstPlayable.AddInput(p2, 0, 1f);
        p2.AddInput(p3, 0, 1f);
        p3.AddInput(p4, 0, 1f);
        p4.AddInput(p5, 0, 1f);
        p5.AddInput(p6, 0, 1f);
        p6.AddInput(p7, 0, 1f);
        p7.AddInput(lastPlayable, 0, 1f);
        o.SetSourcePlayable(firstPlayable);
        
        graph.Play();
    }

    private void Update()
    {
        lastPlayable.GetBehaviour().value = counter;
        counter++;
    }

    private void LateUpdate()
    {
        Debug.Log("Last playable: " + lastPlayable.GetBehaviour().value + ", " +
                  "First Playable: " + firstPlayable.GetBehaviour().value);
    }

    private void OnDestroy()
    {
        graph.Destroy();
    }
}

I’ve also modified Playable Graph Visualizer to display actual values as node titles, and here is gif with code above running.

3853105--326074--NodeDelay.gif

1 Like

Interesting… not a full answer to all your questions, but I haven’t encountered issues with AnimationMixerPlayables so far under my usage.

However, I HAVE confirmed that you can connect a custom ScriptPlayable between an AnimationMixerPlayable and an AnimationScriptPlayable, and everything will work.
You can also make your ScriptPlayable an input to your AnimationScriptPlayable, and everything will work as well.

By “everything will work”, I’ve tested that it:

  1. Still writes out the same animation data
  2. Still mixes properly with other animation playables (tested with an AnimatorControllerPlayable as input 0 into the mixer, and AnimationScriptPlayable as input 1 into the mixer)
  3. Your custom PlayableBehaviour class (T above) will have all its callbacks get called.

This does NOT mean that you should start trying to write data from your ScriptPlayable over your animation data, as it probably won’t mix, and won’t be handled in Unity’s internal multi-threaded animation update,
BUT it is helpful for getting the playable callbacks AND writing animation data at the same time!

This is a screenshot of the PlayableGraph I was testing with. I put Debug.Log(…) calls in my ExamplePlayable (ScriptPlayable):

2 Likes

Here is some of my experience using Playable to develop new animation controllers. I hope it can be helpful to you!

All Playables in the PlayableGraph have two methods:

  • PrepareFrame
  • ProcessFrame

On each frame of the game, the PlayableGraph will be traversed twice:

  • The first traversal starts with the root node (the node connected to PlayableOutput) and traverses in a pre-order manner, calling the PrepareFrame method.
  • The second traversal traverses in a post-order manner and calls the ProcessFrame method.

It is okay to connect ScriptPlayable (as well as any other Playable) to the animation Playable tree. Therefore, you can input AnimationScriptPlayable into a custom ScriptPlayable node and complete the state update logic (such as updating input weights and replacing inputs) in the PrepareFrame method. PlayableGraph ensures that on each frame, the PrepareFrame method of the parent node is executed first, and the ProcessFrame method of the child node is executed first. However, one thing to note is that in this case, the PrepareFrame method of ScriptPlayable can be executed normally, but its ProcessFrame method will not be executed. Only the ProcessFrame method of ScriptPlayable connected to ScriptPlayableOutput will be executed.

In short, only handle pose and motion in AnimationScriptPlayable and put all other logic in the corresponding ScriptPlayable.

The ProcessRootMotion and ProcessAnimation methods of AnimationScriptPlayable are executed during the ProcessFrame period, and its PrepareFrame method is not open to us. If I remember correctly, setting the input weight of AnimationScriptPlayable does not affect data from the subtree. We need to manually make the weight effective through AnimationStream.

There are two issues that can easily cause animation abnormalities:

  • The Playable.SetTime method can jump the animation progress, but it also affects the RootMotion of the animation, causing the character to teleport. For example, if you set a Walk animation that has been played for 10 seconds to 3 seconds, the character’s motion from 3 to 10 seconds will also be rewound, and your character will be suddenly pulled back.
  • In the AnimationScriptPlayable.ProcessRootMotion method, we can modify AnimationStream.velocity to achieve position correction, but the final Animator.velocity (which can be seen in the OnAnimatorMove method) seems not entirely from AnimationStream.velocity.

These two issues seriously hindered my work, but I have not found a suitable solution yet.:frowning:

(In this graph, I didn’t place the ScriptPlayable before the AnimationPlayable, but instead created two trees with the same structure, which have the same effect.)

2 Likes

Wow!

Thanks for all the insight @SolarianZ !
This makes a lot of sense, and unfortunately I don’t have much direct experience with controlling the root motion through AnimationScriptPlayables currently, but if I find anything out sometime, I’ll certainly share my findings too :slight_smile:

Also your PlayableGraphMonitor editor window looks amazing, great work!

1 Like

@ModLunar Thank you! :wink: