Timeline Mixer Documentation
Loading...
Searching...
No Matches
Track Processors

Track processors are executed once per track during the TimelineMixer's setup phase, or when rebinding a track at runtime. They handle the setup of connections to mixers and outputs on the PlayableGraph, and also the disconnection process.

In short, they tell Timeline Mixer what to do with this track, and how to connect it on Timeline mixers PlayableGraph.

Basic knowledge of the Unity Playables API is recommended before proceeding with this section. The code examples are heavily commented, but reviewing Unitys Playables documentation will make this easier to understand.

Let's look at the Playable Graph generated by the SignalTrackProcessor:

Track processors manage the section highlighted in red. They connect the Timeline to any required mixer and other playables, and then to the output (which is bound to your component).

In signal tracks, this Passthrough playable is not strictly necessary but serves as a good example for creating a custom track processor with your own mixer.

Custom Track Processors

You can customize how the timeline connects to its outputs on the Playable Graph by using a custom Track Processor. If your custom track doesn't have a specialized track processor, TimelineMixer will use a default one.

To create one, follow these steps:

  1. Create a new C# script.
  2. Add the TrackProcessor attribute, passing in the type of the timeline track.
  3. Inherit from TrackProcessor instead of MonoBehaviour.
  4. Override the ProcessTrack method and connect your playables there.
  5. Override the DisconnectTrack method if you use custom disconnection logic for rebinding tracks

Here's an example, this is the LightControlTrackProcessor included with the TimelineMixer package:

[TrackProcessor(typeof(LightControlTrack))]
public class LightControlTrackProcessor : TrackProcessor
{
//Store already generated outputs in a dictionary, this lets us find them later and connect to a mixer if that applies to your track
public Dictionary<Light, ScriptPlayableOutput> lightOutputs = new Dictionary<Light, ScriptPlayableOutput>();
private Dictionary<Playable, PlayableNode> mixerNodes = new Dictionary<Playable, PlayableNode>();
public override void ProcessTrack(PlayableGraph graph, TrackAsset track, ScriptPlayable<TimelinePlayable> playable, int trackIndex, Object boundComponent, ref PlayableConnectionInfo info)
{
ScriptPlayableOutput output;
Playable lightMixer;
PlayableNode lightMixerNode; //Wrapper for playables
//Skip if the track is empty or has no binding
if (track.isEmpty && !track.hasClips) return;
if (boundComponent == null) return;
//Check if an output already exists for this Light component
if (lightOutputs.ContainsKey(boundComponent as Light))
{
//We've found an output, from this we can get the mixer, and the PlayableNode wrapper for it
lightOutputs.TryGetValue(boundComponent as Light, out output);
lightMixer = output.GetSourcePlayable();
mixerNodes.TryGetValue(lightMixer, out lightMixerNode);
}
//If no output is found, create a new one
else
{
//Create output
output = ScriptPlayableOutput.Create(graph, track.name);
lightOutputs.Add(boundComponent as Light, output);
//Create and store mixer
lightMixer = ScriptPlayable<LightControlMixer>.Create(graph);
ScriptPlayable<LightControlMixer> scriptPlayable = (ScriptPlayable<LightControlMixer>)lightMixer;
lightMixerNode = new PlayableNode(lightMixer);
lightMixerNode.Name = "Light Mixer Node";
//The custom made mixer requires a reference to its PlayableNode wrapper to make graph traversal easier
scriptPlayable.GetBehaviour().SetPlayableNode(lightMixerNode);
mixerNodes.Add(lightMixer, lightMixerNode);
}
//Connect output to the mixer
output.SetSourcePlayable(lightMixerNode.GetPlayable());
//Set the bound component for the output
output.SetUserData(boundComponent as Light);
//Finally, connect the mixer node to the timeline node using the ConnectPlayable method, this lets TimelineMixer set connection weights
ConnectPlayable(lightMixerNode, info.GetTimelineNode(), trackIndex, ref info);
}
}

This example would make a track processor that is applied to a custom timeline track called LightControlTrack.

Track processors are discovered automatically at runtime through reflection

The PlayableNode class

PlayableNode provides a lot of quality of life features, its purpose is to wrap a Playable such as AnimationMixerPlayable and track its connections, and make connecting and disconnecting easier.

You can find out what ports on one Playable are connected which ports on another, not an easy task normally.

Instead of directly connecting Playables to each other, wrap them in a PlayableNode and use that to connect to other PlayableNodes. Doing this tracks all your connections, and makes navigating downstream AND upstream on the graph possible in an easy way.