0. Playable

由于是基于Playable,所以需要一些前置知识: Playable
https://docs.unity.cn/cn/2020.3/Manual/Playables.html
总而言之,如果需要这个系统播放某些东西,需要准备一个PlayableGraph:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建该图和混合器,然后将它们绑定到 Animator。
playableGraph = PlayableGraph.Create();

var playableOutput = AnimationPlayableOutput.Create(playableGraph,"Animation", GetComponent<Animator>());

mixerPlayable = AnimationMixerPlayable.Create(playableGraph, 2);

playableOutput.SetSourcePlayable(mixerPlayable);

// 创建 AnimationClipPlayable 并将它们连接到混合器。
var clipPlayable0 = AnimationClipPlayable.Create(playableGraph, clip0);

var clipPlayable1 = AnimationClipPlayable.Create(playableGraph, clip1);

playableGraph.Connect(clipPlayable0, 0, mixerPlayable, 0);
playableGraph.Connect(clipPlayable1, 0, mixerPlayable, 1);

//播放该图。
playableGraph.Play();

1. AnimancerComponent概览

基本组件是AnimancerComponent
内部有个AnimancerPlayable,另外依赖Animantor

初始化:

1
2
3
4
5
public void InitializePlayable()
{
_Playable = AnimancerPlayable.Create();
_Playable.CreateOutput(_Animator, this);
}

此处做了下绑定,即把内部的AnimancerPlayable的输出绑定给animator
另外这里Create的实现:

1
return ScriptPlayable<AnimancerPlayable>.Create(graph, Template, 2).GetBehaviour();

此处传递了一个模板Template,然后Unity内部会回调到OnPlayableCreate:

1
2
3
4
5
6
7
8
9
10
11
public override void OnPlayableCreate(Playable playable)
{
_RootPlayable = playable;
_Graph = playable.GetGraph();

_PostUpdatables = new Key.KeyedList<IUpdatable>();
_PreUpdatables = new Key.KeyedList<IUpdatable>();
_PostUpdate = PostUpdate.Create(this);
Layers = new LayerList(this, out _LayerMixer);
States = new StateDictionary(this);
}

