UE 动画蓝图编译详细解析

前言

出于某个业务的需要,我开始详细地研究动画蓝图的编译相关的内容。不得不说,这部分内容规模庞大、文档稀少,给入门 UE 不久的我造成了一点小小的引擎震撼。(蓝图编译尚有一些博客可以参考,动画蓝图就是纯纯抓瞎了。)在经历了约一个月的盲人摸象、联想猜谜、翔山修索、逆向分析后,我总算把它的主要过程摸清楚了。为了减少可能存在的不幸的后来者所受到的上述折磨,我写出这篇文章,说明这一模块的架构设计思路,降低源码阅读的难度。

本文的目的是从头到尾地介绍动画蓝图是怎么从编译时的一个图表变成运行时可以执行逻辑的一个实例的。如果需要详细地进行研究,建议把源码放在旁边作为对照。本文有些地方对代码进行了引用,但是没有直接贴出。

在开始阅读之前,建议你对普通蓝图的编译有所了解。这里推荐几个阅读链接:

Compiler Overview for Blueprints Visual Scripting in Unreal Engine | Unreal Engine 5.1 Documentation

UE4蓝图解析 | 南京周润发

深入Unreal蓝图开发 | 房燕良 (推荐 理解蓝图技术架构创建自定义蓝图节点 两篇)

如果你对本文的任何内容有更正确、精确的了解,请务必提出,非常感谢!

一、类结构与相关术语说明

请注意,本节的二级标题中,有些是类的名字,有些是术语,有些则两者兼有。我曾尝试分得更清晰一点,但是失败了。本文还是把定位放在教程上吧。

斜体的内容为我的确定程度不是很高的内容,有一定推测成分。对于这部分内容请怀疑地接受。

斜体蓝色的内容为纯纯瞎猜的内容,仅构成猜想,提供思路。

UBlueprint / UAnimBlueprint

UBlueprint 类保存了蓝图的所有编译前信息,包括各种图表的信息。编译这个类,可以得到一个 UBlueprintGeneratedClass 类的实例。蓝图类可以看作是一种源代码,它的使命是被编译出另一个类。

UBlueprintGeneratedClass

它是 UClass 的派生类,也就是说它是用来存类型信息的。这里相当不好理解,我觉得主要是 UClass 这个类的名字起的不好,要是一开始就叫 UClassInfo 之类的就好得多了。所以当看到术语 “Generated Class” 时,要仔细分辨它指的是蓝图的编译结果,还是 “UBlueprintGeneratedClass(Info)” 这个类。前者是后者的实例,后者是前者的 GetClass()

为了清晰起见,我们把蓝图的编译结果叫做 “Instance Class”(其它很多地方直接用 Generated Class 来指代 Instance 类。在本文中,接下来出现的 Generated Class 都是指 Info 类)。蓝图就像源代码,将它编译,能够得到一个类,即是 Instance Class。这个 Instance Class 又能被实例化,变成若干运行时的对象。例如,你可以新建一个蓝图,Parent Class 选择 Actor,然后编辑它的 Event Graph 等,最终得到了一个有着特定行为的 Actor 类,这个类就是 Instance Class。你可以把这个蓝图拖进场景,就是利用 Instance Class 实例化出了运行时对象。

Parent Class

指 Instance Class 的父类。当你用蓝图编辑一个类的时候必须要选择 Parent Class,蓝图的作用是在这个 Parent Class 的基础上,利用各种图表(Graph)来定制化它的行为。还是用 Actor 举例,当你建立了一个 Parent Class 为 Actor 的蓝图,它创建的 Instance Class 首先是一个 Actor,然后你可以定制它 Tick 时做什么,Overlap 时做什么等等。

从另一个角度,也可以说 Parent Class 定义了接口,而蓝图用可视化编程的方式提供了实现。注意这种理解只是概念上的,并不是说蓝图的作用就完全等于 override。

UAnimInstance

动画蓝图的 Parent Class。它的最主要的功能是驱动动画蓝图的更新(Update)和求解(Evaluate)(之后会进一步说明)。至于更新求解的具体过程,自然是用 Anim Graph 来定义的。

USkeletalMeshComponent

