How does the binding of Unity’s timeline work?

Timeline is Unity’s solution that lets you play cinematic content. It supports playing animations, particle simulation, audio clip, object activeness, as well as recording directly in timeline window. With timeline, we can create sequences that can be triggered in-game or used for cutscenes. Although timeline can be easily created, playing it at runtime can be more tricky. It’s likely that you want to play a timeline sequence using the character controlled by player in the level and handle the control back once finish, all performed seamlessly. That’s where comes the runtime binding of timeline. To easily binds timeline at runtime, you can use this free tool: https://github.com/Brian-Jiang/PragmaTimeline. The rest of this article will based on this tool.

The first type of binding is GenericBinding, which are the objects that binds to the track you see in timeline editor. In runtime, you may add a PlayableDirector component and set the timeline asset, or you may load the prefab that contains it directly. But either case you will find the objects you bind to the track are empty because they are scene objects that cannot be saved in assets. To play the timeline correctly, you need to get PlayableDirector component and call SetGenericBinding where the key is the track asset of that timeline(weird, but that’s how it works), and the value is the object you want to bind. Although it gives you a way to bind, how can you know which track asset is which track in your timeline? One way is to look up by track name(which I barely change and can mess up easily), but the better way is to record and store in a config file.

In PragmaTimeline, I created a struct that stores this. It still uses a name, but you can easily edit it in inspector

[Serializable]
public struct TrackBindInfo {
    public string key;
    public TrackAsset trackAsset;
}

At runtime, you can iterate through all tracks and try find the generic binding of that track. BindingMap is a dictionary that you pass in to map name to object.

foreach (var track in timelineAsset.GetOutputTracks()) {
    if (track == timelineAsset.markerTrack) continue;
    
    if (trackBindMap.TryGetValue(track, out var trackBindInfo)) {
        var key = trackBindInfo.key;
        if (bindingMap.TryGetValue(key, out var value)) {
            Director.SetGenericBinding(track, (Object) value);
        }
    }
}

Then the second type of binding is ExposedReference which can be used anywhere in Unity although it’s bared mentioned. It allows you to reference scene object by serialize a name and look up the name in an IExposedPropertyTable which contains the object you set earlier. When you use control track, you can see that the object referenced in a clip is using ExposedReference rather than direct reference.

In PragmaTimeline, I also used a struct to store this information.

[Serializable]
public struct ControlBindInfo {
    public string key;
    public int hash;
    public ControlPlayableAsset playableAsset;
}

And similarly at runtime, they are restored

foreach (var controlBind in controlBindMap) {
    var controlBindInfo = controlBind.Value;
    var key = controlBindInfo.key;
    if (bindingMap.TryGetValue(key, out var value)) {
        Director.SetReferenceValue(controlBindInfo.hash, (Object) value);
    }
}

Note that PropertyName is used as key which is a hashed string where the string is just a random guid generated when you edit timeline.

PragmaTimeline supports these two bindings that timeline uses and let you quickly edit it in inspector, by clicking the Update button, it will collection all bindings and let you name it. While in runtime, you can just pass a dictionary map the name to real object

var map = new Dictionary<string, object> {
    {"LogoRenderer", go.GetComponentInChildren<SpriteRenderer>()},
    {"LogoAnimator", go.GetComponent<Animator>()},
}
player = instance.GetComponent<TimelinePlayer>();
player.Init(map);
player.PlayTimeline(true);

PragmaTimeline also supports nested timeline where you just need to use nested dictionary

var map = new Dictionary<string, object> {
    {
        "Logo", new Dictionary<string, object> {
            {"LogoRenderer", go.GetComponentInChildren<SpriteRenderer>()},
            {"LogoAnimator", go.GetComponent<Animator>()}
        }
    },
}

In above example, Logo is map to the sub-timeline and two more object belong to sub-timeline.

If you use PragmaTimeline, you can easily collect objects that need bindings, name them, and use dictionary to restore binding. Otherwise you will need to use your own way to restore them by calling SetReferenceValue and SetGenericBinding on PlayableDirector

Internally, timeline uses Playable API and the PlayableBinding is used to record the output information of PlayableAsset and is serialized with that PlayableAsset.

In runtime, when a playable graph is build, PlayableAsset will provide IPlayable to the graph. The IPlayable can be Playable(act like a base class), ScriptPlayable(where you can provide your PlayableBehaviour to create custom behavior in playable graph), and other Playable(provide by Unity such as AudioClipPlayable). In your custom PlayableBehaviour


Posted

in

by

Tags:

Comments

Leave a comment

Blog at WordPress.com.