Timeline Signals - Marker标记
自从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属性来识别唯一标识通知。对于本文的示例,我们并不需要该功能,因此将使用默认的实现。
public 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接口的类。示例中,接收器会记录接收到通知的时间。
class 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实例现在可以接收发送到该输出的通知。
var 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,所以它可以添加到任意视图,只要使用一些逻辑就可以在准确时间发送通知。
var 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的语法来描述新样式,下面是样式的示例。
JumpSignalEmmiter
{
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。
[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:
public 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:
public 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里标记一些节点:
[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中画出曲线:
const 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自定义样式介绍