How to get the tracking binding from a timeline PlayableBehaviour?

I’ve been looking through the Unity docs for hours and lots of tutorials, but I can’t seem to find how to get the track binding from a Playable.

The reason I want to be able to get the track binding is so that I can store off the initial values of the component for the track in my track mixer with OnPlayableCreate. Once the timeline finishes, I want the initial values to be restored in the OnPlayableDestroy.

Thank you for any answers, advice, or tips!

There may be no way to find Track Binding on OnPlayableCreate, because when the playable is created, the playable may have not connected to any output or other playables (I’m not sure, I haven’t check it). Same as OnPlayableDestroy, that the connection may been already destroyed.
Also, it’ll be a little tricky to retrieve binding output from playable itself. (have to recursively go up and finally find the ScriptableOutput)

Instead, I suggest to use OnBehaviourPlay to initialize and cache the binding, and use OnPlayableDestroy to finalize the binding.
Take Animator as the binding output type as an example:

private bool isInitialized = false;
private Animator cachedTrackBinding = null;

public override void OnBehaviourPlay(Playable playable, FrameData info)
{
    // FrameData.output.GetUserData() is the TrackBinding in UnityEngine.Object form
    Initialize(info.output.GetUserData());
}

private void Initialize(UnityEngine.Object userData)
{
    // we only want to initialize at the very first time
    if (isInitialized) return;

    cachedTrackBinding = userData as Animator;
    if (cachedTrackBinding) {
        // do initialize thing...
    }
    isInitialized = true;
}

public override void OnPlayableDestroy(Playable playable)
{
    if (cachedTrackBinding) {
        // do finalize thing...
    }
}

Note that OnBehaviourPlay will also be called when the timeline paused and resumed, so use a flag to Initialize only once.

(Beware that these initialize/finalize thing will also be executed when it edit mode. If it is not desired, you can check Application.isPlaying to guard it.)

3 Likes

Thank you for such a detailed answer, this was extremely helpful and much closer to the results I wanted then everything else I have tried.

To add a bit more detail, I’m trying to create a clip that lerps an object to a position.
8923184--1222544--upload_2023-4-2_23-17-50.png

I have the cube start a (0,0,0) and the first clip lerps it to (10,10,10) and the second clip lerps it to (20,20,20). At the moment the cube will lerp to the position of whichever clip is playing and if I leave the timeline during editor mode, the cube will be permanently moved to the last position on the timeline. I want to have the cube reset back to its original position before any of the clips were played when in editor mode.

Is there any way to achieve this?

So, I finally got it using @Yuchen_Chang method with a track mixer.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;

public class LerpToPositionTrackMixer : PlayableBehaviour
{
    private bool isInitialized = false;
    private Transform cachedTrackBinding;
    private Vector3 initialPosition;

    public override void OnBehaviourPlay(Playable playable, FrameData info)
    {
        Initialize(playable, info.output.GetUserData());
    }

    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        Transform transform = playerData as Transform;

        if (!transform) { return; }

        for (int i = 0; i < playable.GetInputCount(); i++)
        {
            if (playable.GetInputWeight(i) > 0f)
            {
                var input = playable.GetInput(i);

                ScriptPlayable<LerpToPositionBehaviour> behaviourPlayable = (ScriptPlayable<LerpToPositionBehaviour>)input;

                float elapsedTime = (float)(input.GetTime() / input.GetDuration());

                Debug.Log($"Lerp of clip {i} with {elapsedTime}");

                LerpToPositionBehaviour behaviour = behaviourPlayable.GetBehaviour();
                transform.position = Vector3.Lerp(positionsForInputs[i], behaviour.Target.position, elapsedTime);
            }
        }
    }

    public override void OnPlayableDestroy(Playable playable)
    {
        if (cachedTrackBinding)
        {
            cachedTrackBinding.position = initialPosition;
        }
    }

    private List<Vector3> positionsForInputs = new List<Vector3>();

    private void Initialize(Playable playable, Object userData)
    {
        // we only want to initialize at the very first time
        if (isInitialized) return;

        cachedTrackBinding = userData as Transform;

        if (cachedTrackBinding)
        {
            initialPosition = cachedTrackBinding.position;
            positionsForInputs.Add(initialPosition);

            for (int i = 0; i < playable.GetInputCount(); i++)
            {
                var input = playable.GetInput(i);

                ScriptPlayable<LerpToPositionBehaviour> behaviourPlayable = (ScriptPlayable<LerpToPositionBehaviour>)input;
                LerpToPositionBehaviour behaviour = behaviourPlayable.GetBehaviour();

                positionsForInputs.Add(behaviour.Target.position);
            }
        }

        isInitialized = true;
    }
}

Is this the best way to do it? I’m not sure to be honest. There is a slight bug in this script, where it doesn’t lerp to snap to the final position in the very last frame of the clip, but at the very least the clip resets back to the initial values of the track binding component.

Oh so the question is for previewing, sorry for the misunderstanding.
For editor previewing & reverting after preview, There’s a method called GatherProperties in TrackAsset. If you set the properties that may change when previewing in the method, the timeline will automatically revert them after preview. (Same system as Animation Previewing! I think)

sample code may be like this:

        public void GatherProperties(PlayableDirector director, IPropertyCollector driver)
        {
            const string kLocalPosition = "m_LocalPosition";
            const string kLocalRotation = "m_LocalRotation";

            Transform trackBinding = director.GetGenericBinding(this) as Transform;
            if (trackBinding == null) return;

            var trackBindingGo = trackBinding.gameObject;

            driver.AddFromName<Transform>(trackBindingGo, kLocalPosition + ".x");
            driver.AddFromName<Transform>(trackBindingGo, kLocalPosition + ".y");
            driver.AddFromName<Transform>(trackBindingGo, kLocalPosition + ".z");

            driver.AddFromName<Transform>(trackBindingGo, kLocalRotation + ".x");
            driver.AddFromName<Transform>(trackBindingGo, kLocalRotation + ".y");
            driver.AddFromName<Transform>(trackBindingGo, kLocalRotation + ".z");
            driver.AddFromName<Transform>(trackBindingGo, kLocalRotation + ".w");
        }

keywords: GatherProperties, IPropertyPreview

1 Like

This worked extremely well…words can’t thank you enough. May I ask where you found all this information about timeline, any particular tutorials or documentation?

Glad to see that helps you!
There are seldom timeline script tutorials, but some good code samples may help you.

I think a good start point is to look into code samples from the Timeline package (go to package manager, and import “Samples” under Timeline package), or, import the Default Playables that Unity put on the asset store.
(Those two have almost the same sample tracks, but sample in Timeline package have additional timeline-editor samples; while Default Playables has a super easy-to-use Timeline Playable Wizard that creates all Boilerplate code for you when creating custom track)

Ultimately, there’s a super-detailed blog-post that explains the functions and relations of all important timeline classes, but it’s in Japanese. if you’re interested, the blog-post is here. You can use some page-translation tools of your browser, or a google-translated version is here.
(I benefit from this post a lot)

4 Likes