自从Unity 2017.1发布Timeline以来,受到了广大开发者的欢迎,创作出不少精彩作品。我们也收到很多反馈,其中一项就是Timeline事件功能的请求,它是Timeline缺少的功能,一些用户通过剪辑实现了事件功能,但剪辑有自身的缺点。此篇将为你介绍在Unity 2019.1中新推出的Timeline Signals信号功能和扩展。
介绍
标记可以在Timeline上和剪辑一样添加和处理,能够使用选取,复制粘贴和编辑模式等功能。就像剪辑一样,标记也有具体类型,例如:剪辑分为动画剪辑、激活剪辑、控制剪辑等类型。
工作原理
为了在Timeline上正确设置信号,我们需要三个部分:
Signal Asset信号资源:信号资源是发射器和接收器之间的联系。通常信号资源会用作标识符。
Signal Emitter信号发射器: 信号发射器会放在Timeline上,它包含对信号资源的引用。运行信号发射器时,如果当前时间比发射器的时间大,发射器会被触发,发射器会把信号资源发送到信号接收器。
Signal Receiver信号接收器:信号接收器是带有一组反应的组件,每个反应都关联到信号资源。当接收器知道信号已被触发时,它会激活关联到对应信号资源的反应。
如下图所示,当关联到同一信号资源的发射器触发时,相应反应会被激活。
关于基础的timeline signal的使用可以参考这篇文章。
扩展
unity自带的signal是不可以参数参数的, 或者说自能在signal receiver的inspector传递一个固定的参数,参数超过一个就不能被识别了,如下图所示:
这显然不能满足我们日常的开发需求,因为我们的参数常常来自于signal, 而不是reveiver。还好Unity给了我们自定义扩展的接口, 这样能实现一些比如参数传递、分支逻辑等功能了。
自定义信号发射器signal emitter
定义一个发射器signal emitter, 它是实现 INotification, INotificationOptionProvider接口的类。我们可以使用id属性来识别唯一标识通知。对于本文的示例,我们并不需要该功能,因此将使用默认的实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class MyNotification : INotification { public PropertyName id { get { return new PropertyName("MyNotification"); } } NotificationFlags INotificationOptionProvider.flags { get { return (emitOnce ? NotificationFlags.TriggerOnce : default(NotificationFlags)) | NotificationFlags.TriggerInEditMode; } } }
自定义信号接收器 signal receiver
我们需要一个接收器,它是实现INotificationReceiver接口的类。示例中,接收器会记录接收到通知的时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17class ReceiverExample : MonoBehaviour,INotificationReceiver { public void OnNotify(Playable origin, INotification notification, object context) { if (notification is JumpSignalEmmiter) { JumpSignalEmmiter signal = notification as JumpSignalEmmiter; director.time = signal.jumpTime; //跳转 } else if (notification is SlowSignalEmitter) { SlowSignalEmitter signal = notification as SlowSignalEmitter; director.playableGraph.GetRootPlayable(0).SetSpeed(signal.slowRate); //改变播放速率 } } }
这样你就可以接受到来自signnal的传递过来的参数了。 我们这里直接把INotificationReceiver挂载在MonoBehaviour的GameObject上,就直接可以收到信号了。运行效果如下:
另外我们也可以通过代码手动的派发signal, 而不是一定要通过time cursor来触发,通过使用AddNotificationReceiver方法,把ReceiverExample添加给可运行输出。m_Receiver实例现在可以接收发送到该输出的通知。
1
2
3
4
5
6var output = ScriptPlayableOutput.Create(director.playableGraph, ""); output.AddNotificationReceiver(this); //this 所在的类需要实现 INotificationReceiver JumpSignalEmmiter signal = new JumpSignalEmmiter(); //实现接口INotification sign.jumpTime = 0; output.PushNotification(Playable.Null, signal);
调用PushNotification后不会立即发送通知,它们仅会加入队列排队等候,这表示它们会进行积累,直到视图已经完全处理。
在LateUpdate阶段前,所有队列中的通知都会发送到视图的输出部分。在所有通知都发送后,队列会在新一帧开始前清空。
在视图播放的时候,将在m_Receiverinstance实例上调用OnNotify方法,并将通知作为参数发送。
TimeNotificationBehaviour指定时间点
使用内置类TimeNotificationBehaviour,该类是标准的PlayableBehaviour,所以它可以添加到任意视图,只要使用一些逻辑就可以在准确时间发送通知。
1
2
3
4
5
6var timeNotificationPlayable = ScriptPlayable<TimeNotificationBehaviour>.Create(m_Graph); output.SetSourcePlayable(timeNotificationPlayable); //在时间通知行为添加通知 var notificationBehaviour = timeNotificationPlayable.GetBehaviour(); notificationBehaviour.AddNotification(2.0, new MyNotification());
我们没有在可运行输出上直接调用PushNotification,而是给输出附加TimeNotificationBehaviour,然后给它添加一个通知。该行为会在正确时间自动把通知推送给输出,控制台会显示以下信息:
上述代码并不是在准确的第二秒发送?这是因为AddNotification方法不会确保准确的时间。在Unity开始渲染新一帧时,Playable Graph可运行视图会进行更新。根据游戏的帧率,在通知添加到TimeNotificationBehaviour时,PlayableGraph的估算时间可能不会正好符合指定的时间。
AddNotification方法会确保的是:通知将会在PlayableGraph的时间比通知触发时间大的时候发送。
自定义样式CSS
我们的自定义标记会以常见的“图钉”表示,也可以把该图标改为自己选择的图像。第一步是创建样式表。
样式表可以用来扩展编辑器的可视化外观,我们可以在编辑器文件夹的StyleSheets/Extensions目录下添加common.uss文件。
USS即Unity样式表,使用类似CSS的语法来描述新样式,下面是样式的示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15JumpSignalEmmiter { width: 18px; height: 18px; background-image: resource("Assets/Editor/StyleSheets/ico_collapsed.png"); } JumpSignalEmmiter:checked { background-image: resource("Assets/Editor/StyleSheets/ico_normal.png"); } JumpSignalEmmiter:focus:checked { background-image: resource("Assets/Editor/StyleSheets/ico_selected.png"); }
更多关于介绍Unity 使用uss创作更多的自定义样式, 可以参考官方的API。改完uss, Unity编辑器并不是立即发生样式改变,当点击运行按钮的时候,样式才发生改变。
注意,这里必须是common.uss文件名, 且目录名必须是Editor下StyleSheets/Extensions的子目录。 对应的SignalEmitter需要指定CumstomStyle。
1
2
3
4
5
6
7
8
9[CustomStyle("JumpSignalEmmiter")] public class JumpSignalEmmiter : Marker, INotification, INotificationOptionProvider { //to implement INotification & INotificationOptionProvider }
Tips
1. Signal Emitter参数说明
Retroactive
有时我们直接使用API( PlayableDirector.time = 1.4)或者游戏非常卡发生跳帧的时候, 导致标记没有在时序上经过, 是否还触发此signnal。如果勾上了,就算跳过timeline上的maker,只要当前时间在maker之后,就会触发。
EmitOnce
有时我们会循环播放某一段动作,如果此时勾上此选项, 信号量自会触发一次, 否则的话,每一次都会触发。
2. Context 不显示signnal
Unity默认显示Mark Context只在绑定gameobject或者component的track上, 比如说Control Track就不显示。
有时候有些自定义的signal emitter并不想出现在某些track里, 比如说一些用来控制全局的signal并不想出现在Animation Track里。一种最直接的方法针对对应的track添加一个Attribute, 并在中指定对应的TrackType:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44public enum TrackType { NONE = 0, MARKER = 1, ANIMTION = 1 << 1, CONTROL = 1 << 2, ANCHOR = 1 << 3, OTHER = 1 << 7 // put other at last } [AttributeUsage(AttributeTargets.Class)] public class MarkerAttribute : Attribute { public TrackType supportType = default(TrackType); public MarkerAttribute(TrackType type) { supportType = type; } public void AddType(TrackType type) { supportType |= type; } public bool SupportTrackType(TrackAsset track, Type type) { bool rst = false; if (track is MarkerTrack) { rst = (supportType & TrackType.MARKER) > 0; } else if (track is AnimationTrack) { rst = (supportType & TrackType.ANIMTION) > 0; } else if (track is AnchorTrack) { rst = (supportType & TrackType.ANCHOR) > 0; } else if (track is ControlTrack) { rst = (supportType & TrackType.CONTROL) > 0; } return rst; } }
针对每个Track设置好对应的track类型, 然后再TimelineContextMenu.cs 调用的地方制定特定的track:
1
2
3
4
5
6
7
8
9
10
11
12public static bool DoesTrackSupportMarkerType(TrackAsset track, Type type) { if (track.supportsNotifications) { bool rst = true; var attr = Attribute.GetCustomAttribute(type, typeof(MarkerAttribute)) as MarkerAttribute; if (attr != null) rst = attr.SupportTrackType(track,type); return rst; } return !typeof(INotification).IsAssignableFrom(type); }
比如上述代码,自定义的JumpSignalEmmiter和SlowSignalEmitter, 就只会marker track里显示了。
3. 利用marker生成曲线
自定义的Emitter 继承Marker, 只要不实现INotification, 就不会生成在全局广播。 利用Marker可以标记的性质(即在track里可编辑的关键帧), 生成AnimationCurve, 同时不广播事件,还可以节省消耗。
比如在track里标记一些节点:
1
2
3
4
5
6
7
8
9
10
11
12
13[Serializable] [CustomStyle("TransforSignalmEmitter")] [Marker(TrackType.ANCHOR | TrackType.MARKER)] public class AnchorSignalEmitter : Marker { [SerializeField] Vector3 m_Position = Vector3.zero; public Vector3 position { get { return m_Position; } set { m_Position = value; } } }
然后再对应的track clip中生成曲线, 然后还可以在Track Clip中画出曲线:
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46const int sample_cnt = 20; Vector3[] sample_vtx = new Vector3[sample_cnt]; private void DrawAnchorAsset(AnchorAsset asset, double start, double end, int idx) { FetchKeys(asset, start, end); if (sample_vtx != null) { Handles.color = gizColors[idx % 3]; for (int i = 0; i < sample_cnt - 1; i++) { Handles.DrawLine(sample_vtx[i], sample_vtx[i + 1]); } } } private void FetchKeys(AnchorAsset asset, double start, double end) { double dur = end - start; double delta = dur / sample_cnt; if (asset.IsValid()) { for (int i = 0; i < sample_cnt; i++) { float time = (float)(start + delta * i); float x = asset.clip_pos[0].Evaluate(time); float y = asset.clip_pos[1].Evaluate(time); float z = asset.clip_pos[2].Evaluate(time); sample_vtx[i] = new Vector3(x, y, z); } } } private void CreateClips() { if (signals != null && signals.Count > 0) { AnimationCurve[] m_curves_pos=new AnimationCurve[3]; for (int i = 0; i < signals.Count; i++) { AnchorSignalEmitter sign = signals[i]; float time = (float)sign.time; m_curves_pos[0].AddKey(time, sign.position.x); m_curves_pos[1].AddKey(time, sign.position.y); m_curves_pos[2].AddKey(time, sign.position.z); } } }
最后效果如图所示, 不但在track里画出了曲线, 而且在场景里还可以画出轨迹:
对应的代码都上传到github上了, 欢迎点击查阅。
参考:
[1] 介绍Unity2019的timeline信号量
[2] 创建自定义Timeline 标记Marker
[3] Unity2019 uss自定义样式介绍
最后
以上就是优秀冬日最近收集整理的关于Timeline Signals - Marker标记的全部内容,更多相关Timeline内容请搜索靠谱客的其他文章。
发表评论 取消回复