Any way to link bindings of two tracks via the Timeline Editor?

Pretty sure there isn't, but it doesn't hurt to ask. Who knows, maybe it's a feature worth implementing.
Here's my case:

I'm working on using Timeline to create my levels on a bullethell game.
I have 2+ tracks which I group together because they are always to be bound to the same GameObject, in this case an enemy (I have separate tracks for movement and attacks).

Thing is, I want to be able to instantiate that GameObject at runtime to conserve memory and reduce the loading time of the scene (since the levels are supposed to be quite big).

[In my specific case I can work around this by separating the level in smaller sections and making these separate timelines which can be instantiated full, but I had this other idea while thinking about the case.]

So here's the idea:

In one track, I can instantiate the GameObject required for the binding; I can have a Clip do that for me by having it take a prefab, instantiating it and linking it to the track. That would solve my problem if I had only one track per gameobject instance; but since I'm using multiple tracks per object, that would require the clip somehow knowing exactly what tracks to bind.

I believe there is no current way to do that, since each track that requires binding seems to create it's own exposed property in the Director. In and ideal world I would like to be able to link said tracks to a single binding. That way, when my clip forces it's own binding to change it also binds the required object for all others.

Anyway, just a thought. Seems pretty tough to implement, but also very useful.

Right now this sounds like something you would need a script for, and it would have to instantiate and bind the prefab prior to playing the timeline.

For example:

    public GameObject prefab;

    public void PlayTimelineWithPrefab()
    {
        GameObject go = GameObject.Instantiate(prefab);
        PlayableDirector director = GetComponent<PlayableDirector>();

        // get the outputs - each non-group track corresponds to an output
        foreach (var output in director.playableAsset.outputs)
        {
            // identify the tracks that you want to bind
            if (output.streamName.StartsWith("BindMe"))
            {
                // go.GetComponent<> may be necessary if the track uses a component and
                // not a game object
                director.SetGenericBinding(output.sourceObject, go);
            }
        }
        director.Play();
    }

Is there any specific reason as to why it needs to be set prior to playing the timeline? It doesn't seem to be a limitation of the system since the editor can handle changing bindings just fine while the timeline is running. Looks like a war on references, if anything.


EDIT: I managed to do it! Required some hacking but it worked for a simple case. After messing with it a bit more, the current state is:

I have a custom Track type. When I create a new Mixer for this type of track I give it some extra information; the Track itself and the Director which runs it (which I get using FindObject and comparing PlayableGraphs).
Then, in the Mixer itself all I have to do is check when my Playable type for instantiating the prefab is running. At that point all I have to do is use the references I already have from the moment the Mixer was instantiated and set the Director's bindings!

I'm gonna clean up the code and try to post it here tomorrow.

Regardless, after some thought I guess what I really want is being able to create my own subtrack types, like the Animation Override subtrack. Is that something that is currently possible? I can't find a way to do it.

Ok, so, I believe I found what I want. Problem is, I can't use it.
3142674--238499--upload_2017-7-13_1-17-59.png

Focusing on not crying right now.

EDIT: Not sad anymore, since I managed to hack into that other thing I wanted. Still want this one badly though. :)

The editor actually restarts the graph when the bindings change.

As for SupportChildTracks not being public --It's in our backlog. We made it internal because it's not properly supported on custom tracks yet.

I don't think I get it, but I managed to do just what I said and it still works even when built.
Here's my code.

These two are just as usual.

[Serializable]
public class EnemyControlBehaviour : PlayableBehaviour
{
    public GameObject prefab;
    public Color color;
}
[Serializable]
public class EnemyControlClip : PlayableAsset, ITimelineClipAsset
{
    public EnemyControlBehaviour template = new EnemyControlBehaviour ();

    public ClipCaps clipCaps
    {
        get { return ClipCaps.None; }
    }

    public override Playable CreatePlayable (PlayableGraph graph, GameObject owner)
    {
        var playable = ScriptPlayable<EnemyControlBehaviour>.Create (graph, template);
        return playable;
    }
}

