Playable API

参考链接:


前言

Playable API的由来
新版的Unity鼓励大家用Animator组件(Mecanim),但依然有很多人在使用Legacy动画系统Animation组件。因为Animation组件对于程序来说比较直观,但使用Animation组件,就意味着不可用使用Mecanim动画系统的一些高级功能,例如:动画重定向、Blend Tree等。

所以,设计Playable API的一个目的就是为了代替Legacy动画系统,同时继续使用动画重定向、BlendTree和AvatarMask等功能
,通过Playable API,可以弃用Unity的状态机系统,定制适配于自己项目的动画系统,但是又保留Unity的高级动画特性。

再者,Playable API可以更直接的访问底层动画系统的接口。
最后,我们可以通过Playable来扩展Timeline的功能。

总结下来,其核心功能如下:
在这里插入图片描述

在Unity底层,驱动Playable Graph的实际上依然是Animator组件,所以在使用时需要你传一个Animator组件给Playable Graph。但是你完全可以像使用Animation组件一样使用Playable。


什么是Playables API
其实就是一组API,它可以用于创建一个树形结构的图,这个图叫做PlayableGraph,如下图所示,可以用来存储数据信息(比如AnimationClip). Playables API支持动画、音频和脚本,提供了用脚本与动画和音频系统进行交互的能力:
在这里插入图片描述

Although the Playables API is currently limited to animation, audio, and scripts, it is a generic API that will eventually be used by video and other systems.


理解Playable与Playable API
首先要区分PlayablePlayable API的关系,Playable API就是Animator Controller底层实现的方式,相当于Unity底层关于动画的API,现在由Unity暴露了出来,这些Playable和PlayableGraph相关的API都属于PlayableAPI。而Playable就不同了,字面意思理解,Playable就是可以播放的东西,比如说,一个对AnimationClip的Wrapper,就是一个AnimationPlayable;在PlayableGraph的层面上理解,上面PlayableGraph图中的一个个小长方形,代表的就是Playable,意思是Graph里面的节点;而在代码层面上理解,Playable就是一个Struct,它遵循IPlayable接口,这里用Struct而不用Class是为了保证高效性,因为不会在堆上分配


Playable vs Animation
Unity本身提供了提供给了用户一套进行动画编辑的工具,也就是Animator对应的动画状态机系统(Mecanim),也就是说,Unity内部人员基于PlayableGraph和Playable开发了Mecanim这一套动画系统,而PlayableGraph不仅仅只可用于动画,它可以代表一系列数据的流动结构,包括动画、音频和脚本等,未来还有可能拓展到视频等内容。(These graphs represent a flow of data, indicating what each node produces and consumes. In addition, a single graph is not limited to a single system. A single graph may contain nodes for animation, audio, and scripts)

使用Playables API的优点

  • Runtime创建一个AnimationClip,使用在场景中的一个对象上
  • 简单播放一段动画,而不需要创建和使用AnimatorController,也不会有AnimatorController的相关消耗
  • 动态的Blend不同的AnimationClip,而且可以每帧都可以调整二者的权重
  • 可以在Runtime创建PlayableGraph,添加和删除playable nodes,非常灵活


关于Playable

Unity Manual里提到,Playable就是一个C#的struct,这个结构体,继承于IPlayable接口,同样,Playable Output也是一个C#的struct,继承于IPlayableOutput接口,用于表示PlayableGraph的output

Playable可以分为三种类型,Animation、Audio和Script类型,如下图所示,其实就是这些可播放的资源的Wrapper,注意这里的箭头不是继承关系,它只是范围的从属关系:
在这里插入图片描述



The PlayableGraph

PlayableGraph 定义了一系列的playable outputs,这些outputs是与GameObject或Component联系到一起的,PlayableGraph也记录了这些playable之间的连接关系。

如下图所示是一个PlayableGraph的样子,这里需要从右往左读,这里先不去寄希望于理解它的意思,现在只是了解一下,想要看到这个节点图,需要安装对应的playablegraph visualizer的插件。
在这里插入图片描述


怎么在Unity2020的版本里安装这个插件
顺便提一下这个事情,因为Unity太鸡贼了,它把这个插件在Package Manager里隐藏了,为什么隐藏,因为这个功能是Unity里一个人做的,但是这个人离职了,但是这个功能还没做完,所以这是一个还在测试中的,被搁置的功能,Unity就把它隐藏了。

但是要想手动安装它,也有解决办法,就是把下面这行加到你自己的manifest.json文件里就行

"com.unity.playablegraph-visualizer": "0.2.1-preview.3"


Playable Graph在Unity的生命周期里的位置

Playable Graph最终作用的对象会是存在于Scene里的一个Object,比如Scene里的Animator,所以它肯定也是有内部的执行逻辑和Update顺序的,具体文章参考这里(Remain),这边放不下了



ScriptPlayable and PlayableBehaviour

这是两个Unity提供的类,其接口和类声明如下:

// ScriptPlayable是一个泛型类, 继承于IPlayable, 所以ScriptPlayable对象属于Playable
// 这里的T需要是class, 并且继承于IPlayableBehaviour, 且必须有无参的构造函数
public struct ScriptPlayable<T> : IPlayable, IEquatable<ScriptPlayable<T>> where T : class, IPlayableBehaviour, new()
{
    public static ScriptPlayable<T> Null { get; }

    public static ScriptPlayable<T> Create(PlayableGraph graph, int inputCount = 0);
    public static ScriptPlayable<T> Create(PlayableGraph graph, T template, int inputCount = 0);
    public bool Equals(ScriptPlayable<T> other);
    public T GetBehaviour();
    public PlayableHandle GetHandle();

