Search Unity

How to create custom Timeline Markers

June 25, 2019 in Engine & platform | 9 min. read
Share

Is this article helpful for you?

Thank you for your feedback!

Starting with Unity 2019.1, Timeline supports markers! In this blog post, I will show you how to create custom markers.

Previously, I explained how to use Timeline Signals to trigger events. When we originally designed that feature, we quickly saw that in order to make it work, we couldn’t use clips. Since one of the main characteristics of a signal is that it has no “duration”, we would need a new type of item, hence the addition of markers to Timeline. Internally, signals are implemented using markers. Let’s see how to add a custom marker to Timeline.

The code and assets used for this blog post are available here.

A Simple Marker

A marker is a new item that can be added to a Timeline Asset and is used to represent a point in time. Markers also have a specialization, just like clips do (Activation clip, Audio clip, Animation clip, etc). This lets you create your own type of marker to build something that covers your specific workflows.

In order to add a new type of marker, all you need to do is to create a class that inherits the Marker class:

public class SimpleMarker : UnityEngine.Timeline.Marker {}

That’s it! This custom marker can now be added to any track on the timeline marker area:

At this point, this simple marker is only a visual item. This means that this marker cannot run code when triggered. That doesn’t mean it is isn’t useful; a marker can be a snap point or an annotation (see Part V). It’s also accessible through the Timeline API in editor and at runtime.

We will need to combine a marker with another system to make it able to execute code. If you’re interested in knowing how the system works, read the next two parts, otherwise, you can skip to Part IV (I won’t be offended!).

Part II - Playable Notifications

The Playable API allows notifications to be sent to an object while a PlayableGraph is processed. Playable Notifications can be used to inform a target object that an event occurred. I will build a simple graph and manually send a notification.

First, I need to create a notification: a class that implements the INotification interface.

public class MyNotification : INotification
{
    public PropertyName id { get; }
}

We can use the id property to uniquely identify the notification. For these examples, I don’t really need it, so I will use the default implementation.

Then, I need a receiver: a class that implements the INotificationReceiver interface. For this example, I have a receiver that will print the time at which a notification was received. 

class ReceiverExample : INotificationReceiver
{
   public void OnNotify(Playable origin, INotification notification, object context)
   {
       if (notification != null)
       {
           double time = origin.IsValid() ? origin.GetTime() : 0.0;
           Debug.LogFormat("Received notification of type {0} at time {1}", notification.GetType(), time);
       }
   }
}

In the following example, I created a new playable graph and a new playable output. I added a ReceiverExample to the playable output (using the AddNotificationReceiver method). The m_Receiver instance will now be able to receive notification sent to this output. 

Everything is now in place to send a notification. I can push a new notification using the PushNotification method from the playable output. 

public class ManualNotification : MonoBehaviour
{
    PlayableGraph m_Graph;
    ReceiverExample m_Receiver;

    void Start()
    {
        m_Graph = PlayableGraph.Create("NotificationGraph");
        var output = ScriptPlayableOutput.Create(m_Graph, "NotificationOutput");

        //Create and register a receiver
        m_Receiver = new ReceiverExample();
        output.AddNotificationReceiver(m_Receiver);

        //Push a notification on the output
        output.PushNotification(Playable.Null, new MyNotification());

        m_Graph.Play();
    }
}

Beware! Notifications are not sent as soon as you call PushNotification; they are only queued. This means that they will be accumulated until the graph has been processed completely. Immediately before the LateUpdate stage, all queued notifications will be sent to the graph’s outputs. Once all the notifications are sent, the queue is cleared before a new frame begins.

When the graph is played, the OnNotify method will be called on the m_Receiver instance with the notification that was sent as an argument.  When transitioning in playmode, this message appears in the console:

Received notification of type MyNotification at time 0

Hmm... The receiver correctly received a notification, but how can I control the time at which the notification is sent? We’ll need more help to achieve that.

Part III - TimeNotificationBehaviour

Now that we know how to send a notification through a playable graph, let’s schedule a notification so that it is sent at a time of our choosing. I can use the built-in class TimeNotificationBehaviour for that. This class is a standard PlayableBehaviour, so it can be added to any graph, only with some more logic to send a notification at a precise time. Let’s take the previous example and tweak it a little bit.

public class ScheduledNotification : MonoBehaviour
{
   PlayableGraph m_Graph;
   ReceiverExample m_Receiver;

   void Start()
   {
       m_Graph = PlayableGraph.Create("NotificationGraph");
       var output = ScriptPlayableOutput.Create(m_Graph, "NotificationOutput");

       //Create and register a receiver
       m_Receiver = new ReceiverExample();
       output.AddNotificationReceiver(m_Receiver);

       //Create a TimeNotificationBehaviour
       var timeNotificationPlayable = ScriptPlayable<TimeNotificationBehaviour>.Create(m_Graph);
       output.SetSourcePlayable(timeNotificationPlayable);

       //Add a notification on the time notification behaviour
       var notificationBehaviour = timeNotificationPlayable.GetBehaviour();
       notificationBehaviour.AddNotification(2.0, new MyNotification());

       m_Graph.Play();
   }
}

Here’s the playable graph that was generated:

