Animancer Event

原文地址

结束事件(End Events) 在Animancer Lite中可用,但是除非您购买Animancer Pro,否则在运行时版本中将无法使用设置自定义结束时间"(custom end time) 和使用其他事件的功能。有关更多信息,请参见功能比较。

Animancer包含一个自定义事件系统,该系统与Unity自带的Animation Events有所不同。使用animation clip transitions机制,或者使用代码时,可以在inspector面板中配置事件。请注意,常规事件和结束事件之间有一些区别。

Events in Transitions

可以在inspector面板中,在transition条上,设置事件激活

  • 通过过渡预览窗口(Transition Preview Window) 可以看到事件在什么时间发生。
  • 过渡定义了淡入的方式,而不是淡出的方式(因为它由下一个过渡确定),因此Inspector时间轴显示仅使用默认值。如果结束时间小于动画长度,则会显示淡出在动画结束时结束。或者,如果结束时间大于长度,则会显示默认的0.25秒淡入。
  • 无论使用哪个字段输入值,“事件时间”字段始终会序列化为(normalized time)单位化时间。
  • 每个使用过渡机制播放特定动画的脚本,都将具有该动画自己的事件序列(event sequence),但是您也可以使用过渡资产(transition asset),过渡资产共享相同的事件,可以复用。

Controls

  • 可以一次选择并编辑一个事件,也可以单击时间轴旁边的折叠箭头,以一次显示所有事件
  • 在时间轴中双击以添加一个事件。
  • 选择事件后:
    • 左箭头和右箭头将事件时间向侧面微移一个像素。
    • 按住Shift键一次可移动10个像素。
    • 空格将事件时间舍入一位数字。例如:0.123将成为0.12并且0.999将成为1。

Events in Code

Animancer Events也可以使用如下代码进行配置:

 1// MeleeAttack.cs:
 2
 3[SerializeField] private AnimancerComponent _Animancer;
 4[SerializeField] private AnimationClip _Animation;
 5
 6public void Attack()
 7{
 8    // Play clears all events from all states (see Auto Clear below for details).
 9    var state = _Animancer.Play(_Animation);
10
11    // Call OnHitStart to activate the hit box when the animation passes 40% of its length.
12    state.Events.Add(0.4f, OnHitStart);
13
14    // Call OnHitEnd to deactivate the hit box when the animation passes 60% of its length.
15    state.Events.Add(0.6f, OnHitEnd);
16
17    // Return to Idle when the animation finishes.
18    state.Events.OnEnd = EnterIdleState;
19}

您可以使用Lambda表达式和匿名方法来定义要运行事件的代码,而无需创建另一个单独的方法:

 1void PlayAnimation(AnimancerComponent animancer, AnimationClip clip)
 2{
 3    var state = animancer.Play(clip);
 4
 5    // All of the following options are functionally identical:
 6
 7    // Lambda expression:
 8    state.Events.OnEnd = () =>
 9    {
10        Debug.Log(clip.name + " ended");
11    };
12
13    // One-line Lambda expression:
14    state.Events.OnEnd = () => Debug.Log(clip.name + " ended");
15
16    // Anonymous method:
17    state.Events.OnEnd = delegate()
18    {
19        Debug.Log(clip.name + " ended");
20    };
21}

AnimancerEvent.CurrentEventAnimancerEvent.CurrentState允许您访问当前正在触发的事件及其触发状态:

 1[SerializeField] private AnimancerComponent _Animancer;
 2[SerializeField] private AnimationClip _Animation;
 3
 4private void Awake()
 5{
 6    var state = _Animancer.Play(_Animation);
 7    state.Events.OnEnd = OnEnd;
 8}
 9
10public void OnEnd()
11{
12    Debug.Log(AnimancerEvent.CurrentState + " ended due to " + AnimancerEvent.CurrentEvent);
13}

Shared Event Sequences

