Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Creating a Dynamic Timeline from Script

Discussion in 'Timeline' started by sven_dh, Aug 22, 2019.

  1. sven_dh

    sven_dh

    Joined:
    Jun 6, 2019
    Posts:
    1
    Hi all,

    We would like to create a dynamic timeline at runtime via scripts. Maybe it's better to first state the requirements to check if this is the right approach :):

    We basically have a scene with multiple moving objects depending on time. The user would need to be able to rewind, play, pause and fast forward. We also have events that are triggered that are causing other objects to move or stop. We also want to control time via some sort of slider and be able to go specific points in time.

    My first thought was to record every object in the scene and have some kind of integration with the timeline to rewind or go to specific points in time. However, I was unable to do this. Maybe I'm missing something but dynamically recording and cutting recordings when going back in time and re-recording when pressing play / fast forward seemed very hard to do (because events can change so we would always need to re-record when pressing play / fast forward). Also, when fast forwarding the game with timescale, causes the recording to be speed up.

    So, my second thought was to create custom playables with a duration of 1 second and link them together on the timeline. This way i can rewind through the already created playables. When the player presses play again, I would need to start generating new playables from that point. I thought the best way of testing this was to create a custom PlayableBehaviour that simply moves objects depending on the begin and end positions defined in a property. However, I'm unable to pass the custom properties to the PlayableBehaviour via the PlayableAsset. In my code, the TimelineObject is always null.

    Maybe I'm missing something really critical here but I already scoured a lot of timeline posts and I haven't found any solution to my problem. I'm probably a bit confused with all the Playable / Timeline / Playable Graphs and PlayableDirector references and how to pass different properties to different playables. Any suggestions are always welcome because maybe what I'm doing is also not the right way.

    Thank you very much for your feedback!

    I will post my code to (maybe) make it a bit more clear :p

    Code (CSharp):
    1. [TrackBindingType(typeof(GameObject))]
    2. [TrackClipType(typeof(TrainControlClip))]
    3. public class MoveableControlTrack : TrackAsset
    4. {
    5.  
    6. }
    Code (CSharp):
    1. [Serializable]
    2.     public class TrainControlBehaviour : PlayableBehaviour
    3.     {
    4.  
    5.         public TimelineObject TimelineObject { get; set; }
    6.         public Transform Transform { get; set; }
    7.  
    8.         public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    9.         {
    10.             var gameObject = playerData as GameObject;
    11. // Timeline object is always null
    12.             if (gameObject == null || TimelineObject == null)
    13.                 return;
    14.  
    15.             float ratio = (float) playable.GetTime() / (float) playable.GetDuration();
    16.  
    17.             gameObject.transform.position = Vector3.Lerp(TimelineObject.beginPosition, TimelineObject.endPosition, ratio);
    18.         }
    19.     }
    Code (CSharp):
    1. [Serializable]
    2. public class TrainControlClip : PlayableAsset, ITimelineClipAsset
    3. {
    4.     [SerializeField] public TrainControlBehaviour template = new TrainControlBehaviour();
    5.  
    6.     public ExposedReference<TimelineObject> TimelineObjectReference;
    7.     public ExposedReference<Transform> TransformReference;
    8.  
    9.     public override double duration
    10.     {
    11.         get { return 1; }
    12.     }
    13.  
    14.     public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    15.     {
    16.         var playable = ScriptPlayable<TrainControlBehaviour>.Create(graph);
    17.         var director = owner.GetComponent<PlayableDirector>();
    18.  
    19.         TimelineObject reference = TimelineObjectReference.Resolve(director);
    20.         playable.GetBehaviour().TimelineObject = reference;
    21.         return playable;
    22.     }
    23.  
    24.     // Inheritance from ITimelineClipAsset
    25.     // We don't want to blend clips together
    26.     public ClipCaps clipCaps
    27.     {
    28.         get { return ClipCaps.None; }
    29.     }
    30. }
    Code (CSharp):
    1. public class TimelineObject : MonoBehaviour
    2.     {
    3.  
    4.         public Vector3 beginPosition { get; set; }
    5.         public Vector3 endPosition { get; set; }
    6.         public Quaternion beginRotation { get; set; }
    7.         public Quaternion endRotation { get; set; }
    8.  
    9.         public TimelineObject(Transform transform)
    10.         {
    11.             beginPosition = transform.position;
    12.             beginRotation = transform.rotation;
    13.         }
    14.  
    15.         public void SetEndValues(Transform transform)
    16.         {
    17.             endPosition = transform.position;
    18.             endRotation = transform.rotation;
    19.         }
    20.     }

    TimeManager that creates the clips and ties them together (only one clip at the moment)

    Code (CSharp):
    1. TrainControlClip clip = ScriptableObject.CreateInstance<TrainControlClip>();
    2.  
    3.         var playable = clip.CreatePlayable(director.playableGraph, gameObject);
    4.         TimelineAsset timelineAsset = (TimelineAsset) director.playableAsset;
    5.  
    6.         //create new animationTrack on timeline
    7.         var newTrack = timelineAsset.CreateTrack<MoveableControlTrack>(null, train.name);
    8.  
    9.         //bind object to which the animation shall be assigned to the created animationTrack
    10.         director.SetGenericBinding(newTrack, train);
    11.  
    12.         ////create a timelineClip for the animationClip on the AnimationTrack
    13.         var timelineClip1 = newTrack.CreateClip<TrainControlClip>();
    14.         timelineClip1.displayName = "clip1";
    15.  
    16.         TrainControlClip testPlayable = timelineClip1.asset as TrainControlClip;
    17.  
    18.         TimelineObject timelineObject = new TimelineObject(train.transform);
    19.         train.transform.position += Vector3.forward;
    20.         timelineObject.SetEndValues(train.transform);
    21.  
    22.         director.SetReferenceValue(testPlayable.TimelineObjectReference.exposedName, timelineObject);
     
    IvanIvanovP3 and bhtruong93 like this.
  2. bhtruong93

    bhtruong93

    Joined:
    Jul 12, 2017
    Posts:
    8
    Also having a lot of trouble with this, trying to keep things as simple as possible, but this still doesn't seem to do anything

    Code (CSharp):
    1.     public void CreateTrack(GameObject character)
    2.     {
    3.         ActivationTrack newTrack = new ActivationTrack();
    4.         director.SetGenericBinding(newTrack, character);
    5.         director.RebuildGraph();
    6.     }
     
  3. seant_unity

    seant_unity

    Unity Technologies

    Joined:
    Aug 25, 2015
    Posts:
    1,516
    There's a bit to unpack here, but let me try and clarify a few things to help you along.

    The timeline asset and the playableGraph are two different representations of the timeline. The asset is the static object that is built by the editor. It is then compiled with the bindings into a playable graph. The playable graph is an optimized instance of the timeline.

    I'm not sure I completely understand the requirements. If you just need to record in editor playmode and have a set of static timelines generated from that, I'd recommend looking at the Unity.Recorder package. It has a track for recording gameObject animation. Under the hood, it is using GameObjectRecorder, which may be more useful for a custom solution.

    If you need to record at runtime (i.e. in a player), and scrub the same timeline, the best approach may be doing everything with Playables using AnimationCurves. The reason is a lot of the functionality mentioned above needs the editor, which isn't available.

    One idea is have your TrainControlPlayable use AnimationCurves instead of a simple lerp for the position. If it's recording, add keys to the curves. If it's playing back evaluate the animation curves. In that case you would never need to rebuild the graph. The asset in that case would simply be for setting up default values, and setting which objects need to be recorded.

    If you need to save the results, the AnimationCurves could be shared with the PlayableAsset that generated the playable. The asset would the owner of the curves, and the playable updates them.

    In terms of the script you posted, I think a lot of the issues may be stemming from not using the Timeline Asset to create tracks and clips. For example, 'new ActivationTrack()' should be timelineAsset.CreateTrack<ActivationTrack>(). Or 'TrainControlClip clip = ScriptableObject.CreateInstance<TrainControlClip>();', won't actually create a clip in the timeline, and creating a playable from that will have no effect. If you don't use the appropriate Create calls, then when you rebuild the graph, the timeline asset won't have a link to it and won't 'play it'.

    Anyway, I hope that helps. Feel free to post followup questions where I wasn't clear
     
    mandisaw likes this.