unity 自定义时间轴_如何创建自定义时间轴标记

unity 自定义时间轴

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

Unity 2019.1 开始 ,时间轴支持标记! 在此博客文章中,我将向您展示如何创建自定义标记。

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:

为了添加新型标记,您需要做的就是创建一个继承 Marker 类的类:

1
public class SimpleMarker : UnityEngine.Timeline.Marker {}
1
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.

在这一点上,这个简单的标记只是一个 视觉项目 。 这意味着该标记 在触发时 无法运行代码 。 这并不意味着它没有用。 标记可以是捕捉点或注释(请参见第V部分)。 也可以 通过 编辑器中和运行时 的Timeline API对其 进行 访问 。

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.

Playable API允许在处理PlayableGraph时将通知发送到对象。 可播放的通知可用于通知目标对象已发生事件。 我将构建一个简单的图形并手动发送通知。

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

首先,我需要创建一个 Notification :一个实现 INotification 接口的类。

1
2
3
4
public class MyNotification : INotification
{
    public PropertyName id { get; }
}
1
2
3
4
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.

我们可以使用 id 属性来唯一标识通知。 对于这些示例,我并不是真的需要它,因此我将使用默认实现。

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. 

然后,我需要一个 接收器 :一个实现 INotificationReceiver 接口的类。 对于此示例,我有一个接收器,它将打印接收通知的时间。

1
2
3
4
5
6
7
8
9
10
11
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);
       }
   }
}
1
2
3
4
5
6
7
8
9
10
11
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. 

在以下示例中,我创建了一个新的可播放图形和一个新的可播放输出。 我 在可播放的输出中 添加了 ReceiverExample (使用 AddNotificationReceiver 方法)。 现在, m_Receiver 实例将能够接收发送到此输出的通知。

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

现在一切就绪,可以发送通知。 我可以 从可播放的输出中 使用 PushNotification 方法 推送新的通知 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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.

谨防! 调用 PushNotification 不会立即发送通知 他们只是 排队 这意味着它们将被累积,直到图形被完全处理为止。 LateUpdate阶段 之前 ,所有排队的通知都将发送到图的输出。 发送所有通知后,将在新帧开始之前清除队列。

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:

播放图形时, 将使用 作为参数发送的通知 在 m_Receiver 实例上 调用 OnNotify 方法 。   在播放模式下过渡时,此消息会出现在控制台中:

Received notification of type MyNotification at time 0

在时间0收到类型为MyNotification的通知

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. 

嗯……接收者正确地收到了通知,但是如何控制通知的发送时间呢? 我们需要更多帮助来实现这一目标。