Here's where it's interesting

public class EnemyControlTrack : TrackAsset
{
    public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
    {
        var mixer = ScriptPlayable<EnemyControlMixerBehaviour>.Create(graph, inputCount);

//Giving the Mixer some extra info before returning it
        mixer.GetBehaviour().track = this;

        PlayableDirector[] directors = UnityEngine.Object.FindObjectsOfType<PlayableDirector>();

        var d = from dir in directors
                where dir.playableGraph.Equals(graph.GetRootPlayable(0).GetGraph())
                select dir;

        mixer.GetBehaviour().director = d.First();

        return mixer;
    }
}
public class EnemyControlMixerBehaviour : PlayableBehaviour
{
    Enemy m_TrackBinding;   
    public PlayableDirector director;
    public TrackAsset track;

    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        int inputCount = playable.GetInputCount();

        //Look through all the clips in the mixer
        for (int i = 0; i < inputCount; i++)
        {
            //If the clip is currently running
            if (playable.GetInputWeight(i) == 1)
            {               
                ScriptPlayable<EnemyControlBehaviour> inputPlayable = (ScriptPlayable<EnemyControlBehaviour>)playable.GetInput(i);
                EnemyControlBehaviour input = inputPlayable.GetBehaviour();

                //If the track is is still not bound
                if (playerData == null)
                {
                    //Instantiate and bind the object to the track
                    var g = GameObject.Instantiate(input.prefab, Vector3.zero, Quaternion.identity);
                    m_TrackBinding = g.GetComponent<Enemy>();
                    director.SetGenericBinding(track, m_TrackBinding);                   
                }

                m_TrackBinding.SetColor(input.color);
            }
        }           
    }
}

Here is a video of it working

https://www.youtube.com/watch?v=Ee-HR-uE_jI

I've also built the project with the Director set to play on awake and it seems to work just fine.

There's a .unitypackage with all the assets I used attached to the post.

Yeah, I imagined that was the case. Any prediction of when it might be available?

3143786--238639--EnemyControl.unitypackage (13.5 KB)

1 Like

Oh yeah, that should work just fine! To help your script, the gameObject passed to the CreateTrackMixer has the playableDirector component you need on it.

Right now we have improved customization support planned for 18.1, hopefully some of the smaller fixes (like SupportChildTracks) can land sooner. As you can imagine, the amount of feedback is growing by the day, so we will try to adjust our plans accordingly.


Oh. I didn't even realize it was passing a GameObject. That was literally what took the most time since I had to fight with those graph.root.idontknowwhatimdoing calls.
Now I feel kinda stupid.
Hahahahahah xD
With this reference it gets really easy and I believe a lot more stable as well. For starters it should work with prefabs as well as objects in scene ( I didn't test, but I assume the FindObject approach wouldn't work in that case).

Thanks. SupportChildTracks is the one thing I think is a key feature for the API still missing. Although it all feels pretty clunky at this stage due to the sheer amount of code required and code duplication, it is already so very much powerful. I've spent these last days thinking about the possibilities and I have been loving it so much.

I can even quote a friend to tell exactly how i feel with this:
"Timeline is the kind of thing a person from 2020 looks at and thinks: how the hell did people mess with Unity before Timeline"

Thank you very much for the support.

1 Like

There is a bug so replace it with the following.
It appears when you play it second time, the refrence to bound object is lost when the timeline stops. :)

//If the track is is still not bound
                m_TrackBinding = (Enemy)director.GetGenericBinding (track);
                if (m_TrackBinding == null)
                {
                    var g = GameObject.Instantiate(input.prefab, Vector3.zero, Quaternion.identity);
                    m_TrackBinding = g.GetComponent<Enemy>();
                    director.SetGenericBinding(track, m_TrackBinding);                   
                }
                m_TrackBinding.SetColor(input.color)
;

Yeah, that makes sense. Thanks!