销毁的时候有一句对GC细节:(可以参考:https://www.cnblogs.com/huangxincheng/p/12811291.html)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public override void OnPlayableDestroy(Playable playable)
{
var previous = Current;
Current = this;

DisposeAll();
GC.SuppressFinalize(this);
// No need to destroy every layer and state individually because destroying the graph will do so anyway.
}

~AnimancerPlayable() => DisposeAll();
```
即禁用析构。原因也很简单,这里手动DisposeAll了,一旦再触发析构,就DisposeAll了两次



内部包含两个重要属性,即存放了所有State和Layer
```c#
public AnimancerPlayable.StateDictionary States => Playable.States;
public AnimancerPlayable.LayerList Layers => Playable.Layers;

核心接口Play:

1
2
public AnimancerState Play(AnimationClip clip)
=> Playable.Play(States.GetOrCreate(clip));

这里会根据clip构造一个内部状态State,Play之。
不过这里内部是将layer里面的其他动画全部停掉,一般还是会用带fadeDuration的:

1
2
3
4
5
6
7
8
9
public AnimancerState Play(ITransition transition, float fadeDuration, FadeMode mode = default)
=> Playable.Play(transition, fadeDuration, mode);
public AnimancerState Play(ITransition transition, float fadeDuration, FadeMode mode = default)
{
var state = States.GetOrCreate(transition);
state = Play(state, fadeDuration, mode);
transition.Apply(state);
return state;
}

此处会根据不同的Itransition构造不同的state,然后让state Play,最后Apply一下。

更新位于PrepareFrame(Playable接口),内部调用了UpdateAll。
另外这里更新是需要申请的,并非自动的(RequirePreUpdate)。

AnimancerPlayable会维护一个由AnimancerNode组成的树,对应Graph
(AnimancerState/Layer都是继承自AnimancerNode)
Play的时候会将节点加进去,并且申请Update
这样Update的时候就会调到AnimancerState 的Update了

常见State:

  • ClipState : 动画片段
  • MixerState : BlendTree
  • Layer : Layer

一些继承关系:

  • ClipTransition : AnimancerTransition,
  • LinearMixerTransition : MixerTransition<LinearMixerState, float>
  • ClipState : AnimancerState : AnimancerNode
  • LinearMixerState : MixerState :MixerState : ManualMixerState :MixerState :AnimancerState :AnimancerNode
  • AnimancerLayer : AnimancerNode

2. 一些议题

1. mode有何不同

fademode通常需要处理两件事:

  1. duration
  2. weight

即在duration期间内,从A动画过渡到B动画。一般简单的处理就是weight A从0变到1,B从0变到1。
此时FixedSpeed 和FixedDuration是一样的效果。

但是如果一开始A的weight就不是1,而是0.5,那么FixedSpeed会导致duration变短。
实现位于AniamncerLayer的 EvaluateFadeMode中。

另外比较特别的是FadeFromStart,后面会提到:

This can be useful when you want to repeat an action while the previous animation is still fading out. For example, if you play an ‘Attack’ animation, it ends and starts fading back to ‘Idle’, and while it is doing so you want to start another ‘Attack’. The previous ‘Attack’ can’t simply snap back to the start, so you can use this method to create a second ‘Attack’ state to fade in while the old one fades out.

2.layer如何处理

这里layer的数据结构表达就是AnimancerLayer,本质上也就是一个AniamncerNode,故可以嵌套组合成树结构。
使用方式也很简单,直接获取到Layer play即可:animancer.Layers[1].Play(clip);
这里默认会初始化4个layer。

3. Mixer(BlendTree)如何处理

Mixer都在MixerStates下面,这里的State会复写ForceRecalculateWeights接口计算权重。
https://docs.unity3d.com/cn/current/Manual/BlendTree-2DBlending.html

4. 动画重入如何处理

有个特殊的FadeMode:FromStart
对于Animancer来说,播放动画都会构造一个State来维护运行时数据。
那么问题来了,对于同一个动画片段如果重入,State是一个不?

1
2
3
4
5
6
7
8
9
10
11
12
13
public AnimancerState GetOrCreate(ITransition transition)
{
var key = transition.Key;
if (!TryGet(key, out var state))
{
state = transition.CreateState();
state.SetRoot(Root);
state._Key = key;
Register(state);
}

return state;
}

可以看到这里是用Transition的Key来做Dictionary的Key,如果Key相同,那么State就是一个。
而对于ClipTransition来说:

1
public override object Key => _Clip;

这里的Key就是Clip。
那么,可以认为,如果不同ClipTransition配置了相同的clip,理论上State是不同的(AniamncerClip不同)。
如果是重入同一个ClipTransition,则State应该相同。

那么如果不断Play同一个ClipTransitoin。
FadeFrontStart,此时会尝试重新构造(或者找到一个weight==0)的state。
否则由于找到的是weight不为零的state,之前播放的state权重会发生变动。

5. 关于Humanoid的特殊处理

初始化playable的时候会有这一句:

1
2
3
var isHumanoid = animator.isHuman;
// Generic Rigs get better performance by keeping children connected but Humanoids don't.
KeepChildrenConnected = !isHumanoid;

如果是非Humanoid,会KeepChildren Connected来优化:

Humanoid Rigs default this value to false so that playables will be disconnected from the graph while they are at 0 weight which stops it from evaluating them every frame. Generic Rigs default this value to true because they do not always animate the same standard set of values so every connection change has a higher performance cost than with Humanoid Rigs which is generally more significant than the gains for having fewer playables connected at a time

humanoid playable weight==0则会断开graph,避免每帧evaluate。但是非人形骨骼会尝试保留,即使权重==0。
推测非人形骨骼比如开门,开窗动画会不断重入某个固定动画,所以会一直尝试暴露。
而人形骨骼动画数量多且杂,尝试保留会导致graph太大。

实现位于AnimancerNode的ApplyWeight中

1
2
3
4
5
6
7
8
9
10
11
12
public void ApplyWeight()
{

if (!parent.KeepChildrenConnected)
{
if (_Weight == 0)
{
DisconnectFromGraph();
return;
}
}
}

即如果是人形骨骼,一旦权重为0,就断开连接

6. 一些内置数据结构

Animancer内含一组自定义的Collection:
KeyedList :带key的List,方便删除item而无需遍历整个list
LazyStack : A simple stack implementation that tracks an active index without actually adding or removing objects
ObjectPool : …

LazyStack增减index的时候并非直接暴力AddRemove,而是先仅调整index
KeyedList用在 Updateable注册上,估计是因为增减会比较频繁,所以用此方式加速
(之所以不用Dictionary盲猜是因为foreach会有GC。。。)

7. 如何从动画片段某个位置直接开始播(seek)

state.Time = x

3. 其他一些讨论

诸如行为树,关卡蓝图以及AnimatorController都是比较流行的低代码工具。
好处是方便,做体量小的游戏速度快。

但是Unity状态机实际上并不适用于强动作类型的游戏,核心矛盾在于:

动作游戏动作状态之间的随意切换是普遍现象,特定动作到动作的跳转才是特例

所以会出现Unity状态机但凡动作跳转复杂一点,就会出现:

  • 无法维护
  • 无法重构
  • 调试困难
    等等问题

(事实上,某PVP动作游戏光受击动画片段都有上千个,不说策划连线能类似,Unity打开包含这么多片段的状态机能直接卡到爆)