第三部分– TimeNotificationBehaviour (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.

现在我们知道了如何通过可播放的图表发送通知,让我们安排一个通知,以便在我们选择的时间发送该通知。 我可以使用内置的 TimeNotificationBehaviour 类 。 此类是标准的PlayableBehaviour,因此可以将其添加到任何图形中,仅需使用一些其他逻辑即可在准确的时间发送通知。 让我们以前面的示例为例进行一些调整。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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();
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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:

如您所见,我没有 在可播放的输出上直接 调用 PushNotification ,而是 在输出上附加了 TimeNotificationBehaviour 并向其中 添加了一条通知 。 此行为将在正确的时间自动将通知推送到输出。 控制台现在显示:

Received notification of type MyNotification at time 2.00363647006452

在时间2.00363647006452收到类型为MyNotification的通知

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?

嗯......但为什么却没有 正好有 两个秒 发送 ? 当我将通知添加到 TimeNotificationBehaviour时 ,是否未指定 确切的 两秒钟?

1
notificationBehaviour.AddNotification(2.0, new MyNotification());
1
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. 

AddNotification 方法并不能保证一个确切的时间。 Unity开始渲染新帧时,可播放图表的时间会更新。 根据游戏的帧速率,PlayableGraph的评估时间可能与通知添加到TimeNotificationBehaviour时指定的时间不完全匹配。 什么 AddNotification 方法保证的是,该通知将尽快PlayableGraph的时间比通知触发时间时发送。

第四部分– MarkerNotification (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!

如果您想在“可播放图形”中手动发送通知,那么所有这些新API都很不错,但这可能需要很多工作。 幸运的是,时间轴可以自动生成适当的PlayableGraph来处理通知!

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

还记得第一部分中的标记吗? 让我们创建一个实现 INotification 接口 的新标记 。

1
2
3
4
public class NotificationMarker : Marker, INotification
{
   public PropertyName id { get; }
}
1
2
3
4
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:

Marker 继承 并实现 INotification的类 告诉Timeline它需要生成一个可播放的图形来支持此通知。 如果我将此标记添加到空白时间轴,则会创建以下可播放图形:

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!

这与我在第三部分中创建的可玩图表几乎相同,除了时间轴添加了自己的PlayableBehaviour。 不过,这比创建自己的可玩图表要容易得多!

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.

    如果标记在时间轴标题区域上: 拥有播放当前时间轴的PlayableDirector的对象将收到通知。

  • 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.

任何实现 INotificationReceiver 接口并位于目标对象上的组件都将接收通知。

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

在我的示例中,我向 时间线标题区域 添加了两个 NotificationMarker 。 我还向 播放时间线的对象 添加了 NotificationReceiver

Here’s the output from the console:

这是控制台的输出:

Received notification of type NotificationMarker at time 1.00330553948879

在时间1.00330553948879收到类型为NotificationMarker的通知

Received notification of type NotificationMarker at time 2.016666666666

在时间2.016666666666收到类型为NotificationMarker的通知

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:

只有实现 INotification 接口 的标记 才会生成正确的Playable Graph以支持通知。 为了明确起见,下面是创建自定义标记的不同方法的特殊性的表格:

第五部分-自定义样式 (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: 

第一步是创建样式表。 样式表可用于扩展编辑器的视觉外观。 这可以通过 在StyleSheets / Extensions文件夹层次结构的Editor文件夹中 添加一个名为 common.uss 的文件来完成 。 在我的示例中,我在以下位置添加了一个新文件:

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

“ 5-注释/编辑器/样式表/扩展名/common.uss”

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

USS文件(用于Unity样式表)使用类似于CSS的语法来描述新样式。 这是一个例子:

1
2
3
4
5
6
Annotation
{
   width:18px;
   height:18px;
   background-image: resource("Assets/5-Annotation/Editor/pencil.png");
}
1
2
3
4
5
6
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.

在这种样式中,我指定希望与尺寸属性一起使用铅笔图标。 接下来,我可以告诉时间轴在屏幕上绘制标记时应使用此样式。

1
2
3
4
5
[CustomStyle("Annotation")]
public class Annotation : Marker
{
   [TextArea] public string annotation;
}
1
2
3
4
5
[ 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.

CustomStyle 属性可以用来指定要使用的样式。 在我的示例中,我想使用 添加到 common.uss 文件中 的 注释 样式 。

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:

为了演示如何使用标记和通知,我 在Github存储库中 添加了一个 跳转 标记。 此标记与 JumpReceiver 结合 ,将在时间轴上从一个点“跳”到另一点。 用目标标记指定目标点。 此示例结合了此博客文章中完成的所有操作,包括自定义样式:

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.

通知和标记是Playable Graph和Timeline API的真正强大补充。 我的示例应足以使您开始创建很棒的东西! 如果需要帮助 ,请前往 时间轴论坛

第七部分奖金–片段通知 (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?

我们看到PlayableGraph可以发送通知,而Timeline可以使用这些通知来增强标记的功能。 但是剪辑呢? 片段还可以发送通知吗?

Yes!

是!

Notifications can be sent from a Playable Behaviour: 

可以通过可玩行为发送通知:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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();
   }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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:

在此示例中,我在剪辑的处理过程中每秒发送一次通知。 如果我将此剪辑添加到时间轴,并将 NotificationReceiver添加 到驱动时间轴的对象,则生成的输出如下:

Received notification of type MyNotification at time 1.01593019999564

在时间1.01593019999564收到类型为MyNotification的通知

Received notification of type MyNotification at time 2.00227000191808

在时间2.00227000191808收到类型为MyNotification的通知

Received notification of type MyNotification at time 3.01137680560353

在时间3.01137680560353收到类型为MyNotification的通知

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.

有用! 如果您已经拥有自己的Playable Behavior类,并且想要发送通知,则不需要绝对的标记。 剪辑已经支持许多功能。

翻译自: https://blogs.unity3d.com/2019/06/25/how-to-create-custom-timeline-markers/

unity 自定义时间轴