I've also made some changes to allow for something similar to the linking I first wanted. I'll edit this post later with the code. The approach is actually very simple: it only works with tracks in the same GroupTrack. Once one of them instances and binds itself it looks through all the others and binds them as well.

Right now I'm trying to think of a way to make previewing less of a mess. I think I'll go with the same approach of the Activation Track and build a separate track type for lifetime.

Great that you got it working. Waiting for the package to see how you did it. It will be helpful with my projects too. :)

Here's a package with the Lifetime track approach.
It only works for a specific type for binding (since that's all I require at the moment), but with some reflections it should be easy enough to set the binding to work only for the tracks of the correct binding type.

It also currently doesn't deal with the object being destroyed externally as a possibility, but that should be easy enough to do with a boolean.

3151541--239546--Lifetime.unitypackage (15.5 KB)

Thanks for the package. :)

Tested Lifetime track and it works very good. I deleted the gameobject in play mode but it instantiated a new one and assigned to the tracks again. It works without any problem. Only issue will be if you want to destroy it on purpose, ie health is below 0. :)

Edited.

Issue encountered....
This being a Enemy track I cant transform key the sprite.

Yeah, that's what I was talking about. I'm still thinking about how to deal with it.

Could you explain it a little better? I don't think I get it.

I have made changes so that it only instantiates in region only, and also does not destroy the object if not in region.
I will also implement a bool so that it will instantiate or destroy according to bool in its region.

Also made changes so that it assigns Animator of Enemy to the Animation track.

    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        if(!trackBinding)
            trackBinding = playerData as Enemy;
        if (!trackBinding)
            return;
public class EnemyLifetimeMixerBehaviour : PlayableBehaviour
{
    public TrackAsset track;
    public PlayableDirector director;
    private Enemy instance;

    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {     
        if (!instance)
            instance = playerData as Enemy;

        int inputCount = playable.GetInputCount ();

        if (!track.GetGroup())
        {
            Debug.LogError("LifetimeTrack needs to be inside a GroupTrack!");
            return;
        }

        //Loooking through all clips to see if one is running
        for (int i = 0; i < inputCount; i++)
        {
            float inputWeight = playable.GetInputWeight(i);
            ScriptPlayable<EnemyLifetimeBehaviour> inputPlayable = (ScriptPlayable<EnemyLifetimeBehaviour>)playable.GetInput(i);
            EnemyLifetimeBehaviour input = inputPlayable.GetBehaviour ();

            //If a clip is running
            if(inputWeight == 1)
            {             
                if (!instance && !input.remove)
                {
                    //Instantiate if not already
                    instance = GameObject.Instantiate(input.prefab).GetComponent<Enemy>();

                    //Bind new instance to all tracks in the same group as this one
                    foreach (TrackAsset t in track.GetGroup().GetChildTracks())
                    {
                        // Check for track type and add accordingly
                        if (t.GetType () == typeof(AnimationTrack)) {
                            Animator temp = instance.GetComponent<Animator> ();
                            director.SetGenericBinding (t, instance.GetComponent<Animator> ());
                        }else{
                            director.SetGenericBinding(t, instance);
                        }

                        //Debug.Log(t.GetType ());
                    }
                }

                if (instance != null && input.remove) {

                    // enemy is a seperate object without director
                    // Destroy is only available in monobehaviours

                    //director.gameObject.GetComponent<Enemy> ().DestroyMe ();

                    instance.DestroyMe ();
                }
            }
        }
    }
}
public class Enemy : MonoBehaviour
{
    public void SetColor(Color color)
    {
        GetComponent<SpriteRenderer>().color = color;
    }

    public void DestroyMe()
    {
        Debug.Log ("Enemy DestroyMe called");
        DestroyImmediate (this.gameObject);
    }
}

Edit :
Added bool to specify destroy behavior. So now the enemy can destroy itself or can be destroyed from timeline too.

Any update on this? I was hoping to group multiple tracks together to all use the same binding but it feels like all the functionality that would allow that to happen is hidden behind internals