Creating Custom Tracks
Custom clip-based tracks follow a consistent four-class pattern used by the AI Command, Time Machine, Traffic Signal, and TMP Text Switcher tracks in this project (Assets/mobisim/Timeline/Scripts and Assets/mobisim/Traffic/Scripts). Each class has a specific responsibility. Marker-based tracks such as the Typewriter Event Marker Track use a different pattern: a MarkerTrack, a Marker that implements INotification, and a receiver component that implements INotificationReceiver.
The sections below walk through each class in turn: Track, Clip, Behaviour, and Mixer.
For a full reference on Unity's custom track API, see the Unity Playables API documentation.
Track
The track uses attributes to declare what it binds to and what clip type it accepts, then creates the mixer in CreateTrackMixer.
[TrackColor(0f, 0.5f, 1f)]
[TrackClipType(typeof(MyClip))]
[TrackBindingType(typeof(MyComponent))]
public class MyTrack : TrackAsset
{
public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
{
return ScriptPlayable<MyMixerBehaviour>.Create(graph, inputCount);
}
}
TrackBindingType controls what the track's binding slot accepts in the Timeline window. If the track does not need a scene binding, omit the attribute.
Clip
The clip holds the data an author sets in the Inspector. CreatePlayable is called once per clip when the graph is built; it copies authored values into a new behaviour instance and injects it into the graph.
[Serializable]
public class MyClip : PlayableAsset, ITimelineClipAsset
{
public MyBehaviour template = new MyBehaviour();
public ClipCaps clipCaps => ClipCaps.None;
public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
{
var playable = ScriptPlayable<MyBehaviour>.Create(graph, template);
// copy any ExposedReferences or extra data here
return playable;
}
}
Use ClipCaps.Blending instead of ClipCaps.None if you want the Editor to allow overlapping clips with blend curves. Scene references (transforms, components) cannot be serialized directly on a PlayableAsset; use ExposedReference<T> and resolve it inside CreatePlayable via graph.GetResolver().
Behaviour
The behaviour is a plain data container. Keep all logic out of it; the mixer is the correct place for logic.
[Serializable]
public class MyBehaviour : PlayableBehaviour
{
public float myValue;
}
Mixer
The mixer receives all active behaviours as inputs each frame through ProcessFrame. Use the input weights to blend or select between them, then apply the result to the bound object.
public class MyMixerBehaviour : PlayableBehaviour
{
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
{
var binding = playerData as MyComponent;
if (binding == null) return;
int inputCount = playable.GetInputCount();
for (int i = 0; i < inputCount; i++)
{
float weight = playable.GetInputWeight(i);
var inputPlayable = (ScriptPlayable<MyBehaviour>)playable.GetInput(i);
MyBehaviour behaviour = inputPlayable.GetBehaviour();
if (weight > 0f)
{
// apply behaviour.myValue to binding, weighted by weight
}
}
}
}
For discrete states, pick the input with the highest weight and apply only that one. For continuous values such as color or position, accumulate a weighted sum across all active inputs.