播放一个特定过渡( particular Transition )的每个物体,都将共享相同的事件序列,这意味着一个物体对其进行的任何修改,也会影响其他物体。当使用由“过渡资产(transition asset)”机制提供的,在多个对象之间共享相同的过渡时,但仍需要为它们提供特定于每个物体的事件时,通常会发生这种情况。最简单的解决方法是在启动时仅Instantiate复制过渡资产:

1[SerializeField] private ClipTransition _Animation;
2
3private void Awake()
4{
5    _Animation = Instantiate(_Animation);
6    // Now you can modify the _Animation.Transition.Events as necessary.
7}

这为每个对象提供了过渡的单独副本,因此它们不再共享相同的事件序列,而是浪费了额外的处理时间和内存,因为所有其他过渡数据也被复制了。一个更有效的解决方案是为每个对象制作事件序列的自己的副本:

 1[SerializeField] private AnimancerComponent _Animancer;
 2[SerializeField] private ClipTransition _Animation;
 3
 4private AnimancerEvent.Sequence _Events;
 5
 6private void Awake()
 7{
 8    // Initialise your events by copying the sequence from the transition.
 9	// 只复制事件,不复制事件
10    _Events = new AnimancerEvent.Sequence(_Animation.Transition.Events);
11    // Now you can modify the _Events as necessary.
12}
13
14private void PlayAnimation()
15{
16    // Then when you play it, you just replace the transition events with your own:
17    var state = _Animancer.Play(_Animation);
18    state.Events = _Events;
19}

Hybrid Events

在动画剪辑的过渡条中,根据动画的实际表现去选某一个时刻去设置事件,通常是根据动画配置其时间的最佳方法,但是在代码中设置事件,通常更有助于创建可靠的脚本,并避免错误。幸运的是,您可以通过使用Transition来配置,然后把callback项留为空白,并改为在代码中指定,如下图及下代码所示。

1[SerializeField] private ClipState.Transition _GolfSwing;
2
3private void Awake()
4{
5    // Set the event named "Hit" to call the HitBall method when it is triggered.
6    _GolfSwing.Events.SetCallback("Hit", HitBall);
7}
8
9private void HitBall() { ... }

如果不使用事件的Name属性,您仍然可以按索引访问事件:

1_GolfSwing.Events.SetCallback(0, HitBall);

但这硬编码了一个假设,即在您想要的事件之前不会有其他事件,因此,如果您向其中添加更多事件,可能会在以后引起错误.

Event Names

C#代码中如果附加一个EventNamesAttribute属性,允许您一个名字的集合。这些名字可以事件使用。有一个下拉菜单代替通常的文本字段,EventNamesAttribute属性将使用下拉列表的方式,代替传统的文本输入框的方式指定事件名。

Without Attribute With Attribute

Clear Automatically

调用AnimancerComponent.Play或Stop方法,将会自动清除绑定在state上的事件。以确保您不必操心以前可能使用相同动画的其他脚本是否忘记释放。播放动画的每个脚本负责管理它预期发生的事情,而不必担心其他脚本的做法。

This usually means that only one state on each object will actually have events at any given time so AnimancerEvent.Sequences are recycled in an ObjectPool:

Accessing the AnimancerState.Events property will get an event sequence from the pool if it does not already have one. You can assign your own AnimancerEvent.Sequence to that property and it will then be removed from the state when the events are cleared (but since you made the sequence it will not be cleared or added to the pool). Transitions each have their own sequence which they assign whenever they are played.

例如,假定某角色有一个attack动作,且当动作播放完毕之后,会切换到idle动作。但在它的attack动作播放到一半的时候,有敌人对它进行攻击了,这时候需要打断attack动作,改为播放flinch动作,接受再切到idle动作。并且不需要关注attack动作的结束事件(end)。所以如果要再次播放attack动作。只需要重新给这个attack动作注册一次事件回调即可。但如果这个角色有一种特殊技能,需要产生连击效果(attack combo)。连击效果需要由若干组不同表现的attack动作,组成一个先后的序列。这时候就不希望还是按照原来那样子,到了动作结束后切换到idle动作。

就是说,强制允许动画/动作互相打断的规则通常非常重要,因此在“中断管理”示例中进行了介绍。

Garbage