让你看到动画的关键组件。每个这样的 Component 里面都保存了一个 UAnimInstance 的指针,也就是说每个 Component 都有它自己的 Anim Instance,它在 OnRegister 的时候创建这个 Instance,在 Tick 的时候更新 Instance。

UAnimBlueprintGeneratedClass

对一个普通的 Actor 对象,调用 GetClass(),会得到一个 UClass 对象。那么对于一个 UAnimInstance 对象,调用 GetClass(),就会得到一个 UAnimBlueprintGeneratedClass 对象。所以,UAnimBlueprintGeneratedClass 里面会存储一些对所有 Anim Instance 都适用的描述信息。比如,每个 Anim Graph 里面都会有一个 Output 节点,它是动画更新的起始点。那么,UAnimBlueprintGeneratedClass 里面就可以设置一个 FProperty 字段,表示 Anim Instance 里面的 Output 节点的属性指针;在运行时,就可以利用这个 FProperty 从 Instance 中取出实际的 Output 节点。

Class Default Object (CDO)

当你用 NewObject<SomeInstanceClass>(Outer, SomeGeneratedClass)创建对象时,创建出的对象就是 Class Default Object(CDO)的一份拷贝。CDO 是存储在 UClass(也就是 Generated Class) 的实例里的,蓝图编译的其中一个步骤便是设置 CDO。

CDO 的实际类型是 Instance Class。需要注意的是,在 C++ 代码里面是写不出所谓 Instance Class 这个类型的,你顶多把 CDO 的指针向下 Cast 到 Parent Class 的类型。你在蓝图编辑界面中创建的变量会进入 Instance Class,也就会进入 CDO,但是你不能用常规的指针赋值方法设置这些属性的默认值,如果要访问的话需要用到反射。

FKismetCompilerContext / FAnimBlueprintCompilerContext

编译器类。通过 override 其中的各种函数,你可以定制整个编译流程。

UAnimSubsystem / UAnimSubsystemInstance

动画系统的关键部分,但是没有文档。UE 可能期望程序员使用读心术来弄懂这个类是用来干啥的。

可能把所有的东西都放进 Generated Class 或者 Instance Class 里面会显得太臃肿了,又或者是想要一个更加可扩展的架构,总之,可能出于这样那样的目的,UE 整出了 Subsystem 机制,把动画蓝图的一些功能拆分进一些模块。例如,Update 时蓝图虚拟机的调用、快速路径、状态机相关功能,都被划进了独自的 Subsystem。

UAnimSubsystem 存放着对于特定 Anim Instance 来说,不变的、共有的信息。从数量上来说,UAnimSubsystemUAnimBlueprintGeneratedClass 是对等的。而 UAnimSubsystemInstance 则存放着可能会随着具体实例变化的信息。

UAnimSubsystem 存放在具体 Instance Class 的 Sparse Class Data 中(待会说明),而 UAnimSubsystemInstance 直接存在 Instance Class 里面(会被实例化进 CDO 里)。在动画蓝图编译器中,你有时会见到 Constant 和 Mutable 这一对术语,很可能前者和 UAnimSubsystem 有关,后者和 UAnimSubsystemInstance 有关。

UBlueprintExtension / UAnimBlueprintExtension

为了支持对应的 Subsystem,你可能需要在特定的编译阶段执行特定的操作。

这么来理解:你实现了自己的 Compiler Context 类,重写了其中的许多操作,最终目的是正确地生成你的 Generated Class 与 CDO。那么,既然你把 Generated Class 与 CDO 中的一些东西拆分了出来,形成了 Subsystem 与 Subsystem Instance,那被拆分出来的东西的编译流程自然也要拆分出来,自成模块。所以 Blueprint Extension 就是用来实现 Subsystem 的编译逻辑的。

例如,UAnimSubsystem_Base 的作用是支持动画节点在运行时调用蓝图虚拟机。那么与之对应的 UAnimBlueprintExtension_Base,它重写了许多函数,这些函数会在编译流程的合适时机调用,此外它还包含了许多数据结构,所有这些东西的目的都是为了在编译时正确地生成对应的 Subsystem,并且把它放入 Sparse Class Data。

所以,虽然它的名字叫 Blueprint Extension,好像

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值