Timeline|02 Playable原理分析
有啥好处就不空谈了,直接上干货
架构
Playable有很多类型,但是总体上,由两部分组成
- Playable
- PlayableOutput
从名字就看得出来,他两是对应的,一个是输入,一个是输出
Playable
作为输入节点,一共支持如下类型
PlayableOutput
作为输出,类型就少很多了,因为从上面的输入来看,我们也只有三种类型的输出
PlayableGraph
输入输出管理器,可以看做是一个管理类,管理我们所有的输入(Playable)和输出(PlayableOutput)
如果使用可视化插件,我们可以发现,一个简单的Graph如下(一个输入,一个输出)
当然,Graph通常是不止两个节点的
简单使用
一个模型,挂在了Animator,没用Animation也没有AnimatorController
public class PlayableTest : MonoBehaviour
{
public AnimationClip clip;
private PlayableGraph graph;
void Start()
{
// 创建管理器
graph = PlayableGraph.Create("Test");
// 创建一个AnimationClip输入
var animationClipPlayable = AnimationClipPlayable.Create(graph, clip);
// 创建Animation输出
var animationPlayableOutput = AnimationPlayableOutput.Create(graph, "OutPut", GetComponent<Animator>());
// 连接输入输出
animationPlayableOutput.SetSourcePlayable(animationClipPlayable);
// 播放
graph.Play();
}
}
当然这么做可能有一点麻烦,官方提供了一个简单的工具类,可以直接播放一个方法
AnimationPlayableUtilities.PlayClip(GetComponent<Animator>(), clip, out PlayableGraph graph);
Graph是可以设置更新频率的
通过方法PlayableGraph.SetTimeUpdateMode
- DSPClock 基于DSP,与声音同步
- GameTime 基于Time.time,如果timeScale是0,那么也会停止
- UnscaledGameTime 同上,不过不会停止
- Manual 手动更新,需要自己调用PlayableGraph.Evaluate()
PlayableGraph Visualizer
一个可视化的插件,很好用,Add From Git Url : com.unity.playablegraph-visualizer
然后就可以在Window里打开了
BlendTree
混合树,在状态机里可以很方便的使用,可以通过AnimationMixerPlayable实现
public class PlayableTest : MonoBehaviour
{
public AnimationClip walk;
public AnimationClip run;
[Range(0,1)]
public float weight;
private PlayableGraph graph;
private AnimationMixerPlayable mixerPlayable;
void Start()
{
// 创建管理器
graph = PlayableGraph.Create("Test");
// 走路 输入
var walkPlayable = AnimationClipPlayable.Create(graph, walk);
// 跑步输入
var runPlayable = AnimationClipPlayable.Create(graph, run);
// 混合输入,目前需要的input是2
mixerPlayable = AnimationMixerPlayable.Create(graph, 2);
// 将2个Clip输入连接到混合节点
graph.Connect(walkPlayable, 0, mixerPlayable, 0);
graph.Connect(runPlayable, 0, mixerPlayable, 1);
// 创建Animation输出
var animationPlayableOutput = AnimationPlayableOutput.Create(graph, "OutPut", GetComponent<Animator>());
// 连接输入,这时候连接的是混合节点
animationPlayableOutput.SetSourcePlayable(mixerPlayable);
// 播放
graph.Play();
}
void Update()
{
mixerPlayable.SetInputWeight(0, 1 - weight);
mixerPlayable.SetInputWeight(1, weight);
}
}
Graph.Connect
这个API,稍微有一点抽象
第一个参数是连接对象,第三个参数是连接目标,这两个参数不难理解,就是你想把哪两个Playable连起来
第二个参数,指的是连接对象的输出端口
第四个参数,指的是连接目标的输入端口
也就是说,这个API的作用是,把连接对象的某一个输出端口,连接到目标的某一个输入端口上
Animation Layers
首先我们创建一个骨骼遮罩,只需要露个头
public class PlayableTest : MonoBehaviour
{
public AnimationClip eyeClose;
public AnimationClip run;
// 遮罩
public AvatarMask faceOnly;
[Range(0,1)]
public float runWeight;
[Range(0,1)]
public float simleWeight;
private PlayableGraph graph;
private AnimationLayerMixerPlayable animationLayerMixerPlayable;
void Start()
{
graph= PlayableGraph.Create("ChanPlayableGraph");
// 两个动画输入
var runClipPlayable = AnimationClipPlayable.Create(graph, run);
var eyeCloseClipPlayable = AnimationClipPlayable.Create(graph, eyeClose);
// Layer混合
animationLayerMixerPlayable = AnimationLayerMixerPlayable.Create(graph, 2);
graph.Connect(runClipPlayable, 0, animationLayerMixerPlayable, 0);//第0层Layer
graph.Connect(eyeCloseClipPlayable, 0, animationLayerMixerPlayable, 1);//第1层Layer
// 设置遮罩,这是给eyeClose设置的,目的是让这个动画只会影响头部
animationLayerMixerPlayable.SetLayerMaskFromAvatarMask(1, faceOnly);
// 输出
var animationOutputPlayable = AnimationPlayableOutput.Create(graph, "AnimationOutput", GetComponent<Animator>());
animationOutputPlayable.SetSourcePlayable(animationLayerMixerPlayable);
graph.Play();
}
void Update()
{
animationLayerMixerPlayable.SetInputWeight(0, runWeight);
animationLayerMixerPlayable.SetInputWeight(1, simleWeight);
}
}
刚开始因为两个Layer的权重都是0,所以啥动画也没有
Animation Controller
Playable同样支持和状态机混用
我们给模型一个状态机,并加入一个跑步的初始动画
public class PlayableTest : MonoBehaviour
{
public AnimatorController animatorController;
public AnimationClip runL;
public AnimationClip runR;
[Range(-1, 1)] public float directWeight;
private PlayableGraph graph;
private AnimationMixerPlayable animationMixerPlayable;
void Start()
{
graph= PlayableGraph.Create("ChanPlayableGraph");
// 2输入
var runL_Playable = AnimationClipPlayable.Create(graph, runL);
var runR_Playable = AnimationClipPlayable.Create(graph, runR);
// 1状态机输入
var animatorControllerPlayable = AnimatorControllerPlayable.Create(graph, animatorController);
// 混合节点
animationMixerPlayable = AnimationMixerPlayable.Create(graph, 3);
// 连接
graph.Connect(runL_Playable, 0, animationMixerPlayable, 0);
graph.Connect(animatorControllerPlayable, 0, animationMixerPlayable, 1);
graph.Connect(runR_Playable, 0, animationMixerPlayable, 2);
var animationOutputPlayable = AnimationPlayableOutput.Create(graph, "AnimationOutput", GetComponent<Animator>());
animationOutputPlayable.SetSourcePlayable(animationMixerPlayable);
graph.Play();
}
void Update()
{
var LWeight = directWeight < 0 ? -directWeight : 0;
var RWeight = directWeight > 0 ? directWeight : 0;
var runWeight = 1 - LWeight - RWeight;
animationMixerPlayable.SetInputWeight(0, LWeight);
animationMixerPlayable.SetInputWeight(1, runWeight);
animationMixerPlayable.SetInputWeight(2, RWeight);
}
}
进阶一点,还可以自己混合状态机
public class Test : MonoBehaviour
{
public AnimatorController controller;
public AnimationClip clip;
public float fade;
private PlayableGraph graph;
private AnimationMixerPlayable animationMixerPlayable;
private void Awake()
{
graph = PlayableGraph.Create("ChanPlayableGraph");
graph.SetTimeUpdateMode(DirectorUpdateMode.Manual);
// 1状态机输入
var animatorControllerPlayable = AnimatorControllerPlayable.Create(graph, controller);
// 混合节点
animationMixerPlayable = AnimationMixerPlayable.Create(graph, 2);
// 连接
graph.Connect(animatorControllerPlayable, 0, animationMixerPlayable, 0);
var animationOutputPlayable = AnimationPlayableOutput.Create(graph, "AnimationOutput", GetComponent<Animator>());
animationOutputPlayable.SetSourcePlayable(animationMixerPlayable);
animationMixerPlayable.SetInputWeight(0, 1);
graph.Play();
}
private async void PlayAnimation(AnimationClip clip, float fadeTime = 0.25f)
{
if (clip.length < fadeTime)
return;
// 创建Clip
var animationClipPlayable = AnimationClipPlayable.Create(graph, clip);
graph.Connect(animationClipPlayable, 0, animationMixerPlayable, 1);
// 淡入
var duration = 0f;
while (duration < fadeTime)
{
duration += Time.deltaTime;
if (duration > fadeTime)
duration = fadeTime;
var lerp = duration / fadeTime;
animationMixerPlayable.SetInputWeight(0, Mathf.Max(1 - lerp, 0));
animationMixerPlayable.SetInputWeight(1, lerp);
await UniTask.Yield();
}
// 减去淡入淡出时间后是否还有持续时间
var remainingTime = clip.length - 2 * fadeTime;
if (remainingTime > 0)
await UniTask.Delay((int)(remainingTime * 1000f));
// 淡出时间
var fadeOutTime = Mathf.Min(clip.length - fadeTime, fadeTime);
duration = 0f;
while (duration < fadeOutTime)
{
duration += Time.deltaTime;
if (duration > fadeOutTime)
duration = fadeOutTime;
var lerp = duration / fadeOutTime;
animationMixerPlayable.SetInputWeight(0, lerp);
animationMixerPlayable.SetInputWeight(1, Mathf.Max(1 - lerp, 0));
await UniTask.Yield();
}
animationMixerPlayable.SetInputWeight(0, 1);
graph.Disconnect(animationMixerPlayable, 1);
}
void Update()
{
graph.Evaluate(Time.deltaTime);
}
}
这样就可以使用PlayAnimation(clip, fade);
播放动画了