UE Slate 特殊语法分析
简述
UE4 的Slate系统有很多奇怪的语法,创建新的SWidget对象时,语法中会包含SNew,.XXX() , +XXX 和 [XXX]类型的语法 。
在自定义SWidget类的时候,会需要用到SLATE_BEGIN_ARGS
, SLATE_END_ARGS
等语法,并且需要实现Construct方法,
这些特殊的语法和规则,都是UE4通过宏定义,运算符重载来完成的。UE4希望尽可能简化Slate相关的代码编写难度,
但是对于不熟悉“内幕”的人来说,这些特殊语法可能会造成一些困惑。这里尝试去分析Slate所有的特殊语法,了解这些特殊语法
是怎么起作用的,写Slate相关代码时需要遵守的各种“潜规则”又是怎么来的。
从SNew开始
一个最简单的创建SWidget的代码大致如下
SNew(STextBlock).Text(FText::FromString("Hello World"));
几乎所有的Slate相关宏,都可以在 SlateCore\Public\Widgets\DeclarativeSyntaxSupport.h 中找到。
其中,SNew的定义如下:
#define SNew( WidgetType, ... ) \
MakeTDecl<WidgetType>( #WidgetType, __FILE__, __LINE__, RequiredArgs::MakeRequiredArgs(__VA_ARGS__) ) <<= TYPENAME_OUTSIDE_TEMPLATE WidgetType::FArguments()
TYPENAME_OUTSIDE_TEMPLATE定义为
#define TYPENAME_OUTSIDE_TEMPLATE typename
把这个东西直接忽略掉似乎也没事
那么,把最开始的代码,宏展开,去掉typename修饰,假定当前的__FILE__是Source.cpp, __LINE__是20(这是SNew代码出现的位置,Debug用的)
大概就成了下面这个样子:
MakeTDecl<STextBlock>("STextBlock","Source.cpp",20,RequiredArgs::MakeRequiredArgs())
<<=
STextBlock::FArguments().Text(FText::FromString("Hello World"));
分析一下这行代码的执行顺序,C++代码的执行顺序,具体顺序规则相当复杂1,不过应该会遵守以下规则(个人总结):
- 如果需要执行某个函数,那么必定会先对所有参数求值(如果参数是函数,那么参数对应的函数必定会先执行)
- XXX.YYY().ZZZ()类型的多个函数连续调用,必定是先调用左边的函数,再用它的返回值去调用右边的函数
- 如果有多个运算符号都可以执行,那么优先级高的运算符先执行
由于C++中 <<= 预算符的优先级相当低,所以上面代码的执行顺序大概可以认为是:
- 先执行 <<= 符号左边的表达式 MakeTDecl(…)
- 再执行 <<= 符号右边的表达式 STextBlock::FArguments().xxx
- 最后,执行 <<= 预算符(这个运算符被重载了),返回最终的SWidget对象(智能指针)
编译器可能选择不严格按照这个顺序执行代码,但是最终结果应该与这个顺序的结果一致 (之后的执行顺序分析不再赘述这一点)
已经可以发现,实际的代码执行和看上去的并不一致,在使用SNew创建SWidget的时候,跟在SNew后面的一系列 .XXX().YYY()函数,
看上去就像是在调用SWidget对象的函数,更改它的属性。然而,实际上这些函数是在另外的对象上调用的,并没有直接修改SWidget对象的数据。
(当然,最终能生效,说明肯定还是间接更改SWidget对象的属性了,只不过更改过程不想看上去那么直接)
更具体的分析一下一上代码的执行顺序:
- RequiredArgs::MakeRequiredArgs() 方法被调用,这是一个泛型方法,根据传入参数的不同,会调用到不同的方法,最终生成
T0RequiredArgs – T5RequiredArgs的对象并返回,这个对象的作用,可以认为是存储SNew中,除了类名之外的其它参数,最多可以有五个 - MakeTDecl 方法被调用,生成一个TDecl的对象,这个对象包含真正的SWidget对象。TDecl对象,在构造方法里面,会负责
对应SWidget对象的创建。这个对象的创建不是直接通过new方法来生成的,而是通过专门的Allocator,UE可能在内部使用了
内存池的技术,减少不必要的对象生成。
此时,虽然对应的SWidget对象已经生成了,但是对应的Construct方法并没有调用,所以这个SWidget对象还是没有真正初始化的。
另外,TDecl的构造方法中,会将SWidget对象的Debug信息(SNew的文件名,行号,SWidget的名称)赋值给这个SWidget对象。 - STextBlock::FArguments()被调用,生成STextBlock::FArguments对象,这个对象用来执行接下来的.Text()方法