	// 提供ScriptPlayable转为Playable的隐式转换
    public static implicit operator Playable(ScriptPlayable<T> playable);
    // 提供Playable转为ScriptPlayable的显式转换
    public static explicit operator ScriptPlayable<T>(Playable playable);
}

// PlayableBehaviour是一个接口类, IPlayableBehaviour其实也是个接口类
[RequiredByNativeCode]
public abstract class PlayableBehaviour : IPlayableBehaviour, ICloneable
{
    public PlayableBehaviour();

    public virtual object Clone();// 遵守ICloneable接口必须实现的函数
    [Obsolete("OnBehaviourDelay is obsolete; use a custom ScriptPlayable to implement this feature", false)]
    public virtual void OnBehaviourDelay(Playable playable, FrameData info);
    public virtual void OnBehaviourPause(Playable playable, FrameData info);
    public virtual void OnBehaviourPlay(Playable playable, FrameData info);
    public virtual void OnGraphStart(Playable playable);
    public virtual void OnGraphStop(Playable playable);
    public virtual void OnPlayableCreate(Playable playable);
    public virtual void OnPlayableDestroy(Playable playable);
    public virtual void PrepareData(Playable playable, FrameData info);
    public virtual void PrepareFrame(Playable playable, FrameData info);
    public virtual void ProcessFrame(Playable playable, FrameData info, object playerData);
}

public interface IPlayableBehaviour
{
    [RequiredByNativeCode]
    void OnBehaviourPause(Playable playable, FrameData info);
    [RequiredByNativeCode]
    void OnBehaviourPlay(Playable playable, FrameData info);
    [RequiredByNativeCode]
    void OnGraphStart(Playable playable);
    [RequiredByNativeCode]
    void OnGraphStop(Playable playable);
    [RequiredByNativeCode]
    void OnPlayableCreate(Playable playable);
    [RequiredByNativeCode]
    void OnPlayableDestroy(Playable playable);
    [RequiredByNativeCode]
    void PrepareFrame(Playable playable, FrameData info);
    [RequiredByNativeCode]
    void ProcessFrame(Playable playable, FrameData info, object playerData);
}

可以看出来,要想创建一个ScriptPlayable,首先需要定义这个脚本Playable的行为模式,它的行为模式用PlayableBehaviour类的对象来表示,在创建完该对象之后,就可以创建实际的ScriptPlayable对象了。ScriptPlayable本质是一个PlayableBehaviour的Wrapper,一个ScriptPlayable,加上表示它行为的对象,组合起来就能得到完整的自定义Playable对象了。(不过为啥要搞两道工序? 直接定义一个ScriptPlayable,然后定义一堆虚函数不好么)

下面是创建自定义的PlayableBehaviour的方法:

public class MyCustomPlayableBehaviour : PlayableBehaviour 
{
	// Implementation of the custom playable behaviour 
	// Override PlayableBehaviour methods as needed 
}

// PlayableBehaviour的继承关系
public abstract class PlayableBehaviour : IPlayableBehaviour, ICloneable
{
	...
}

public interface IPlayableBehaviour
{
	...
}

namespace System
{
    public interface ICloneable
    {
        object Clone();
    }
}

接下来创建对应的ScriptPlayable的方法,好像一定要用统一的Create接口

graph = PlayableGraph.Create("Graph");


//1. 需要使用ScriptPlayable<T>.Create的方法来统一创建ScriptPlayable
//2. 创建的时候需要指定对应的PlayableGraph

// 写法一, 第二行会调用myPlayableBehaviour的Clone函数, 把它Copy一份传给ScriptPlayable
// 这也是为什么ScriptPlayable<T>的T必须要继承于ICloneable的原因
MyCustomPlayableBehaviour myPlayableBehaviour = new MyCustomPlayableBehaviour();
ScriptPlayable<MyCustomPlayableBehaviour> myScriptPlayable1 = ScriptPlayable<MyCustomPlayableBehaviour>.Create(graph, myPlayableBehaviour);


// 写法二, 省略了MyCustomPlayableBehaviour对象的创建, 里面会调用其默认构造函数创建对象
// 这也是为什么ScriptPlayable<T>的T必须要有无参的构造函数的原因
ScriptPlayable<MyCustomPlayableBehaviour> myScriptPlayable2 = ScriptPlayable<MyCustomPlayableBehaviour>.Create(graph);

使用对应的接口,可以从ScriptPlayable里重新获取其PlayableBehaviour对象:

ScriptPlayable<T> .GetBehaviour() 

PlayableBehaviour接口分析

下面可以仔细研究PlayableBehaviour类里的接口,因为ScriptPlayable本质上是PlayableBehaviour的Wrapper,PlayableBehaviour里有哪些接口,也说明了我们可以用ScriptPlayable做哪些事情:

namespace UnityEngine.Playables
{
    // 此类代表一个ScriptPlayable拥有的行为
    [RequiredByNativeCode]
    public abstract class PlayableBehaviour : IPlayableBehaviour, ICloneable
    {
        public PlayableBehaviour();

        public virtual object Clone();
        
        // 由于对应的Playable是在PlayableGraph里执行的, 这里提供了一大堆回调函数
        
        // 当ScriptPlayable被暂停或播放时的回调
        public virtual void OnBehaviourPause(Playable playable, FrameData info);
        public virtual void OnBehaviourPlay(Playable playable, FrameData info);

		// PlayableGraph开始或暂停的回调
        public virtual void OnGraphStart(Playable playable);
        public virtual void OnGraphStop(Playable playable);
        
	      // 当ScriptPlayable被摧毁或创建时的回调
        public virtual void OnPlayableCreate(Playable playable);
        public virtual void OnPlayableDestroy(Playable playable);