animancer事件使用“委托”作为每个事件实际操作方法入口,这是非常方便的一种做法。当他们不在被使用时,会引起GC操作,这样会带来一些缺点。而且每当播放一次新的动画剪辑时,事件信息将会自动地被清除一遍。要重新使用事件,就要重新指定一遍。如果使用的是一个临时生成的delegate,就会产生大量的临时空间,导致GC的产生。

要解决这个问题,可以把delegate实例用一个成员变量缓存起来,如下所示:

 1[SerializeField] private AnimancerComponent _Animancer;
 2[SerializeField] private AnimationClip _Animation;
 3
 4private System.Action _OnAnimationEnd;
 5
 6void Awake()
 7{
 8    _OnAnimationEnd = () => Debug.Log("Animation ended");
 9}
10
11void PlayAnimation()
12{
13	// 如果是用代码设置的,而不是在state的trasition条上设置的。需要每次播放的时候都指定一下
14	// 当然,回调函数本身可以只创建一次。
15    _Animancer.Play(_Animation).Events.OnEnd = _OnAnimationEnd;
16}

过渡(transition)具有自己的事件,这些事件不会清除,并且在播放状态时会自动分配给状态,因此在界面面板上,将事件添加到它们,是另一种缓存形式,通常比使用单独的字段将委托存储在其中更方便:

可以使用AnimancerEvent.Sequence来控制管理事件,如下

 1// MeleeAttack.cs:
 2
 3[SerializeField] private AnimancerComponent _Animancer;
 4[SerializeField] private AnimationClip _Animation;
 5
 6private AnimancerEvent.Sequence _Events;
 7
 8private void Awake()
 9{
10    // Specifying the capacity is optional and it will still expand if necessary.
11    // But if the number of elements is known, specifying it is more efficient.
12    _Events = new AnimancerEvent.Sequence(2);
13    _Animation.Events.Add(0.4f, OnHitStart);
14    _Animation.Events.Add(0.6f, OnHitEnd);
15
16    // The End Event is not included in the capacity.
17    _Animation.Events.OnEnd = EnterIdleState;
18}
19
20public void Attack()
21{
22    var state = _Animation.Play(_Animation);
23    state.Events = _Events;
24}

您的事件序列将在播放其他动画时从状态中删除,但不会清除,因此您下次可以简单地重新分配它。

Looping

根据动画是否循环,事件的行为有所不同:

  • 非循环动画(Non-Looping animation ) 中,当其经过指定时间时,将在帧上触发一次。
  • 循环动画(Looping animation) 中,当经过指定时刻,将在帧上的每个循环中触发它们。
    • 如果动画播放的速度足够快,使得一帧内已经经过多次循环了,则事件将被触发适当的次数。如果要确保每个帧仅触发一次回调,则可以存储AnimancerPlayable.FrameID,并在每次调用方法时检查它是否已更改。
    • 循环事件必须在的范围内0 <= normalizedTime < 1才能正常运行。超出此范围的事件将在下一次的Update中,触发ArgumentOutOfRangeException异常。
    • AnimationEvent.AlmostOne是一个常量,其中包含float小于1的最大可能值,以供您想在循环结束时设置事件时使用。

其他详情

该系统还有其他一些值得一提的细节:

  • 使用AnimancerEvent.Invoke设置staticAnimancerEvent.CurrentEventAnimancerEvent.CurrentState,该方法允许任何对象访问事件的详细信息以及触发事件的状态,然后立即将其清除
  • 更改AnimancerState.Time可以防止该状态在该帧期间触发更多事件。
  • 该AnimancerState.Events序列不能由它自- 己的事件进行修改。
  • Animancer Events work with Mixers. Blend Trees will trigger regular Animation Events on all of the AnimationClips they contain, but this allows events to be placed on the MixerState itself so they get triggered according to the weighted average normalized time of the mixed states.
  • They also technically work with Controller States, though they are tied to the overall ControllerState and do not check what the Animator Controller is doing internally.
  • If you want to run your own code as part of the animation update, you can implement IUpdatable.

[[download_button]]