Search Unity

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

Discussion in 'Timeline' started by AndrewKaninchen, Jul 12, 2017.

  1. AndrewKaninchen

    AndrewKaninchen

    Joined:
    Oct 30, 2016
    Posts:
    149
    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.
     
    Last edited: Sep 10, 2018
  2. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    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:
    Code (CSharp):
    1.     public GameObject prefab;
    2.  
    3.     public void PlayTimelineWithPrefab()
    4.     {
    5.         GameObject go = GameObject.Instantiate(prefab);
    6.         PlayableDirector director = GetComponent<PlayableDirector>();
    7.  
    8.         // get the outputs - each non-group track corresponds to an output
    9.         foreach (var output in director.playableAsset.outputs)
    10.         {
    11.             // identify the tracks that you want to bind
    12.             if (output.streamName.StartsWith("BindMe"))
    13.             {
    14.                 // go.GetComponent<> may be necessary if the track uses a component and
    15.                 // not a game object
    16.                 director.SetGenericBinding(output.sourceObject, go);
    17.             }
    18.         }
    19.         director.Play();
    20.     }
    21.  
     
  3. AndrewKaninchen

    AndrewKaninchen

    Joined:
    Oct 30, 2016
    Posts:
    149
    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.
     
    Last edited: Jul 13, 2017
  4. AndrewKaninchen

    AndrewKaninchen

    Joined:
    Oct 30, 2016
    Posts:
    149
    Ok, so, I believe I found what I want. Problem is, I can't use it.
    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. :)
     
    Last edited: Jul 13, 2017
  5. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    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.
     
  6. AndrewKaninchen

    AndrewKaninchen

    Joined:
    Oct 30, 2016
    Posts:
    149
    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.
    Code (CSharp):
    1. [Serializable]
    2. public class EnemyControlBehaviour : PlayableBehaviour
    3. {
    4.     public GameObject prefab;
    5.     public Color color;
    6. }
    7.  
    Code (CSharp):
    1. [Serializable]
    2. public class EnemyControlClip : PlayableAsset, ITimelineClipAsset
    3. {
    4.     public EnemyControlBehaviour template = new EnemyControlBehaviour ();
    5.  
    6.     public ClipCaps clipCaps
    7.     {
    8.         get { return ClipCaps.None; }
    9.     }
    10.  
    11.     public override Playable CreatePlayable (PlayableGraph graph, GameObject owner)
    12.     {
    13.         var playable = ScriptPlayable<EnemyControlBehaviour>.Create (graph, template);
    14.         return playable;
    15.     }
    16. }
    17.  
    18.  
    Here's where it's interesting

    Code (CSharp):
    1. public class EnemyControlTrack : TrackAsset
    2. {
    3.     public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
    4.     {
    5.         var mixer = ScriptPlayable<EnemyControlMixerBehaviour>.Create(graph, inputCount);
    6.        
    7. //Giving the Mixer some extra info before returning it
    8.         mixer.GetBehaviour().track = this;
    9.  
    10.         PlayableDirector[] directors = UnityEngine.Object.FindObjectsOfType<PlayableDirector>();
    11.  
    12.         var d = from dir in directors
    13.                 where dir.playableGraph.Equals(graph.GetRootPlayable(0).GetGraph())
    14.                 select dir;
    15.  
    16.         mixer.GetBehaviour().director = d.First();
    17.  
    18.         return mixer;
    19.     }
    20. }
    Code (CSharp):
    1. public class EnemyControlMixerBehaviour : PlayableBehaviour
    2. {
    3.     Enemy m_TrackBinding;  
    4.     public PlayableDirector director;
    5.     public TrackAsset track;
    6.  
    7.     public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    8.     {
    9.         int inputCount = playable.GetInputCount();
    10.  
    11.         //Look through all the clips in the mixer
    12.         for (int i = 0; i < inputCount; i++)
    13.         {
    14.             //If the clip is currently running
    15.             if (playable.GetInputWeight(i) == 1)
    16.             {              
    17.                 ScriptPlayable<EnemyControlBehaviour> inputPlayable = (ScriptPlayable<EnemyControlBehaviour>)playable.GetInput(i);
    18.                 EnemyControlBehaviour input = inputPlayable.GetBehaviour();
    19.  
    20.                 //If the track is is still not bound
    21.                 if (playerData == null)
    22.                 {
    23.                     //Instantiate and bind the object to the track
    24.                     var g = GameObject.Instantiate(input.prefab, Vector3.zero, Quaternion.identity);
    25.                     m_TrackBinding = g.GetComponent<Enemy>();
    26.                     director.SetGenericBinding(track, m_TrackBinding);                  
    27.                 }
    28.  
    29.                 m_TrackBinding.SetColor(input.color);
    30.             }
    31.         }          
    32.     }
    33. }
    Here is a video of it working


    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?
     

    Attached Files:

  7. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    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.
     
  8. AndrewKaninchen

    AndrewKaninchen

    Joined:
    Oct 30, 2016
    Posts:
    149
    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.
     
    seant_unity likes this.
  9. msl_manni

    msl_manni

    Joined:
    Jul 5, 2011
    Posts:
    272

    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. :)

    Code (CSharp):
    1.  
    2. //If the track is is still not bound
    3.                 m_TrackBinding = (Enemy)director.GetGenericBinding (track);
    4.                 if (m_TrackBinding == null)
    5.                 {
    6.                     var g = GameObject.Instantiate(input.prefab, Vector3.zero, Quaternion.identity);
    7.                     m_TrackBinding = g.GetComponent<Enemy>();
    8.                     director.SetGenericBinding(track, m_TrackBinding);                  
    9.                 }
    10.                 m_TrackBinding.SetColor(input.color)
    11. ;
     
  10. AndrewKaninchen

    AndrewKaninchen

    Joined:
    Oct 30, 2016
    Posts:
    149
    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.
     
  11. msl_manni

    msl_manni

    Joined:
    Jul 5, 2011
    Posts:
    272
    Great that you got it working. Waiting for the package to see how you did it. It will be helpful with my projects too. :)
     
  12. AndrewKaninchen

    AndrewKaninchen

    Joined:
    Oct 30, 2016
    Posts:
    149
    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.
     

    Attached Files:

    Last edited: Jul 19, 2017
  13. msl_manni

    msl_manni

    Joined:
    Jul 5, 2011
    Posts:
    272
    Thanks for the package. :)
     
  14. msl_manni

    msl_manni

    Joined:
    Jul 5, 2011
    Posts:
    272
    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.
     
    Last edited: Jul 20, 2017
  15. AndrewKaninchen

    AndrewKaninchen

    Joined:
    Oct 30, 2016
    Posts:
    149
    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.
     
  16. msl_manni

    msl_manni

    Joined:
    Jul 5, 2011
    Posts:
    272
    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.


    Code (CSharp):
    1.     public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    2.     {
    3.         if(!trackBinding)
    4.             trackBinding = playerData as Enemy;
    5.         if (!trackBinding)
    6.             return;
    7.  

    Code (CSharp):
    1. public class EnemyLifetimeMixerBehaviour : PlayableBehaviour
    2. {
    3.     public TrackAsset track;
    4.     public PlayableDirector director;
    5.     private Enemy instance;
    6.  
    7.     public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    8.     {    
    9.         if (!instance)
    10.             instance = playerData as Enemy;
    11.  
    12.         int inputCount = playable.GetInputCount ();
    13.  
    14.         if (!track.GetGroup())
    15.         {
    16.             Debug.LogError("LifetimeTrack needs to be inside a GroupTrack!");
    17.             return;
    18.         }
    19.  
    20.         //Loooking through all clips to see if one is running
    21.         for (int i = 0; i < inputCount; i++)
    22.         {
    23.             float inputWeight = playable.GetInputWeight(i);
    24.             ScriptPlayable<EnemyLifetimeBehaviour> inputPlayable = (ScriptPlayable<EnemyLifetimeBehaviour>)playable.GetInput(i);
    25.             EnemyLifetimeBehaviour input = inputPlayable.GetBehaviour ();
    26.          
    27.             //If a clip is running
    28.             if(inputWeight == 1)
    29.             {            
    30.                 if (!instance && !input.remove)
    31.                 {
    32.                     //Instantiate if not already
    33.                     instance = GameObject.Instantiate(input.prefab).GetComponent<Enemy>();
    34.  
    35.                     //Bind new instance to all tracks in the same group as this one
    36.                     foreach (TrackAsset t in track.GetGroup().GetChildTracks())
    37.                     {
    38.                         // Check for track type and add accordingly
    39.                         if (t.GetType () == typeof(AnimationTrack)) {
    40.                             Animator temp = instance.GetComponent<Animator> ();
    41.                             director.SetGenericBinding (t, instance.GetComponent<Animator> ());
    42.                         }else{
    43.                             director.SetGenericBinding(t, instance);
    44.                         }
    45.  
    46.                         //Debug.Log(t.GetType ());
    47.                     }
    48.                 }
    49.  
    50.                 if (instance != null && input.remove) {
    51.                  
    52.                     // enemy is a seperate object without director
    53.                     // Destroy is only available in monobehaviours
    54.  
    55.                     //director.gameObject.GetComponent<Enemy> ().DestroyMe ();
    56.  
    57.                     instance.DestroyMe ();
    58.                 }
    59.             }
    60.         }
    61.     }
    62. }
    63.  
    Code (CSharp):
    1. public class Enemy : MonoBehaviour
    2. {
    3.     public void SetColor(Color color)
    4.     {
    5.         GetComponent<SpriteRenderer>().color = color;
    6.     }
    7.  
    8.     public void DestroyMe()
    9.     {
    10.         Debug.Log ("Enemy DestroyMe called");
    11.         DestroyImmediate (this.gameObject);
    12.     }
    13. }
    Edit :
    Added bool to specify destroy behavior. So now the enemy can destroy itself or can be destroyed from timeline too.
     
    Last edited: Jul 21, 2017
  17. Windwalk_Rosco

    Windwalk_Rosco

    Joined:
    Aug 10, 2020
    Posts:
    20
    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