		// 类似于Update函数
        public virtual void PrepareData(Playable playable, FrameData info);
        public virtual void PrepareFrame(Playable playable, FrameData info);
        public virtual void ProcessFrame(Playable playable, FrameData info, object playerData);
    }
}

总结一下,ScriptPlayable其实很简单,它是一种类似于MonoBehaviour的东西,无非MonoBehaviour是在游戏周期里执行,而ScriptPlayable是在PlayableGraph的执行周期里而已,它提供了以下回调函数:

  • PlayableGraph暂停和播放的回调
  • PlayableGraph创建和被销毁的回调
  • ScriptPlayable暂停和播放的回调
  • PrepareData函数,感觉可以理解为PreUpdate1函数,应该是每帧执行的
  • PrepareFrame函数,感觉可以理解为PreUpdate2函数
  • ProcessFrame函数,感觉可以理解为Update函数

PlayableBehaviour.SetTraversalMode
关于PlayableBehaviour,有一个enum叫PlayableTraversalMode,如下所示:

namespace UnityEngine.Playables
{
    // Traversal mode for Playables.
    public enum PlayableTraversalMode
    {
        // Summary: Causes the Playable to prepare and process it's inputs when demanded by an output.
        // 当一个编程Playable是mix模式时,会在它被一个output需要的时候,让它去prepare和process它的inputs
        Mix = 0,
        // Summary:
        //     Causes the Playable to act as a passthrough for PrepareFrame and ProcessFrame.
        //     If the PlayableOutput being processed is connected to the n-th input port of
        //     the Playable, the Playable only propagates the n-th output port. Use this enum
        //     value in conjunction with PlayableOutput SetSourceOutputPort.
        // 当一个编程Playable是passthrough模式时,它好像只会把第n个input接口里传递的东西直接处理传给PlayableOutput
        // 其他的就不处理了,这里的n是由SetSourceOutputPort来指定的
        Passthrough = 1
    }
}

FrameData

前面的ScriptPlayable动态调用的函数里都有个参数是FrameData,它是一个结构体,作为每帧接受的信息:

public abstract class PlayableBehaviour : IPlayableBehaviour, ICloneable
{
    public PlayableBehaviour();
    ...
    public virtual void PrepareData(Playable playable, FrameData info);
	public virtual void PrepareFrame(Playable playable, FrameData info);
	public virtual void ProcessFrame(Playable playable, FrameData info, object playerData);
}

