环境
- Unity 2020、Unity 2021
Playables的定义
Playables包含三个部分:Playable、PlayableOutput、PlayableGraph
图1 PlayableGraph 图2 PlayableOutput 图3 Playable【Playable】代表各种可播放物的数据与行为。参考上图,除了Output外的所有节点都是Playable。
【PlayableOutput】它将输入Playable处理后的的状态信息,反应到场景中的Unity对象上,从而使播放效果呈现出来。
【PlayableGraph】它是Playables这套系统的容器与管理器。它负责创建、销毁各种Playable、PlayableOutput的节点,也管理节点间的关系。
从类的角度看Playables(逻辑/行为部分)
图4 Playables类图 PlayableGraph
中的节点只能是PlayableOutput
或Playable
类型(它们的子类型也可以,都归属到这两种类型里)。PlayableOutput与Playable的结构、实现方式都是相似的,只是用途不同,并且在目前的项目使用中基本用不到自定义的PlayableOutput,因此 后面只针对Playable做一些说明。
Playable
是所有可播放物(见图3)的基类型(不是基类),因此它们可以隐式转换为Playable(通过PlayableHandle实现)。Playable只能由Unity定义好后给我使用,我们无法完全自定义一个Playable结构出来,因为要用到C#侧的internal接口,并在native层实现具体的逻辑。
Unity为我们提供了拓展Playable
的能力,那就是ScriptPlayable<T>:IPlayable
与PlayableBehaviour:IPlayableBehaviour
。
ScriptPlayable<T>
是Unity定义好的一个Playable子类型,它只是一个空壳,所有的逻辑都由PlayableBehaviour来实现,而IPlayableBehaviour是我们可以自己实现拓展的。从设计模式角度来看,ScriptPlayable<T>就是一个适配器(Adapter),它将IPlayableBehaviour转换为IPlayable给PlayableGraph使用。
(反过来说,我们要拓展Playable,只能实现IPlayableBehaviour或者继承PlayableBehaviour,来定义自己的Behaviour类。并用ScriptPlayable<T>将Behaviour进行包装后,传递给PlayableGraph使用。)
PlayableHandle
这个结构在其他文章中几乎没有被提到过,但是它很重要。Handle是Playable是这套系统访问native层的接口,它持有相关对象的指针。PlayableHandle是访问所有Playable的Handle,无论是基类型Playable
还是子类型AnimationClipPlayable
、AnimationMixerPlayable
,都借助PlayableHandle
访问native里的接口。
分析Unity的设计点
下面列举几个,自己猜测出来的,Unity团队在Playables这套系统中的设计点:
1、Playable与PlayableOutput
它们都是空壳子,真正的接口在其对应的Handle里(并且功能的实现是在native层)。相关的Extensions类也只是把Handle的接口开放出来。定义这些空壳,是为了在C#层能够使用native提供的功能。
2、IPlayable与IPlayableOutput
这两个interface的存在是为了在C#中提供接口约束能力,并不承担实际功能或代码设计的目的。因为Playables相关的基础接口都采用struct实现,而struct是不能继承的。Unity底层通过Handle结构模拟了一种struct继承的实现方式(struct相互转换),并通过定义interface对一些接口的使用进行强制约束。
3、如何实现Playable
与其子类型的相互转换
如前所述,所有的Playable结构(struct)都是空壳,只提供了访问native的接口,它们的实质都在于PlayableHandle。只要PlayableHandle保存的指针是同一个,创建新的基类型或者子类型的实例,即可完成相互转换。
为了更符合面向对象的用法,Unity在每个子类型中都通过声明转换操作符(implicit、explicit)的方式,实现Playable与子类型的相互转换。例:
public static implicit operator Playable(AnimationMixerPlayable playable);
public static explicit operator AnimationMixerPlayable(Playable playable);
4、IPlayableBehaviour
存在的意义
这里其实我没搞很明白,甚至有些迷惑!!
PlayableBehaviour
是实现(implement)了IPlayableBehaviour
的C#类,注意这里是类(class)了,不是结构体(struct)。从这个小点也可以看出,它本身不是Playable,而是用于拓展Playable的另一套东西。
PlayableBehaviour是一个抽象类(abstract),它为所有IPlayableBehaviour的接口都只提供了空实现,即:空的虚方法(virtual)。这里我能理解到的用途是 简化拓展的代码量,因为IPlayableBehaviour提供的是一组生命周期的接口(类似Awake、Start、Update),我们只需要实现自己需要的几个即可,没必要把所有接口都写出来。PlayableBehaviour的存在,就为我们提供了,可以按需实现的便利。
Unity官方Scripting API文档里介绍PlayableBehaviour,说它是所有自定义Playable脚本的基类。言下之意,我们拓展Playable时,应该继承PlayableBehaviour,而不是实现IPlayableBehaviour。但是有两个点凑在一起,使得这个设定很奇怪:
- 在C#中(反编译)能看到的Playable相关模块里,没有直接使用PlayableBehaviour的地方。
PlayableHandle
与ScriptPlayable<T>
使用的都是IPlayableBehaviour。 - PlayableBehaviour中有个方法,在IPlayableBehaviour中并没有定义:
public virtual void PrepareData(Playable playable, FrameData info)
。文档中说当Playable延迟时,会调用PrepareData,但什么情况下算延迟不知道;在PrepareData中应该做些什么事情也不知道。
令人困惑的也是两点:
- 既然设定
PlayableBehaviour
为所以自定义的基类,那么为啥还要定义IPlayableBehaviour
?
这里不需要模拟继承;IPlayableBehaviour里的接口又不全。 - 所有Playable接口,接收的都是IPlayableBehaviour类型,那么PrepareData是如何被调用的?
网友评论