As you can see, instead of calling PushNotification directly on the playable output, I attached a TimeNotificationBehaviour to the output and added a notification to it. This behaviour will automatically push the notification to the output at the correct time. The console now displays:

Received notification of type MyNotification at time 2.00363647006452

All right! Now we can control when a notification is sent!

Hmm… But why was it not sent at exactly two seconds?  When I added the notification to the TimeNotificationBehaviour, didn’t I specify exactly two seconds?

notificationBehaviour.AddNotification(2.0, new MyNotification());

The AddNotification method does not guarantee an exact time. The Playable Graph’s time is updated when Unity begins rendering a new frame. Depending on the game’s frame rate, the evaluation time of a PlayableGraph may not exactly match the time specified when the notification was added to the TimeNotificationBehaviour. What the AddNotification method guarantees is that the notification will be sent as soon as the PlayableGraph’s time is greater than the notification trigger time.

Part IV - MarkerNotification

All these new APIs are nice if you want to manually send notifications in a Playable Graph, but it can be a lot of work. Fortunately, Timeline can automatically generate the proper PlayableGraph to handle notifications!

Remember the marker from Part I? Let’s create a new marker that implements the INotification interface.

public class NotificationMarker : Marker, INotification
{
   public PropertyName id { get; }
}

A class that inherits from Marker and implements INotification tells Timeline that it needs to generate a playable graph to supports this notification. If I add this marker to an empty timeline, the following playable graph is created:

This is nearly identical to the playable graph I created in Part III, except that Timeline added its own PlayableBehaviour. This was much easier than creating my own Playable Graph though!

The only thing left is to identify who is going to receive the notifications. The rules are the same as signals:

  • If the marker is on the timeline header area: the object which owns the PlayableDirector that plays the current timeline will receive notifications.
  • If the marker is on a track: the object bound to the track will receive notifications.

Any component that implements the INotificationReceiver interface and is located on the target object will receive notifications.

In my example, I added two NotificationMarkers to the timeline header area. I also added a NotificationReceiver to the object that plays the timeline.

Here’s the output from the console:

Received notification of type NotificationMarker at time 1.00330553948879

Received notification of type NotificationMarker at time 2.016666666666

Which is exactly the same as Part III.

Only the markers that implement the INotification interface will generate the proper Playable Graph to support notifications. To make things clear, here’s a table of the particularities of the different ways to create custom markers:

Part V - Custom Style

Our custom marker is represented visually by a generic “pin” icon, but it is also possible to change this icon to the image of your choice. To demonstrate how to do this, I will create an Annotation marker.

The first step is to create a stylesheet. Stylesheets can be used to extend the Editor’s visual appearance. This can be done by adding a file named common.uss in an Editor folder in a StyleSheets/Extensions folder hierarchy. In my example, I added a new file at the following location: 

“5-Annotation/Editor/Stylesheets/Extensions/common.uss”

USS files (for Unity Style Sheet) use a CSS-like syntax to describe new styles. Here is an example:

Annotation
{
   width:18px;
   height:18px;
   background-image: resource("Assets/5-Annotation/Editor/pencil.png");
}

In this style, I specified that I wish to use a pencil icon along with size properties. Next, I can tell Timeline that this style should be used when drawing a marker on-screen.

[CustomStyle("Annotation")]
public class Annotation : Marker
{
   [TextArea] public string annotation;
}

The CustomStyle attribute can be used to specify which style to use. In my example, I want to use the Annotation style that I added to the common.uss file.

When adding this Annotation marker to a timeline, it will use the custom style I created:

Part VI - Putting it all together

To demonstrate what can be done with markers and notifications, I added a Jump marker to the Github repo. This marker, combined with a JumpReceiver, will ‘’jump’’ from one point to another in a timeline. The destination point is specified with a Destination marker. This example combines everything that was done in this blog post, including a custom style:

The orange arrow is the jump point and the purple arrow is the destination point. See how I did it here.

Notifications and markers are really powerful additions to the Playable Graph and Timeline APIs. My examples should cover enough to get you started creating cool stuff! Head to the Timeline forums if you need help.

Bonus Part VII - Notifications from clips

We saw that a PlayableGraph can send notifications and that Timeline can use those to enhance the capabilities of markers. But what about clips? Can clips also send notifications?

Yes!

Notifications can be sent from a Playable Behaviour: 

public class ClipNotificationBehaviour : PlayableBehaviour
{
   double m_PreviousTime;

   public override void OnGraphStart(Playable playable)
   {
       m_PreviousTime = 0;
   }

   public override void ProcessFrame(Playable playable, FrameData info, object playerData)
   {
       if ((int)m_PreviousTime < (int)playable.GetTime())
       {
           info.output.PushNotification(playable, new MyNotification());
       }

       m_PreviousTime = playable.GetTime();
   }
}

In this example, I push a notification at each second during the clip’s processing. If I add this clip to a timeline and a NotificationReceiver to the object that drives the timeline, here’s the generated output:

Received notification of type MyNotification at time 1.01593019999564

Received notification of type MyNotification at time 2.00227000191808

Received notification of type MyNotification at time 3.01137680560353

It works! If you already have your own Playable Behaviour classes and want to send notifications, you don’t absolutely need a marker; clips already support much of the functionality.

June 25, 2019 in Engine & platform | 9 min. read

Is this article helpful for you?

Thank you for your feedback!