FrameData其实就是个Data类,里面有一堆数据,没有任何函数,而且FrameData里的数据全部是只读的:

  • frameId: 对应帧的id
  • deltaTime
  • weight: 对应Playable的权重
  • effectiveWeight: The accumulated weight of the Playable during the PlayableGraph traversal.(对应ScriptPlayable的有效权重(意思是在[0,1]之间?)
  • effectiveParentSpeed: graph遍历阶段,到该Playable的parent Playable时的累积速度(accumulated speed)
  • effectiveSpeed: 播放到当前Playable的累积速度
  • evaluationType: 读取PlayableGraph的PrepareFrame函数被调用的方式,有EvaluatePlayback两种
  • seekOccurred: Indicates that the local time was explicitly set.
  • timeHeld: bool型变量,表示local time不会再增长,因为它播放完了,而且extrapolation mode设置为了Hold,应该就是播放一次的意思
  • timeLooped: bool型变量,表示local time wrapped,而且extrapolation mode设置为了Loop,应该是循环播放的意思
  • output: 类型为PlayableOutput (The PlayableOutput that initiated this graph traversal.)
  • effectivePlayState: 播放到当前Playable的累积播放状态(play state)

关于EvaluationType
FrameData里可以通过evaluationType读取该Playable对应PlayableGraph的Update方式,有两种:

  • Evaluate:graph会以调用PlayableGraph.Evaluate([DefaultValue(“0”)] float deltaTime)函数的方式进行Update,应该是脚本自行控制
  • Playback:graph会在runtime调用PlayableGraph.Play函数之后开始运行,然后会随Update或FixedUpdate函数的频率进行Update

每一个Playable都会有PrepareFrame函数
这里的PrepareFrame函数不只是ScriptPlayable有,当PlayableGraph Play的时候,会遍历到每一个PlayableOutput,PlayableOutput都各自连接到了Scene里的GameObject上。

在遍历过程中,每个Playable的PrepareFrame函数都会被调用,这个函数是为了"Prepare itself for the next evaluation",在这个阶段里,Playable可以修改其子节点(比如说增减删改input节点)。在PrepareFrame阶段完成后,所有的PlayableOutputs就回来接管这个过程,来处理这些准备好的数据。



一些例子

例一:使用Playable API播放Idle动画

举个例子,使用Playable APIs,代替老版的Legacy的Animation组件,但是又支持BlendTree、动画重定向等功能,后面这些功能Animator Controller是支持的,而Animation Component是不支持的。

先在场景里面放好一个模型,然后给他挂载一个Animator组件,指定其Avatar,但是不给Animator指定原本有的Animator Controller文件,如下图所示:
在这里插入图片描述

然后写一个脚本Test.cs:

using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;

// 确保同物体上有Animator组件,但是不需要给该Animator赋值Animator Controller
[RequireComponent(typeof(Animator))]
public class Test : MonoBehaviour
{
    public AnimationClip clip;//注意这里要像使用Animation组件一样,从外部指定AnimationClip进来
    PlayableGraph playableGraph;

    void Start()
    {
        // 创建一个PlayableGraph
        playableGraph = PlayableGraph.Create("PlayAnimationSample");
        // 基于playableGraph创建一个动画类型的Output节点,名字是Animation,目标对象是物体上的Animator组件
        AnimationPlayableOutput playableOutput = AnimationPlayableOutput.Create(playableGraph, "Animation", GetComponent<Animator>());
        // 基于本组件已经索引好的AnimationClip创建一个AnimationClipPlayable
        AnimationClipPlayable clipPlayable = AnimationClipPlayable.Create(playableGraph, clip);
        // 将playable连接到output
        playableOutput.SetSourcePlayable(clipPlayable);
        // 播放这个graph
        playableGraph.Play();
    }

    void OnDisable()
    {
        // 销毁所有的Playables和PlayableOutputs
        playableGraph.Destroy();
    }
}

最后给模型挂载这个脚本,然后像用Animation组件一样拖进去对应的AnimationClip后,点击Play,就可以看到角色在播放拖拽进去的clip了:
在这里插入图片描述

回顾一下这里的操作,这里主要是五步:

  • 创建自己的动画状态机,名字叫做PlayableGraph
  • 建立输出,类型为Output Playable,动画模块需要传入对应的Animator(实际上底层好像还是用Animator做的)
  • 建立输入,类型为AnimationClip
  • 把输入跟输出连接起来
  • 状态机点击Play

上面的代码还有一个简化的版本,如下所示:

using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;

[RequireComponent(typeof(Animator))]
public class PlayAnimationUtilitiesSample : MonoBehaviour
{
    public AnimationClip clip;
    PlayableGraph playableGraph;
    void Start()
    {
	    // 迅速创建一个graph,省却了之前创建输入的AnimationClipPlayable和输出的AnimationPlayableOutput,以及二者的绑定过程
        AnimationPlayableUtilities.PlayClip(GetComponent<Animator>(), clip, out playableGraph);
    }

    void OnDisable()
    {
        // Destroys all Playables and Outputs created by the graph.
        playableGraph.Destroy();
    }
}


例二:用ScriptPlayable动态调整AnimationMixerPlayable中子Playable的权重

例一播放了一个动画,这里试试播放两个动画的Blending效果。AnimationMixerPlayable相当于一个容器,用来存放多个AnimationClipPlayable,再加一个ScriptPlayable,用于在每帧动态调整两个AnimationClip的权重。

代码如下:

using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;

// 创建一个自定义的PlayableBehaviour类
public class BlenderPlayableBehaviour : PlayableBehaviour
{
	// 存了一个mixerPlayable的引用, 会在Update函数(PrepareFrame)里动态更新其权重
    public AnimationMixerPlayable mixerPlayable;// 相当于BlendTree
	
	// 只override这个函数
    public override void PrepareFrame(Playable playable, FrameData info)
    {
        // blend值会随着时间变化,逐渐在[0,1]范围内来回取值
        float blend = Mathf.PingPong((float)playable.GetTime(), 1.0f);

        mixerPlayable.SetInputWeight(0, blend);//0号clip 权重为blend
        mixerPlayable.SetInputWeight(1, 1.0f - blend);//1号clip 权重为1 - blend

        base.PrepareFrame(playable, info);//调用基类的PrepareFrame函数
    }
}

public class PlayableBehaviourSample : MonoBehaviour
{
    PlayableGraph m_Graph;
    public AnimationClip clipA;
    public AnimationClip clipB;

    void Start()
    {
        m_Graph = PlayableGraph.Create();

        var animOutput = AnimationPlayableOutput.Create(m_Graph, "AnimationOutput", GetComponent<Animator>());

		// 创建一个AnimationMixerPlayable
        var mixerPlayable = AnimationMixerPlayable.Create(m_Graph, 2, false);
		
		// 创建俩AnimationClipPlayable
        var clipPlayableA = AnimationClipPlayable.Create(m_Graph, clipA);
        var clipPlayableB = AnimationClipPlayable.Create(m_Graph, clipB);

		// 根据上面的BlenderPlayableBehaviour类创建对应的wrapper, 即blenderPlayable 
        ScriptPlayable<BlenderPlayableBehaviour> blenderPlayable = ScriptPlayable<BlenderPlayableBehaviour>.Create(m_Graph, 1);
        // 是个照顾对应的mixerPlayble
        blenderPlayable.GetBehaviour().mixerPlayable = mixerPlayable;//注意一下写法,需要调用GetBehaviour()

		// 连接节点, 注意mixerPlayable连接的是scriptPlayable
        m_Graph.Connect(clipPlayableA, 0, mixerPlayable, 0);
        m_Graph.Connect(clipPlayableB, 0, mixerPlayable, 1);
        m_Graph.Connect(mixerPlayable, 0, blenderPlayable, 0);

		// scriptPlayable连接到最终的Output上
        animOutput.SetSourcePlayable(blenderPlayable);
        animOutput.SetSourceInputPort(0);

        // Play the graph.
        m_Graph.Play();
    }

    private void OnDestroy()
    {
        // Destroy the graph once done with it.
        m_Graph.Destroy();
    }
}

如下所示,是这段代码对应的PlayableGraph:
在这里插入图片描述



例三:使用AnimationLayerMixerPlayable

前面的AnimationMixerPlayable是对两个AnimationClip进行混合,而这里就是把两个AnimationLayer进行混合,示例代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Audio;
using UnityEngine.Playables;


/// <summary>
/// 这篇程序可以参考官方 https://docs.unity3d.com/ScriptReference/Playables.PlayableGraph.Connect.html
/// </summary>
[RequireComponent(typeof(Animator))]
public class Playable_RotateMoveCube : MonoBehaviour
{
	// 使用两个AnimationPlayable,混合得到MixerPlayable
    public AnimationClip animClipA;
    public AnimationClip animClipB;

    private PlayableGraph graph;
    private AnimationLayerMixerPlayable mixer;
    private AnimationPlayableOutput output;


    void Start()
    {
        graph = PlayableGraph.Create("RotateMoveCube");
        output = AnimationPlayableOutput.Create(graph, "Animation", GetComponent<Animator>());
        // inputCount 可以接几个playable
        mixer = AnimationLayerMixerPlayable.Create(graph, 3);
        output.SetSourcePlayable(mixer);

        AnimationClipPlayable clipPlayA = AnimationClipPlayable.Create(graph, animClipA);
        AnimationClipPlayable clipPlayB = AnimationClipPlayable.Create(graph, animClipB);
        // sourceOutputPort 默认为0
        graph.Connect(clipPlayA, 0, mixer, 0);
        graph.Connect(clipPlayB, 0, mixer, 1);
        // weight填0.5f只会有一半效果,和我理解的a + b + c = 1不同
        mixer.SetInputWeight(0, 1f);
        mixer.SetInputWeight(1, 1f);
        graph.Play();
    }

    public void ClickClose()
    {
        // ...这里暂无判空等检测
        graph.Disconnect(mixer, 0);
        graph.Disconnect(mixer, 1);

        graph.DestroyPlayable(mixer);
        graph.DestroyOutput(output);

        // cube位置会停在销毁的时刻
    }

    public void ClickStopB()
    {
        // ...这里暂无判空等检测
        mixer.SetInputWeight(1, 0f);

        //graph.Disconnect(mixer, 1);
    }

    void OnDisable()
    {
        graph.Destroy();
    }

}

AnimationPlayableMixer和AnimationLayerPlayableMixer
看名字应该大概能知道,AnimationPlayableMixer是用于混合多个AnimationClip,感觉像是Blend Tree,而AnimationLayerPlayableMixer是混合多个AnimationLayer的,其接口函数如下:
在这里插入图片描述



什么是Animation Pose

插件提供的Graph View下,赫然看到这个黄色的Animation Pose节点,但是没有官方解释这个事情,如下图所示:
在这里插入图片描述

https://forum.unity.com/threads/what-is-animation-pose-playable.895379/上做了一些相关的介绍,大概意思是说,AnimationPose是Unity的Native Code里的一种Playable,它记录的是人物的Pose信息,这种Playable没有被暴露出来,有人说它是用于动画A向B转换到一半的时候,突然要转向C,这个时候就用Animation Pose记录A到B的中间转态的姿势的,也有人说是用于记录Default Animation Pose的,这样当有的骨头不识别transform时,就播放默认的Animation Pose,总之,应该是记录Animation的姿势的节点,但是没有被暴露出来



后面的内容就是Playable API与Unity提供的多线程结合的部分了,我这一块也没有完全理解,现在只是留下一些记录和整理

Animation C# Jobs给Playable增加的API

参考链接:https://blogs.unity3d.com/2018/08/27/animation-c-jobs/

Unity有一个多线程的Job System,Playable API也会用到多线程,相关动画部分的代码主要是三个struct:

  • AnimationScriptPlayable:注意,它并不继承于ScriptPlayable,而是直接继承于IPlayable和IAnimationJobPlayable接口
  • IAnimationJob:一个简单的接口类
  • AnimationStream:一个简单的结构体,包含了一些动画数据

AnimationScriptPlayable和IAnimationJob

AnimationScriptPlayable也是一个Playable,可以作为节点放在graph里,感觉AnimationScriptPlayableScriptPlayable没有太大关系,接口如下:

namespace UnityEngine.Animations
{
	// AnimationScriptPlayable里基本啥也没有, 其实只有包含的AnimationJob的Set和Get函数
    public struct AnimationScriptPlayable : IAnimationJobPlayable, IPlayable, IEquatable<AnimationScriptPlayable>
    {
        public static AnimationScriptPlayable Null { get; }

        public static AnimationScriptPlayable Create<T>(PlayableGraph graph, T jobData, int inputCount = 0) where T : struct, IAnimationJob;
        public bool Equals(AnimationScriptPlayable other);
        public PlayableHandle GetHandle();
        public T GetJobData<T>() where T : struct, IAnimationJob;
        public bool GetProcessInputs();
        public void SetJobData<T>(T jobData) where T : struct, IAnimationJob;
        public void SetProcessInputs(bool value);

		// 提供Playable与AnimationScriptPlayable之间的相互转化
        public static implicit operator Playable(AnimationScriptPlayable playable);
        public static explicit operator AnimationScriptPlayable(Playable playable);
    }

	
    public interface IAnimationJobPlayable : IPlayable
    {
        T GetJobData<T>() where T : struct, IAnimationJob;
        void SetJobData<T>(T jobData) where T : struct, IAnimationJob;
    }

    public interface IAnimationJob
    {
        void ProcessAnimation(AnimationStream stream);
        void ProcessRootMotion(AnimationStream stream);
    }
}

可以看出,AnimationScriptPlayable其实是一个AnimatyionJob的Wrapper,它的作用相当于graph和job的中间接头人(proxy)。这里的Job,一般是由用户自定义的struct,继承于IAnimationJob接口,也就是实现了ProcessAnimationProcessRootMotion俩函数的任意类的对象都可以作为Job。


public void SetProcessInputs(bool value)
AnimationScriptPlayable里都有这个接口,默认情况下,AnimationScriptPlayable的所有Inputs都会被处理

By default, all the AnimationScriptPlayable inputs are processed. In the case of only one input (a.k.a. a post-process job), this stream will contain the result of the processed input. In the case of multiple inputs (a.k.a. a mix job), it’s preferable to process the inputs manually. To do so, the method AnimationScriptPlayable.SetProcessInputs(bool) will enable or disable the processing passes on the inputs. To trigger the processing of an input and acquire the resulting stream in manual mode, call AnimationStream.GetInputStream().


例一:创建Animation Job,再创建AnimationScriptPlayable

就是一个API的写法说明,创建了一个AnimationScriptPlayable对象,该对象包含了一个animation job:

using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;
using UnityEngine.Experimental.Animations; 

// 创建自定义类, 实现继承的两个接口, 其实啥也没干
public struct AnimationJob : IAnimationJob
{
    public void ProcessRootMotion(AnimationStream stream)
    {
    }
 
    public void ProcessAnimation(AnimationStream stream)
    {
    }
}
 
[RequireComponent(typeof(Animator))]
public class AnimationScriptExample : MonoBehaviour
{
    PlayableGraph m_Graph;
    AnimationScriptPlayable m_ScriptPlayable;
 
    void OnEnable()
    {
        m_Graph = PlayableGraph.Create("AnimationScriptExample");
 
 		// new一个job
        var animationJob = new AnimationJob();
        // 创建AnimationScriptPlayable时需要传入一个Job对象
        m_ScriptPlayable = AnimationScriptPlayable.Create(m_Graph, animationJob);
 
        // Create the output and link it to the playable.
        var output = AnimationPlayableOutput.Create(m_Graph, "Output", GetComponent<Animator>());
        output.SetSourcePlayable(m_ScriptPlayable);
    }
 
    void OnDisable()
    {
        m_Graph.Destroy();
    }
}

例二:使用AnimationScriptPlayable代替MixerPlayable

参考链接:https://github.com/Unity-Technologies/animation-jobs-samples
如下图所示,目的是为了替代原本的MixerPlayable,把两个AnimationClipPlayable直接进行Blend:
在这里插入图片描述
代码如下:

// ============== 先编写一个AnimationJob类, 负责动态的Blend动画 =============
using Unity.Collections;
using UnityEngine;

#if UNITY_2019_3_OR_NEWER
using UnityEngine.Animations;
#else
using UnityEngine.Experimental.Animations;
#endif

// 自定义一个MixerJob, 代替AnimationMixerPlayable
// 注意, 这里跟直接使用AnimationMixerPlayable实现机理不同的是
// - AnimationMixerPlayable是通过调整多个输入的AnimationClipPlayable的权重, 然后Unity内部去进行Blend的
// - 这里的MixerJob的处理方式更底层, 它是直接去修改Joints的Transform, 由于内部是手动Set每个Joint的Transform的
// 所以连接MixerJob的AnimationScriptPlayable的多个AnimationClipPlayable, 其权重值无所谓是多少
public struct MixerJob : IAnimationJob
{
    // 这里的Mixer做的是很具体的东西, 它会把所有的模型上的Joints的Transform根据对应的权重进行混合
    public NativeArray<TransformStreamHandle> handles;      // 每个Handle对应一个Joint的Transform
    public NativeArray<float> boneWeights;                  
    public float weight;

    public void ProcessRootMotion(AnimationStream stream)
    {
        Debug.Log("ProcessRootMotion");
        AnimationStream streamA = stream.GetInputStream(0);
        AnimationStream streamB = stream.GetInputStream(1);

        // 把两个动画对应Stream的velocity和angularVelocity进行混合(不过这玩意儿跟RootMotion有何关系)
        // 当weight为0时, 全部采用Input 0对应Stream的数据
        // 当weight为1时, 全部采用Input 1对应Stream的数据
        var velocity = Vector3.Lerp(streamA.velocity, streamB.velocity, weight);
        var angularVelocity = Vector3.Lerp(streamA.angularVelocity, streamB.angularVelocity, weight);
        stream.velocity = velocity;
        stream.angularVelocity = angularVelocity;
    }

    public void ProcessAnimation(AnimationStream stream)
    {
        Debug.Log("ProcessAnimation");
        AnimationStream streamA = stream.GetInputStream(0);
        AnimationStream streamB = stream.GetInputStream(1);

        // 遍历每个Joint对应Transform的Handle
        int numHandles = handles.Length;
        for (var i = 0; i < numHandles; ++i)
        {
            var handle = handles[i];

            // 获取该Transform在不同的动画Stream里的Local数据, 然后根据权重进行Blend
            // 当weight为0时, 全部采用Input 0对应Stream的数据
            // 当weight为1时, 全部采用Input 1对应Stream的数据
            var posA = handle.GetLocalPosition(streamA);
            var posB = handle.GetLocalPosition(streamB);
            handle.SetLocalPosition(stream, Vector3.Lerp(posA, posB, weight * boneWeights[i]));

            var rotA = handle.GetLocalRotation(streamA);
            var rotB = handle.GetLocalRotation(streamB);
            handle.SetLocalRotation(stream, Quaternion.Slerp(rotA, rotB, weight * boneWeights[i]));
        }
    }
}

// ============== 实际的使用脚本 =============
using Unity.Collections;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Animations;

using UnityEngine.Experimental.Animations;

public class SimpleMixer : MonoBehaviour
{
    // 俩Clip的插值权重
    [Range(0.0f, 1.0f)]
    public float weight;

    // NativeArray是一种特殊的数组, C++和C#端都可以访问同一块内存
    NativeArray<TransformStreamHandle> m_Handles;
    NativeArray<float> m_BoneWeights;					// 其实没用到这玩意儿

    PlayableGraph m_Graph;
    AnimationScriptPlayable m_CustomMixerPlayable;

    void OnEnable()
    {
        // Load动画clip
        var idleClip = SampleUtility.LoadAnimationClipFromFbx("DefaultMale/Models/DefaultMale_Generic", "Idle");
        var romClip = SampleUtility.LoadAnimationClipFromFbx("DefaultMale/Models/DefaultMale_Generic", "ROM");
        if (idleClip == null || romClip == null)
            return;

        var animator = GetComponent<Animator>();

        // Get all the transforms in the hierarchy.
        Transform[] transforms = animator.transform.GetComponentsInChildren<Transform>();
        var numTransforms = transforms.Length - 1;

        // new一个Native数组, 数组的大小为Animator对应模型的所有GameObject的数量
        m_Handles = new NativeArray<TransformStreamHandle>(numTransforms, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);

        // new一个Bone的权重数组, 默认的初始权重值都为1.0f, 其实在这里没啥用, 主要是为了配合AvatarMask的
        m_BoneWeights = new NativeArray<float>(numTransforms, Allocator.Persistent, NativeArrayOptions.ClearMemory);
        for (var i = 0; i < numTransforms; ++i)
        {
            // 把Animator对应GameObject的子GameObject的Transform绑定到animator上
            m_Handles[i] = animator.BindStreamTransform(transforms[i + 1]);
            m_BoneWeights[i] = 1.0f;
        }

        // 创建自定义的AnimationJob
        var job = new MixerJob()
        {
            handles = m_Handles,
            boneWeights = m_BoneWeights,
            weight = 0.0f
        };

        // 创建Graph
        m_Graph = PlayableGraph.Create("SimpleMixer");
        m_Graph.SetTimeUpdateMode(DirectorUpdateMode.GameTime);

        // 使用刚刚创建的AnimationJob, 创建AnimationScriptPlayable
        m_CustomMixerPlayable = AnimationScriptPlayable.Create(m_Graph, job);
        m_CustomMixerPlayable.SetProcessInputs(false);
        // 连接两个AnimationClipPlayable, 其实这个权重值无所谓, 连起来传了数据就行
        m_CustomMixerPlayable.AddInput(AnimationClipPlayable.Create(m_Graph, idleClip), 0, 0.0f);// 这里的0.0f可以换成任意值
        m_CustomMixerPlayable.AddInput(AnimationClipPlayable.Create(m_Graph, romClip), 0, 0.0f);

        var output = AnimationPlayableOutput.Create(m_Graph, "output", animator);
        output.SetSourcePlayable(m_CustomMixerPlayable);

        m_Graph.Play();
    }

    void Update()
    {
        // MixerJob是个Struct, 这里Copy了一份出来
        MixerJob job = m_CustomMixerPlayable.GetJobData<MixerJob>();

        // 注意, 在这个过程中, 俩AnimationClipPlayable的权重都是1
        job.weight = weight;

        // 改了权重值再Set回去
        m_CustomMixerPlayable.SetJobData(job);
    }

    void OnDisable()
    {
        m_Graph.Destroy();
        m_Handles.Dispose();
        m_BoneWeights.Dispose();
    }
}

更多关于AnimationJobs的例子

我都写在这里了,参考:https://github.com/hwx0000/animation-jobs-samples



AnimationStream and the handles

AnimationStream类没有继承于任何接口,它包含了PlayableGraph里传递给ScriptAnimationPlayable里的关于Animator的动画数据,有了它,就可以在自定义的动画脚本Playable里读取Animator里存的的动画数据了,代码如下:

// 在Playable之间传递的动画流数据
public struct AnimationStream
{
	// 一些动画相关的数据, 比如rootMotion数据和动画数据, 注意很多数据是只读的
    public bool isValid { get; }
    public float deltaTime { get; }
 
    public Vector3 velocity { get; set; }
    public Vector3 angularVelocity { get; set; }
 
    public Vector3 rootMotionPosition { get; }
    public Quaternion rootMotionRotation { get; }
 
    public bool isHumanStream { get; }
    public AnimationHumanStream AsHuman();
 
    public int inputStreamCount { get; }
    public AnimationStream GetInputStream(int index);
}

It isn’t possible to have a direct access to the stream data since the same data can be at a different offset in the stream from one frame to the other (for example, by adding or removing an AnimationClip in the graph). The data may have moved, or may not exist anymore in the stream. To ensure the safety and validity of those accesses, we’re introducing two sets of handles: the stream and the scene handles, which each have a transform and a component property handle.

Stream Data只能通过这种Playable的传递来获取,不可以直接从外部获取,为了保证安全和获取数据的有效性,需要使用Stream Handle和Scene Handle,这俩Handle都自带一个transform和一个component property handle。

更多的参考:https://m.blog.naver.com/sspsos74/221425132060


The stream handles
主要是用来保护stream读取数据的合法性的,如果报错会抛出C#异常,有两种stream handles:

  • TransformStreamHandle
  • PropertyStreamHandle.

AnimationHumanStream
当AnimationStream的模型是Humanoid类型时,可以通过其接口转化为AnimationHumanStream:
在这里插入图片描述
转化为AnimationHumanStream之后,就有了对应为Human的各种动画数据的读取操作,如下图所示是部分AnimationHumanStream的接口函数:
在这里插入图片描述



其他的相关类与API整理

AnimationPlayableUtilities

此类主要是为了简化API操作而做的,一共提供了五个Static方法:

// 根据参数,直接在Grapgh里面添加node,并设置好连接关系,然后Play
public static void Play(Animator animator, Playables.Playable playable, Playables.PlayableGraph graph);

// 创建一个graph,同时根据输入的RuntimeAnimatorContoller返回一个创建的AnimatorControllerPlayable
public static Animations.AnimatorControllerPlayable PlayAnimatorController(Animator animator, RuntimeAnimatorController controller, out Playables.PlayableGraph graph);

// 创建一个graph,根据输入的clip创建对应的AnimationClipPlayable,并播放这个graph
public static Animations.AnimationClipPlayable PlayClip(Animator animator, AnimationClip clip, out Playables.PlayableGraph graph);

// 创建一个graph,同时返回一个有inputCount个Layer的AnimationLayerMixerPlayable(可能不全是Layer)
public static Animations.AnimationLayerMixerPlayable PlayLayerMixer(Animator animator, int inputCount, out Playables.PlayableGraph graph);

// 创建一个graph,同时返回一个有inputCount个AnimationClipPlayable的AnimationMixerPlayable(可能不全是AnimationClipPlayable)
public static Animations.AnimationMixerPlayable PlayMixer(Animator animator, int inputCount, out Playables.PlayableGraph graph);

Notification

遵循INotification接口,具体的例子等会补充

Properrties

  • id

Constructors

  • public Notification(string name);


一些更深入的理解

Playable

Playable就是graph里互相连接的节点,每一个Playable可以设置其每一个子Playable的"weight"或"infulence"

一个PlayableGraph可以有多个outputs,它们也叫players,它们均继承于IPlayableOutout接口,每个PulayableOutput都会取其source playable的结果,然后把它应用到场景的一个物体上,举个例子,动画的 AnimationPlayableOutput会与两个东西进行连接,一个是Graph里的SourcePlayable,一个是场景里的物体(物体上的Animator),当graph is played,在graph evaluation过程中会算出一个AnimationPose,然后把它应用在Animator上

The ScriptPlayable 是一种custom的脚本做成的Playable(感觉AnimationPlayable这些Playables的本质应该也是ScriptPlayable),T需要derived from PlayableBehaviour,它们可以在graph evaluation阶段(在PlayableBehaviour.PrepareFrame and PlayableBehaviour.ProcessFrame函数中)写入相关的参数。

A good example of a ScriptPlayable is the TimelinePlayable which is controlling the Timeline graph. It creates and links together the playables in charge of the tracks and the clips.

Unity的Timeline里,就是通过名为TimelinePlayable的Script Playable来控制Timeline graph,她负责根据tracks和里面的clips来创建和连接里面的Playables。

PlayableGraph的Play过程中,会遍历每个PlayableOutput,它会先调用所有Playable的PrepareFrame函数,在这个函数里,每个Playable可以改变它的children,比如添加或者删改子节点。在准备过程结束后,由所有的PlayableOutputs负责处理这些结果。

When a PlayableGraph is played, each PlayableOutput will be traversed. During this traversal, it will call the PrepareFrame method on each Playable. This allows the Playable to “prepare itself for the next evaluation”. It is during the PrepareFrame stage that each Playable can modify its children (either by adding new inputs or by removing some of them). This enables Playable to “spawn” new children branches in the Playable tree at runtime. This means that Playable trees are not static structures. They can adapt and change over time.

Once the preparation is done, the PlayableOutputs are in charge of processing the result, that’s why they are also called “players”. In the case of an AnimationPlayableOutput, the Animator is in charge of processing the graph. And in the case of a ScriptPlayableOutput, PlayableBehaviour.ProcessFrame will be called on each ScriptPlayable.(所以ScriptPlayableOutput的输出执行方式就是执行ProcessFrame函数?)


PlayableAsset

一种asset文件类,可以用于在runtime instantiate出来一个Playable

Properties:

  • public double duration: playable对应资源的播放时间(seconds)
  • public IEnumerable outputs:A description of the outputs of the instantiated Playable.

公有函数

// 会根据owner,在graph里创建一个Playable,返回这个Playable对应的Root Playable(因为这个可能是一个Tree型的Playable)
public Playables.Playable CreatePlayable(Playables.PlayableGraph graph, GameObject owner);

附录

在这里记录一些使用PlayableGraph的心得

Editor下播放动画
之前我在Editor下,不断更改了PlayableGraph里的AnimationLayerMixerPlayable的状态,但是此时场景里面的人物并没有更新,然后我加了个函数,就可以了:

// 这个函数在Editor下, 当输入变化时改变动画数据
public static void PlayAnimationOnInputPoint(MyAsset asset, Vector2 inputP, GameObject model)
{
    if (!s_BlendSpaceGraph.IsValid())
        CreateGraph(asset, model);

 	// 改变里面的AnimationLayerMixerPlayable的mixer的权重
    for (int i = 0; i < asset.Count; i++)
    {
        var clip = asset.Samples[i].animationClip;
        if (map.ContainsKey(clip))
            s_CurMixer.SetInputWeight(i, map[clip]);// TODO
        else
            s_CurMixer.SetInputWeight(i, 0);
    }

    if (!s_BlendSpaceGraph.IsPlaying())
    {
        s_BlendSpaceGraph.Play();
        // 加了下面这一行代码, 就可以播放了
        // 这一行的代码会重新去读取PlayableGraph里的Output, 然后会update里面所有的Playable
        s_BlendSpaceGraph.Evaluate();
    }
}


PlaybleGraph里动态改变AnimationClipPlayable
这是一个AnimationPlayableOutput直接连接ClipPlayable的简单Graph,之前我是这么写的,发现不对:

// 切换播放的Clip
var output = graph.GetOutput(0);

AnimationClipPlayable clipPlayable = AnimationClipPlayable.Create(graph, clip);
output.SetSourcePlayable(clipPlayable);

发现需要手动摧毁,再Set

// 切换播放的Clip
var output = graph.GetOutput(0);

graph.DestroyPlayable(output.GetSourcePlayable());
AnimationClipPlayable clipPlayable = AnimationClipPlayable.Create(graph, clip);
output.SetSourcePlayable(clipPlayable);

未经作者允许,本文谢绝转载,谢谢各位阅读,欢迎批评指正。
文章链接(防止爬虫):https://blog.csdn.net/alexhu2010q/article/details/113921119

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值