一、属性 UPROPERTY

属性声明

属性使用标准的 C++变量语法声明,前面用 UPROPERTY 宏来定义属性元数据和变量说明符。
UPROPERT 宏作为声明序言的变量可被引擎执行垃圾回收,也可在虚幻编辑器中显示和编辑。

1
2
UPROPERTY([specifier, specifier, ...], [meta(key=value, key=value, ...)])
Type VariableName;

属性说明符

声明属性时,属性说明符 可被添加到声明,以控制属性与引擎和编辑器诸多方面的相处方式。

``

实例:实例化到场景中的
原型:未实例化,比如直接点开蓝图

注意:EditAnyWhere 可以分别在实例和原型修改值。如果实例没有修改,则值始终等于原型中的值。如果实例修改了,则覆盖原型中的值。

属性标签 效果
AdvancedDisplay 属性将被放置在其出现的任意面板的高级(下拉) 部分中。
AssetRegistrySearchable AssetRegistrySearchable 说明符说明此属性与其值将被自动添加到将此包含为成员变量的所有资源类实例的资源注册表。不可在结构体属性或参数上使用。
⭐ BlueprintAssignable ⭐只能与多播委托共用。公开属性在蓝图中指定。
BlueprintAuthorityOnly 此属性必须为一个多播委托。在蓝图中,其只接受带 BlueprintAuthorityOnly 标签的事件。
BlueprintCallable ⭐ 仅用于多播委托。应公开属性在蓝图代码中调用。
BlueprintGetter=GetterFunctionName 此属性指定一个自定义存取器函数。如此属性不带 BlueprintSetter 或 BlueprintReadWrite 标签,则其为隐式 BlueprintReadOnly
⭐ BlueprintReadOnly ⭐此属性可由蓝图读取,但不能被修改。
⭐ BlueprintReadWrite ⭐可从蓝图读取或写入此属性。
BlueprintSetter=SetterFunctionName 此属性拥有一个自定义编译函数,被隐式标记为 BlueprintReadWrite。注意:必须对变异函数进行命名,并为相同类的一部分。
⭐ Category="TopCategory|SubCategory|..." ⭐指定在蓝图编辑工具中显示时的属性类别。使用 | 运算符定义嵌套类目。
Config 此属性将被设为可配置。当前值可被存入与类相关的 .ini 文件中,创建后将被加载。无法在默认属性中给定一个值。暗示为 BlueprintReadOnly
DuplicateTransient 说明在任意类型的复制中(复制/粘贴、二进制复制等),属性的值应被重设为类默认值。
EditAnywhere ⭐说明此属性可通过属性窗口在原型和实例上进行编辑。
⭐ EditDefaultsOnly ⭐说明此属性可通过属性窗口进行编辑,但只能在原型上进行。
EditFixedSize 只适用于动态数组。这能防止用户通过虚幻编辑器属性窗口修改数组长度。
EditInline 允许用户在虚幻编辑器的属性查看器中编辑此属性所引用的Object的属性(只适用于Object引用,包括Object引用的数组)。
⭐ EditInstanceOnly ⭐说明此属性可通过属性窗口进行编辑,但只能在实例上进行,不能在原型上进行。
Export 只适用于Object属性(或Object数组)。说明Object被复制时(例如复制/粘贴操作)指定到此属性的Object应整体导出为一个子Object块,而非只是输出Object引用本身。
GlobalConfig 工作原理与 Config 相似,不同点是无法在子类中进行覆盖。无法在默认属性中对其给定一个值。暗示为 BlueprintReadOnly
Instanced 仅限Object(UCLASS)属性。此类的一个实例创建时,其将被给定一个Object的特殊副本,指定到默认项中的此属性。用于实例化类默认属性中定义的子Object。暗示为 EditInline 和 Export
Interp 说明值可随时间由Sequencer中的一个轨道驱动。
Localized 此属性的值将拥有一个定义的本地化值。多用于字符串。暗示为 ReadOnly
Native 属性为本地:C++代码负责对其进行序列化并公开到垃圾回收
NoClear 阻止从编辑器将此Object引用设为空。隐藏编辑器中的清除(和浏览)按钮。
NoExport 只适用于本地类。此属性不应包含在自动生成的类声明中。
NonPIEDuplicateTransient 属性将在复制中被重设为默认值,除非其被复制用于PIE会话。
NonTransactional 说明对此属性值的修改不会包含在编辑器的撤销/重新执行历史中。
NotReplicated 跳过复制。这只会应用到服务请求函数中的结构体成员和参数。
⭐ Replicated 属性应随网络进行复制。
⭐ ReplicatedUsing=FunctionName ReplicatedUsing 说明符指定一个回调函数,其在属性通过网络更新时执行。
RepRetry 只适用于结构体属性。如果此属性未能完全发送(举例而言:Object引用尚无法通过网络进行序列化),则重新尝试对其的复制。对简单引用而言,这是默认选择;但对结构体而言,这会产生带宽开销,并非优选项。因此在指定此标签之前其均为禁用状态。
SaveGame 此说明符可简便地将域显式包含,用于属性关卡中的检查点/保存系统。应在作为游戏存档一部分的所有域上设置此标签,并使用代理归档器对其进行读写。
SerializeText 本地属性应被序列化为文本(ImportTextExportText)。
SkipSerialization 此属性不会被序列化,但仍能导出为一个文本格式(例如用于复制/粘贴操作)。
SimpleDisplay 出现在 细节 面板中的可见或可编辑属性,无需打开”高级”部分即可见。
TextExportTransient 此属性将不会导出为一个文本格式(因此其无法用于复制/粘贴操作)。
Transient 属性为临时,意味着其无法被保存或加载。以此方法标记的属性将在加载时被零填充。
⭐ VisibleAnywhere ⭐ 说明此属性在所有属性窗口中可见,但无法被编辑。此说明符与”Edit”说明符不兼容。
⭐ VisibleDefaultsOnly ⭐ 说明此属性只在原型的属性窗口中可见,无法被编辑。此说明符与所有”Edit”说明符均不兼容。
⭐ VisibleInstanceOnly ⭐ 说明此属性只在实例的属性窗口中可见(在原型属性窗口中不可见),无法被编辑。此说明符与所有”Edit”说明符均不兼容。

数据类型

![[Pasted image 20230902142824.png|181]]

上面为原生 C++类型,下面为对应的 UE 类型

为什么 UE 自己定义类型?

  1. 跨平台
  2. 兼容反射
  3. 方便宏替换,C++原生类型有空格不方便
    ![[Pasted image 20230901195356.png]]

整数

整数数据类型转换是”int”或”uint”后跟位大小。

变量类型 说明
uint8 8位无符号
uint16 16位无符号
uint32 32位无符号
uint64 64位无符号
int8 8位有符号
int16 16位有符号
int32 32位有符号
int64 64位有符号

作为位掩码

整数属性位掩码

整数属性现在可以位掩码形式公开给编辑器。要将整数属性标记为位掩码,只需在 meta 分段中添加 Bitmask 即可,如下所示:

1
2
3
4
/*~ BasicBits appears as a list of generic flags in the editor, instead of an integer field. */
//BasicBits在编辑器中显示为一个通用flag标志列表,而不是一个整数字段
UPROPERTY(EditAnywhere, Meta = (Bitmask))
int32 BasicBits;

添加此元标记将使整数作为下拉列表形式可供编辑,它们使用笼统命名标记(”Flag 1”、”Flag 2”、”Flag 3”等等),可以 单独打开或关闭。

![[cd2166f5df34fcddfa25e7e5196ce3a7_MD5.jpg]]

蓝图整数位掩码

也可以让蓝图可调用函数的整型参数表现为位掩码,方法是在参数的 UPARAM 指定器上添加 Bitmask 元标签(不需要值)。

1
2
3
4
/*~ You can set MyFunction using a generic list of flags instead of typing in an integer value. */
//你可以使用一个通用的flag标志列表来设置MyFunction,而不是输入一个整数值。
UFUNCTION(BlueprintCallable)
void MyFunction(UPARAM(meta=(Bitmask)) int32 BasicBitsParam)
自定义位标记名称

自定义位标记名称,首先必须使用Bitflags元标记来创建 UENUM

1
2
3
4
5
6
7
UENUM(Meta = (Bitflags))
enum class EColorBits
{
ECB_Red,
ECB_Green,
ECB_Blue
};

比特掩码枚举类型的范围是0到31,包括0和31。其对应于32位整型变量的位数(从第0位开始)。在上面的例子中,第0位是 ECB_Red,第1位是 ECB_Green,第2位是 ECB_Blue

作为另一种声明方式,你可以使用 ENUM_CLASS_FLAGS 在定义完枚举类型后,将其变成一个位掩码。 为了在编辑器中使用标志选择器(flag selector),我们还必须添加元字段 UseEnumValuesAsMaskValuesInEditor 并将其设置为 true。关键的区别在于,这个方法直接使用掩码值,而不是比特数。使用此方法制作的等效枚举类型看起来像这样:

1
2
3
4
5
6
7
8
9
UENUM(Meta = (Bitflags, UseEnumValuesAsMaskValuesInEditor = "true"))
enum class EColorBits
{
ECB_Red = 0x01,
ECB_Green = 0x02,
ECB_Blue = 0x04
};

ENUM_CLASS_FLAGS(EColorBits);

创建该UENUM后,可以使用BitmaskEnum元标记来引用它,如:

1
2
3
4
/*~ This property lists flags matching the names of values from EColorBits. */
//这个属性列出了与EColorBits中值的名称相匹配的标志
UPROPERTY(EditAnywhere, Meta = (Bitmask, BitmaskEnum = "EColorBits"))
int32 ColorFlags;

完成这个更改后,下拉框中列出的位标记将使用列举类条目的名称和值。在上述示例中, ECB_Red 值为0,表示它被选中时将激活位0(将ColorFlags增加1)。ECB_Green对应于位1(将ColorFlags增加2),ECB_Blue 对应于位2(将ColorFlags增加4)。

![[47dbb9ed69b8e8601e913aa90c304f74_MD5.jpg]]

同样,你可以在 UPARAM 标签的meta部分添加 BitmaskEnum 和对应的枚举类型名称来定制它。

1
2
3
/*~ MyOtherFunction shows flags named after the values from EColorBits. */
UFUNCTION(BlueprintCallable)
void MyOtherFunction(UPARAM(meta=(Bitmask, BitmaskEnum = "EColorBits")) int32 ColorFlagsParam)

虽然列举类型包含超过32个条目,但在属性编辑器UI中,位掩码关联中只会看到前32个值。同样,虽然可接受显式值条目,但显式值介于0-31的条目不会包含在下拉列表中。

浮点数

虚幻使用标准 C++浮点类型、浮点和双精度。

1
2
float  //32bit
double //64bit

布尔

布尔类型可以使用 C++ bool 关键字表示或表示为位域。

1
2
uint32 bIsHungry : 1;
bool bIsThirsty;

枚举

枚举主要有两种,典型的 enum 和 C++11 引入的 enum class

  • enum class 限定了作用域
  • enum 类型不太安全,可以使用 TEnumAsByte 来处理 enum 类型。(enum class 则不需要处理)

TEnumAsByte 类型的主要作用是在 C++ 代码中声明和操作枚举类型的变量。它提供了一种类型安全的方式来处理枚举值,并确保正确的枚举值转换。

使用 TEnumAsByte 类型时,您需要指定要包装的枚举类型作为模板参数。例如,如果您有一个名为 EMyEnum 的枚举类型,您可以声明一个 TEnumAsByte 类型的变量来存储该枚举的值。

以下是 TEnumAsByte 类型的一些常见用法:

  1. 声明变量:
1
TEnumAsByte<EMyEnum> MyEnumValue;
  1. 初始化变量:
1
MyEnumValue = EMyEnum::Value1;
  1. 获取变量的枚举值:
1
EMyEnum EnumValue = MyEnumValue.Get();
  1. 设置变量的枚举值:
1
MyEnumValue.Set(EMyEnum::Value2);
  1. 比较变量的枚举值:
1
2
3
4
if (MyEnumValue == EMyEnum::Value3)
{
// 执行某些操作
}

通过使用 TEnumAsByte 类型,您可以确保在处理枚举类型时遵循类型安全性,并减少由于不正确的枚举值转换而引起的错误。

二、类 UCLASS

虚幻引擎包含一个用于处理游戏对象的强大系统。虚幻引擎中**所有对象的基类都是 UObject**。

声明包含一个类的标准 C++ 类声明。在标准声明之上,描述符(如类说明符和元数据)将被传递到 UCLASS 宏。
UCLASS 宏的作用是标记 UObject 的子类,以便 UObject 处理系统可以识别它们。

1
2
3
4
5
UCLASS([specifier, specifier, ...], [meta(key=value, key=value, ...)])
class ClassName : public ParentName
{
GENERATED_BODY()
}

1 UCLASS 宏

UCLASS 宏为 UObject 提供了一个 UCLASS 引用,用于描述它在虚幻引擎中的类型。每个 UCLASS 都保留一个称作 类默认对象(Class Default Object) 的对象,简称 CDOCDO 是 UE 反射系统中包含对象的原版副本,引擎启动时创建。
引擎初始化时,他会为每个类创建类默认对象,然后执行每个类的构造函数,设置他们的默认值。由 C++类创建的蓝图从 CDO 中获得始化的默认值

你可以为指定对象获取其 UCLASSCDO,虽然它们通常都是只读类型。

使用 GetClass() 函数即可访问对象实例的 UCLASS。

[!NOTE]
UObject 类还可包括仅限本地的属性,这些属性没有用 UFUNCTION 或者 UPROPERTY 指定器标记用于反射。只有用指定器宏标记过的函数和属性会列举在它们对应的 UCLASS 中。

2 类说明符

声明类时,可以为声明添加 类说明符,以控制类相对于引擎和编辑器的各个方面的行为。

类说明符 效果
Abstract Abstract 说明符会将类声明为”抽象基类“,阻止用户向关卡中添加此类的 Actor。对于单独存在时没有意义的类,此说明符非常有用。例如,ATriggerBase 基类是抽象类,而 ATriggerBox 子类不是抽象类,可以放置在关卡中。
AdvancedClassDisplay AdvancedClassDisplay 说明符强制类的所有属性仅在显示这些属性的 “细节面板(Details Panel)” 的”高级(Advanced)”部分中显示。要覆盖单个属性上的此说明符,在该属性上使用 SimpleDisplay 说明符。
AutoCollapseCategories=(Category1, Category2, ...) AutoCollapseCategories 说明符使父类上的 AutoExpandCategories 说明符的列出类别的效果无效。
AutoExpandCategories=(Category1, Category2, ...) 为此类的对象指定应自动在虚幻编辑器属性窗口中展开的一个或多个类别。要自动展开未使用类别声明的变量,请使用声明变量的类的名称。
Blueprintable ⭐将此类公开为用于创建蓝图的可接受基类。默认为 NotBlueprintable,除非继承时就并非如此。此说明符由子类继承。
⭐ BlueprintType 将此类公开为可用于蓝图中的变量的类型。
ClassGroup=GroupName 指示在虚幻编辑器的 Actor 浏览器中启用 组视图(Group View) 时,Actor 浏览器 应在指定的 GroupName 中包含此类及此类的所有子类。
CollapseCategories 指示此类的属性不应划分到虚幻编辑器属性窗口的类别中。此说明符会传播到子类,可由 DontCollapseCategories 说明符覆盖。
Config=ConfigName 指示此类可在配置文件(.ini)中存储数据。如果存在任何使用 configglobalconfig 说明符声明的类属性,此说明符将使这些属性存储在指定的配置文件中。此说明符会传播到所有子类并且无法使此说明符无效,但是子类可通过重新声明 config 说明符并提供不同的 ConfigName 来更改配置文件。常见的 ConfigName 值是”Engine”、”Editor”、”Input”和”Game”。
Const 此类中的所有属性和函数都是 const 并且导出为 const。此说明符由子类继承。
ConversionRoot 根转换,将子类限制为仅可沿层级向上转换为第一个根类的子类。
CustomConstructor 阻止构造函数声明自动生成。
DefaultToInstanced 此类的所有实例都被认为是”实例化的”。实例化的类(组件)将在构造时被复制。此说明符由子类继承。
DependsOn=(ClassName1, ClassName2, ...) 列出的所有类将先于此类被编译。提供的类名必须指示同一个或前一个包中的类。可以使用单个 DependsOn 行(以逗号分隔)来标识多个依赖类,或者可以通过为每个类使用单独的 DependsOn 行来指定多个依赖类。当某个类使用在另一个类中声明的结构体或枚举时,这非常重要,因为编译器仅知道它已编译了类中的哪些部分。
Deprecated 此类已弃用,序列化时将不保存此类的对象。此说明符由子类继承。
DontAutoCollapseCategories=(Category, Category, ...) 使列出的类别的继承自父类的 AutoCollapseCategories 说明符无效。
DontCollapseCategories 使继承自基类的 CollapseCatogories 说明符无效。
EditInlineNew 指示可以从虚幻编辑器”属性(Property)”窗口创建此类的对象,而非从现有资源引用。默认行为是仅可通过”属性(Property)”窗口指定对现有对象的引用。此说明符会传播到所有子类;子类可通过 NotEditInlineNew 说明符覆盖它。
HideCategories=(Category1, Category2, ...) 列出一个或多个应该对用户完全隐藏的分类。要隐藏未使用类别声明的属性,请使用声明变量的类的名称。此说明符会传播到子类。
HideDropdown 阻止此类在属性窗口组合框中显示。
HideFunctions=(Category1, Category2, ...) 让指定分类中的所有函数都对用户完全隐藏。
HideFunctions=FunctionName 将提到的函数对用户完全隐藏。
Intrinsic 指示此类直接在 C++中声明,无 Unreal Header Tool 生成的样板。请勿在新类上使用此说明符。
MinimalAPI 导致仅导出此类的类型信息,以供其他模块使用。可以以此类为目标进行强制转换,但此类的函数无法被调用(除了使用内联方法)。这可以缩短编译时间,因为没有针对无需从其他模块访问其所有函数的类导出一切。
NoExport 指示此类的声明不应包含在由标头生成器自动生成的 C++头文件中。必须在单独的头文件中手动定义该 C++类声明。仅对本地类有效。请勿对新类使用此说明符。
NonTransient 使继承自基类的 Transient 说明符无效。
NotBlueprintable 指定此类不是可用于创建蓝图的可接受基类。此为默认说明符,将由子类继承。
NotPlaceable 使继承自基类的 Placeable 说明符无效。指示不可以在编辑器中将此类的对象放置到关卡、UI 场景或蓝图中。
PerObjectConfig 此类的配置信息将按对象存储,在 .ini`文件中,每个对象都有一个分段,根据对象命名,格式为 [ObjectName ClassName] `。此说明符会传播到子类。
Placeable 指示可在编辑器中创建此类,而且可将此类放置到关卡、UI 场景或蓝图(取决于类类型)中。此标志会传播到所有子类;子类可使用 NotPlaceable 说明符覆盖此标志。
ShowCategories=(Category1, Category2, ...) 使列出的类别的继承自基类的 HideCategories 说明符无效。
ShowFunctions=(Category1, Category2, ...) 在属性查看器中显示列出的类别中的所有函数。
ShowFunctions=FunctionName 在属性查看器中显示指定的函数。
Transient 从不将属于此类的对象保存到磁盘。当与播放器或窗口等本质上不持久的特定种类的原生类配合使用时,它非常有用。此说明符会传播到子类,但是可由 NonTransient 说明符覆盖。
Within=OuterClassName 此类的对象无法在 OuterClassName 对象的实例之外存在。这意味着,要创建此类的对象,需要提供 OuterClassName 的一个实例作为其 Outer 对象。

3 TSubclassOf

TSubclassOf 是提供 UClass 类型安全性的模板类。它在分配从特定类型派生出来的类时很有效

  • 例如,你可以把这个变量公开给蓝图,设计者可以在蓝图中为玩家角色指定生成的武器类别。
  • 例如您在创建一个投射物类,允许设计者指定伤害类型。您可只创建一个 UClass 类型的 UPROPERTY,让设计者指定派生自 UDamageType 的类(但选择列表里会出现所有 UClass 类,干扰选择);建议使用 TSubclassOf 模板强制要求此类的选择。以下示例代码展示了不同之处:
1
2
3
/** type of damage */
UPROPERTY(EditDefaultsOnly, Category=Damage)
UClass* DamageType;

Vs.

1
2
3
/** type of damage */
UPROPERTY(EditDefaultsOnly, Category=Damage)
TSubclassOf<UDamageType> DamageType;

在第二个声明中,模板类告知编辑器的属性窗口只列出派生自 UDamageType 的类(作为属性选择)。在第一个声明中可选择任意 UClass。下图对此进行了说明。

![[0373df02883ba7ef2855694b2d997626_MD5.jpg]]

策略游戏投射物蓝图的范例

UPROPERTY 安全外,您还能获得 C++ 层级上的类型安全。如尝试进行不兼容 TSubclassOf 类型的相互指定,将出现编译错误。
尝试指定泛型 UClass 时,它将执行一个运行时检查,以确定它可执行指定。如运行时检查失败,结果数值为 nullptr。

1
2
3
4
5
6
7
8
9
UClass* ClassA = UDamageType::StaticClass();

TSubclassOf<UDamageType> ClassB;

ClassB = ClassA; // Performs a runtime check

TSubclassOf<UDamageType_Lava> ClassC;

ClassB = ClassC; // Performs a compile time check

搞清楚:

4 StaticClass/GetClass/ClassDefaultObject(CDO)

搞懂这三个的区别

UClass 和反射

什么是反射?简单来说,反射的作用就是在不知道这个类是什么类的情况下获取到它的一些信息。 C++ 是没有反射机制的所以 UE 底层实现了一套反射机制。C# 怎么用反射:

1
2
string s = "鸡桑大帅逼”;
Type t = s.GetType();

Type 里面就包含了 string 这个类的各种信息。接下来请我和大声念三遍:

  • UClass 就是 C# 里的 Type!
    虽然 UClass 和 C# 里的 Type 的作用并不完全相同,但是这么理解会帮我们弄明白 UE 底层的很多事情。

GetClass ()

GetClass() 的作用就是我们生成一个 UObject 实例后,去拿这个实例的 UClass,类似 C# 的 **GetType**。

接下来我们做个小实验。新建一个自己的 Actor 就叫 MyActor 好了,写一个方法并且让它蓝图可调用:

![[d5d6733a8dda58a5199ef0087152203c_MD5.jpg]]

建两个蓝图,并拖到场景中,在关卡蓝图里调用 IsSameUClass

![[8843ff51e75527c39334e8d163224d9e_MD5.jpg]]

![[3aec8a80e5a071ec461cf21d1cff9c47_MD5.jpg]]

显示的结果是 0,也就是两个 MyActor 的 GetClass () 结果不一样
但是如果场景上的两个 Actor 都是 BP_MyActor 同一个蓝图,它们的 GetClass () 结果是一样的。

先解释第一个结果:为什么同样 C++ 类型不同蓝图返回的 GetClass () 结果不一样? 那是因为 UClass 不仅需要记录类型信息,还需要承担序列化的工作。同样 C++ 类型,但是蓝图不一样,需要序列化数据是不一样的,所以 UClass 不一样。
同样类型,同样蓝图,但是场景中实例不一样,但是这时需要序列化的蓝图数据是一样的(像场景中的位置信息不是记录在蓝图里的),所以它们的 UClass 是一样的,这是第二个结果的原因。

GetStaticClass ()

GetClass 很好很强大。但是有个问题,它是 UObject 的成员函数。**我现在没有 UObject 实例,但是我想拿到某个类的 UClass 怎么办?
使用 StaticClass()

1
UClass* myUClass = AMyActor::StaticClass();

这样子我就不需要有 UObject 实例也能拿到某个类的 UClass 了。同时因为它是 Static 的所以每次调用 T:: StaticClass 返回的都是同一个结果。

1
2
3
4
5
auto static1Class = AMyActor::StaticClass();
auto static2Class = AMyActor::StaticClass();
auto result = static1Class == static2Class;
//结果是1
UE_LOG(LogTemp, Warning, TEXT("Is StaticClass Same: %d"), result);

再讲 StaticClass 的另一个功能,帮助大家更加理解 StaticClass。先来看 UObject、UClass、Actor 类图:

![[6f4a4c16ca3964e8d81a7f7984fba90b_MD5.jpg]]

现在又有个问题来了,我现在有一个 UClass,如何知道这个 UClass 和另一个 UClass 是不是继承关系? 直接转类型判断吗?不对,看上面那张图,UClass 是没有子类的。那怎么办?答案是 UClass 存储了它要描述的类的父类的 StaticClass

有点绕,我举例说明一下。我现在有个 MyActor 类的 UClass,想知道 MyActor 是不是 UObject 的子类,我就这么做:

  • 调用 UObject::StaticClass()
  • 拿到 MyActor 的 UClass 存储的 MyActor 的父类的 StaticClass 也就是 Actor 的 StaticClass
  • 比较 Actor 的 StaticClass 和 UObject 的 StaticClass 是不是相等
  • 发现不相等,这次我拿 Actor 的 StaticClass(因为它也是一个 UClass)里存的 Actor 父类的 StaticClass 也就是 UObject 的 StaticClass,然后比较,发现相等,返回 true
  • 如果我发现不相等,我就一直比,比到最后没有父类,返回 false

上面这段逻辑就是 UClass 的 IsChildOf 的实现原理。调用 IsChildOf 的时候传入泛型:

![[5cde1608b92f443c7ba603fef61f58c6_MD5.png]]

这是具体实现。UStruct 是 UClass 父类。

![[dfbfbc18d7faa479a8bfe0218ef448cf_MD5.jpg]]

GetSuperStruct () 就是返回它存储的父类的 StaticClass。

![[b22a0653c9c4c750ec4aa20b0aeb8e09_MD5.png]]

顺带再讲一下 IsA 这个函数,功能和 IsChildOf 一样,不同的是 IsChildOfUClass 用的,IsA 是给 UObject 对象实例用的,比如想知道一个 MyActor 实例是不是 UObject 的子类:

1
2
//myActor是MyAcor*类型
bool result = myActor->IsA<UObject>();

ClassDefaultObject

GetClassGetStaticClass 都明白了之后,理解 ClassDefaultObject 就简单多了。

ClassDefaultObject(简称 CDO),类默认对象。通过 CDO 我可以拿到一个 UObject 初始化时的值。虽然 CDO 有个 Default 默认,但是用默认去描述它的功能不是很准确。回到我们创建的类 MyActor,给它添加一个外部可以修改变量 testIntValue,默认值 1:

![[a7bc95bbdf6ebca930c9a2914cd1cd7a_MD5.jpg]]

对于两个蓝图分别修改,一个 20,另一个 10:

![[57874a26c52ee95fecabf36963ab8645_MD5.jpg]]

![[d18d08201c2014c335b39b11499ef9f0_MD5.jpg]]

然后在 BeginPlay 写下测试代码:

1
2
3
4
//先修改值再获取ClassDefaultObject
testIntValue = -1;
auto defaultInt = GetClass()->GetDefaultObject<AMyActor>()->testIntValue;
UE_LOG(LogTemp, Warning, TEXT("DefaultObject int: %d"), defaultInt);

最后打印的结果是一个 20,一个 10。

如果想拿到代码里给它设的默认值 1 要用 StaticClass

1
2
3
auto staticDefaultInt = AMyActor::StaticClass()->GetDefaultObject<AMyActor>()->testIntValue;
//打印1
UE_LOG(LogTemp, Warning, TEXT("StaticDefaultObject int: %d"), staticDefaultInt);

GetClass ()->GetClass ()->GetClass ()

最后来点丧心病狂的东西。上面我画了一张类图,UClass 也是 UObject 的子类。

![[6f4a4c16ca3964e8d81a7f7984fba90b_MD5.jpg]]

一个 UObject 实例可以调用 GetClass () 来获得它的 UClass,那么对 UClass 调用 GetClass () 会出现什么结果呢?在 MyActor 里我这么写:

![[48e4b8c5b43fb016de8c944ed1728c29_MD5.jpg]]

结果如下:

![[561e435fb6760478155967e25821838b_MD5.jpg]]

调用第一次 GetClass 和第二次产生的结果不一样,第三次以后结果是一样的了。至于为什么会这样我不知道,网上也没查出个所以然来,如果有谁弄懂请告诉我,我这里就先记录下来。

总结

  • UClass。存储类信息,用于反射。把它当成 C# 的 Type 来理解。
  • GetClass ()。获得一个 UObject 实例的 UClass,是 UObject 成员函数。
  • GetStaticClass ()。不需要有实例就能获得 UClass。是静态的,每次调用返回相同结果。
  • ClassDefaultObject。类默认对象,可以获得 UObject 初始化时的值。注意 GetClass ()->GetDefaultObject () 和 T:: StaticClass ()->GetDefaultObject () 不一样。

4 虚幻头文件工具 UHT

为利用 UObject 派生类型所提供的功能,需要在头文件上为这些类型执行一个预处理步骤,以核对需要的信息。该预处理步骤由 UnrealHeaderTool(简称 UHT) 执行。UObject 派生的类型需要遵守特定的结构。

头文件格式

UObject 在源(. cpp)文件中的实现与其他 C++ 类相似,其在头(. h)文件中的定义必须遵守特定的基础结构
UObject 派生类的基础头文件可能看起来与此相似,假定 UObject 派生物被称为 UMyObject,其创建时所在的项目被称为 MyProject:

1
2
3
4
5
6
7
8
9
10
11
#pragma once

#include 'Object.h'
#include 'MyObject.generated.h'

UCLASS()
class MYPROJECT_API UMyObject : public UObject
{
GENERATED_BODY()

};

虚幻引擎特定的部分如下:

  1. 此行预计为此文件中最后一个 #include 指令。如此头文件需要了解其他类,可将它们在文件中的任意位置提前声明,或包括在 MyObject. generated. h 上。
1
#include "MyObject.generated.h"
  1. UCLASS 宏使虚幻引擎能识别 UMyObject。此宏支持大量参数类说明符,参数决定类功能的开或关。
1
UCLASS()
  1. **如 MyProject 希望将 UMyObject 类公开到其他模块,则需要指定 MYPROJECT_API**。这对游戏项目将包括的模块或插件十分实用。这些模块和插件将故意使类公开,在多个项目间提供可携的自含式功能。
1
class MYPROJECT_API UMyObject : public UObject
  1. GENERATED_BODY 宏不获取参数,但会对类进行设置,以支持引擎要求的基础结构。所有 UCLASS 和 USTRUCT 均有此要求。

    1
    GENERATED_BODY() //GENERATED_BODY()宏必须被放置在类体的最前方。
  2. 虚幻头文件工具支持最下 C++集。当使用自定义 #ifdefs 宏包裹 UCLASS 的部分时,UHT 会忽略不包含 WITH_EDITOR 或者 WITHEDITORONLY_DATA 宏的宏。

5 头文件包含最佳实践

假设有两个类 A 和 B,类 A 要将类 B 的对象 (或者指正)作为自己的成员使用,并且类 B 将类 A 的对象 (或者指针)作为自己可以访问的数据,那么这个时候要在a.h 中 include b.h, 同时在b.h 中要 include a.h,但是相互包含是不可以的,这个时候就要用到类的前向声明了。

类的前向声明是利用了编译器的特性,编译器在编译的过程中只需要知道各个元素的名称和相应的大小就可以。而在 c++中每一个类的大小是固定的,这个时候使用前向声明的类就可以通过编译器。

  1. 前向声明的类不能定义对象。
  2. 可以用于定义指向这个类型的指针和引用。
  3. 用于申明使用该类型作为形参或返回类型的函数。

前向声明作用

根据其用途,前向声明的主要作用为:
(1)避免重复定义变量;
(2)避免引入函数定义/声明文件,从而函数文件发生更改时不会重新编译依赖文件;
(3)解决循环依赖问题。

前两种用途好理解,第三种稍微复杂点,但却是前向声明最重要的用途。其解决类 A 包含类 B,同时类 B 包含类 A 的依赖问题。循环依赖一般是设计层面的问题,可通过接口、引入辅助类等手段化解。前向声明也能解决,只是架构上稍微别扭。

不管 A 和 B 是否定义在同一个文件中,c++永远无法解决如下形式的循环依赖(后文解释原因):

![[8f6b7e7b2036f5566eedbe4acb0acd95_MD5.webp]]

前向声明解决该问题需要与指针配合,转换成另一种形式。要点如下:

  • 至少将某类的变量类型转换成指针,例如 A 中将 B 转成 B*;
  • 类 A 中对 B 使用前向声明;
  • 类 A 的定义文件中移除对类 B 文件的包含(做了包含保护则可忽略)。

使用前向声明后,以下是一种可行的解决形式(两个类均使用了前向声明):
![[v2-98995fabb527844ea3f831507501d1f0_720w.webp]]

深入前向声明

如果你有其他编程语言的经验,会发现c++有点怪异:Java/C#/Python/PHP等语言可以轻松做到循环引用,无需使用类似的前向声明技巧。这不禁让人思考:C++为何必须要用前向声明才能化解?

原因在于C++定义对象有两种方式:一种是A a形式,a即对象,调用成员变量或函数用.,对象在栈中分配;另一种是A a,a是指针,调用成员变量或函数用->,其指向地址存储实际对象,对象在堆中分配。*

分配对象需要知道具体的内存大小,但以下形式我们不能确定类A和类B对象的大小:

![[47c2208061f7dc3c5e25c131b851958f_MD5.webp]]

对于这个简单例子,你可以直观认为A和B占用同样的内存,例如1字节,但也可以是2字节,3字节等;根据内存对齐要求,一般是4字节,8字节等。无论哪种情况,编译器无法确定其对象占用内存,便会报错停止编译。所以你应该知道为什么C++永远不应该(不能)这样做了吧?

那为何前向声明加指针的组合能解决循环引用问题的呢?因为正常情况下,数据类型指针在同一机器的编译器里占同样的内存。指针一般是4或者8个字节,对应32和64位指针。用了指针,即使有循环引用,类的大小也能轻易的确定下来。这也是Java/C#/Python/PHP等可以轻松循环引用的原因:这些语言中,对象变量其实都是指针,也意味着对象变量都是引用传递。

如果不移除文件的相互包含,能否省去前向声明呢?答案是不能,原因如下:

  1. C++按照一个个编译单元(translation unit)进行编译,如果两个文件互相包含且没有 #pragma once 等包含保护措施,则会出现递归包含,编译器报错;
  2. 如果两个头文件都有文件包含保护,编译 A 时会把 B 包含进来,但因为 B 包含了 A,A 中的包含保护生效,导致 B 文件内的内容实际未引入 A,于是报 B 为未知符号的错误。

总的来说,不管是否移除对方的头文件,前向声明都是必须的。实践中为了避免文件变动时重新编译的耗费,移除不必要的头文件是一个好习惯。

前向声明和详细类型说明符

A.h 中使用 class 前向声明或详细类型说明符,在 A.cpp 中包含其他库的头文件,这样其他文件包含 A.h 时就可以减少引入的代码数量(防止代码膨胀)

![[Pasted image 20230903231808.png]]

Rider 的智能提示,比如我想声明 USphereComponent ,可以选择 2/3 都行

详细类型说明符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
//#include "Components/CapsuleComponent.h"
#include "Bird.generated.h"


UCLASS()
class PROJECTDN_API ABird : public APawn
{
GENERATED_BODY()
...
private:
UPROPERTY(VisibleAnywhere)
//UCapsuleComponent* Capsule; 这样写,需要引入CapsuleComponent.h头文件
class UCapsuleComponent* Capsule; //前向声明形式,不需要引入CapsuleComponent.h头文件
};

(推荐)前向声明:

1
2
3
4
5
6
7
8
9
10
11
class UCapsuleComponent; //前向声明形式,不需要引入CapsuleComponent.h头文件

UCLASS()
class PROJECTDN_API ABird : public APawn
{
GENERATED_BODY()
...
private:
UPROPERTY(VisibleAnywhere)
UCapsuleComponent* Capsule;
};

在 cpp 中引入头文件:

1
2
3
4
5
6
7
8
#include "ProjectDN/Public/Pawns/Bird.h"
#include "Components/CapsuleComponent.h" //在cpp中引入CapsuleComponent.h头文件

ABird::ABird()
{
PrimaryActorTick.bCanEverTick = true;
Capsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("CapsuleComponent"));
}

以下类型必须包含在头文件

  • 子类必须将父类的头文件包含在自己的头文件中
  • 当需要类型大小时
  • 需要访问类的成员变量和函数

三、结构体 USTRUCT

结构体(Struct) 是一种数据结构,帮助你组织和操作相关属性。在虚幻引擎中,结构体会被引擎的反射系统识别为 USTRUCT,但不属于 UObject 生态圈,且不能在 UClasse 的内部使用。

  • 在相同的数据布局下, USTRUCT 比 UObject 能更快创建。
  • USTRUCT支持UPROPERTY, 但它不由垃圾回收系统管理,不能提供UFUNCTION

实现USTRUCT

要把一个结构体变成 USTRUCT,请遵循以下步骤:

  1. 打开你要定义结构体的 header (.h) 文件。
  2. 要定义你的C++结构体,请将 USTRUCT 宏放在结构体定义的上方。
  3. 将 GENERATED_BODY() 宏作为定义的第一行。

其结果应该与下面的的例子一致:

1
2
3
4
5
USTRUCT([Specifier, Specifier, ...])
struct FStructName
{
GENERATED_BODY()
};

你可以用UPROPERTY来标记结构体的相关变量,使它们在虚幻反射系统和蓝图脚本中可见。

结构体说明符

结构体说明符 提供元数据,控制你的结构在引擎和编辑器中各方面的表现。

  • Atomic :表示该结构体应始终被序列化为一个单元。将不会为该类创建自动生成的代码。标头仅用于解析元数据。
  • BlueprintType: 将此结构体作为一种类型公开,可用于蓝图中的变量。
  • NoExport :将不会为该类创建自动生成的代码。标头仅用于解析元数据。

最佳做法与技巧

下面是一些使用 USTRUCT 时需要记住的有用提示:

  1. USTRUCT 可以使用虚幻引擎的智能指针和垃圾回收系统来防止垃圾回收删除 UObjects
  2. 结构体最好用于简单数据类型。对于你的项目中更复杂的交互,也许可以使用 UObject 或 AActor 子类来代替。
  3. USTRUCTs 不可以 用于复制。但是 UPROPERTY 变量 可以 用于复制。
  4. 虚幻引擎可以自动为结构体创建Make和Break函数。
    1. Make函数出现在任何带有 BlueprintType 标签的 USTRUCT 中。
    2. 如果在USTRUCT中至少有一个 BlueprintReadOnly 或 BlueprintReadWrite 属性,Break函数就会出现。
    3. Break 函数创建的纯节点为每个标记为 BlueprintReadOnly 或 BlueprintReadWrite 的资产提供一个输出引脚。

数据表

继承 FTableRowBase 之后可以用于 DataTable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
USTRUCT()
struct FInventoryItemInfo : public FTableRowBase
{
GENERATED_BODY()

UPROPERTY(EditAnywhere, BlueprintReadWrite)
FString ItemName UMETA(DisplayName="名称");

UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 index UMETA(DisplayName="编号");

UPROPERTY(EditAnywhere, BlueprintReadWrite)
bool bCanStaking UMETA(DisplayName="可否叠加");

UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 Count UMETA(DisplayName="数量");

UPROPERTY(EditAnywhere, BlueprintReadWrite)
UTexture2D* Icon UMETA(DisplayName="图标");

UPROPERTY(EditAnywhere, BlueprintReadWrite)
TSubclassOf<AActor> ItemClass;

UPROPERTY(EditAnywhere, BlueprintReadWrite)
USkeletalMesh* SkeletalMesh;
};

UPROPERTY(EditAnywhere,BlueprintReadWrite)
FInventoryItemInfo EmptyItem;

![[2a910ca10a8e8545b4aadc6ff0026ade_MD5.png]]

四、接口 UINTERFACE

接口类用于确保一组可能不相关的类实现一组公共的函数。在一些游戏功能可能由原本不相似的大型复杂类共享的情况下,这很有用。

游戏可能有这样一个系统,玩家角色进入触发器体积时可以激活陷阱、提醒敌人或向玩家奖励积分。这可以通过陷阱、敌人或积分奖励上的”ReactToTrigger”函数来实现。但是,陷阱可能派生自AActor,敌人派生自专门的APawnACharacter子类,而积分奖励派生自 UDataAsset 。

所有这些类都需要共享的功能,但除了 UObject 之外,没有其他公共的父类。在这种情况下,推荐使用接口。

1 接口声明

声明接口类与声明普通的虚幻类相似,但仍有两个主要区别。

  1. 首先,接口类使用 UINTERFACE 宏而不是 UCLASS 宏,继承 UInterface 而不是 UObject

 

1
2
3
4
5
UINTERFACE([specifier, specifier, ...], [meta(key=value, key=value, ...)])
class UClassName : public UInterface
{
GENERATED_BODY()
};
  1. 其次,UINTERFACE 类不是实际的接口;它是一个空白类,它的存在只是为了向虚幻引擎反射系统确保可见性。将由其他类继承的实际接口必须具有相同的类名,但是开头字母U必须改为I
title:ReactToTriggerInterface.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#pragma once

#include "ReactToTriggerInterface.generated.h"

//确保反射系统可见性
UINTERFACE(MinimalAPI, Blueprintable)
class UReactToTriggerInterface : public UInterface
{
GENERATED_BODY()
};

//实际接口
class IReactToTriggerInterface
{
GENERATED_BODY()

public:
/** 在此处添加接口函数声明 */
};
  • 前缀为 U的类不需要构造函数或任何其他函数
  • 前缀为 I的类将包含所有接口函数,且此类实际上将被你的其他类继承。

2 接口说明符

接口说明符用于向虚幻反射系统公开你的类,详见下表:

接口说明符 含义
Blueprintable ⭐将该类公开为可接受的用于创建蓝图的基类,让蓝图实现此接口
BlueprintType ⭐将该类公开为可用于蓝图中的变量的类型。
DependsOn=(ClassName1, ClassName2, ...) 所有列出的类都将在该类之前编译。ClassName必须在同一个(或上一个)包中指定一个类。多个依赖性类可以使用以逗号分隔的单个”DependsOn”行来指定,也可以使用单个”DependsOn”行为每个类指定。当一个类使用在另一个类中声明的结构体或枚举时,这一点非常重要,因为编译器只知道它已经编译的类中的内容。
MinimalAPI 仅导致该类的类型信息被导出以供其他模块使用。你可以向该类转换,但不能调用该类的函数(内联方法除外)。对于不需要其所有函数在其他模块中均可供访问的类,通过不导出这些类的所有内容,这可以缩短编译时间。

3 在 C++中实现接口

若要在一个新的类中使用你的接口,只需从前缀为 I 的接口类继承(除了你正在使用的任何基于 UObject的类)即可。

title:Trap.h
1
2
3
4
5
6
7
8
9
10
11
12
13
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "ReactToTriggerInterface.h"
#include "Trap.generated.h"

UCLASS(Blueprintable, Category="MyGame")
class ATrap : public AActor, public IReactToTriggerInterface
{
GENERATED_BODY()

public:
/** 在此处添加接口函数重载。 */
}

4 声明接口函数

有几种方法可以在接口中声明函数,由环境决定能够实现或调用哪种方法。
所有方法都必须在前缀为 I 的类中为接口声明,而且必须为 public ,以便对外部的类可见。

仅C++的接口函数

可以在接口的头文件中声明一个不带 UFUNCTION 说明的虚函数。这些函数必须为 virtual,以便在实现接口的类中重载它们。

title:ReactToTrigger.h
1
2
public:
virtual bool ReactToTrigger();

然后,可以在头文件本身或接口的 .cpp 文件中提供一个默认的实现。

title:ReactToTrigger.cpp
1
2
3
4
bool IReactToTriggerInterface::ReactToTrigger()
{
return false;
}

当在一个Actor类中实现接口后,可以创建并实现一个针对该类的重载。

title:Trap.h
1
2
public:
virtual bool ReactToTrigger() override;
title:Trap.cpp
1
2
3
4
bool ATrap::ReactToTrigger()
{
return false;
}

但是,这些C++接口函数对蓝图不可见。

蓝图可调用接口函数

要创建蓝图可调用的接口函数,必须在带 BlueprintCallable 说明符的函数声明中提供一个 UFUNCTION 宏。还必须使用 BlueprintImplementableEvent 或 BlueprintNativeEvent 说明,而且函数不能为virtual
接口中用 BlueprintImplementableEvent 标记函数也不需要实现为纯虚函数!仍可以正常继承

title:ReactToTrigger.h
1
2
3
4
public:
/**只能在蓝图中实现的React To Trigger版本。*/
UFUNCTION(BlueprintCallable, BlueprintImplementableEvent, Category=Trigger Reaction)
bool ReactToTrigger();
title:ReactToTrigger.h
1
2
3
4
public:
/**可以在C++或蓝图中实现的React To Trigger版本。*/
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category=Trigger Reaction)
bool ReactToTrigger();

BlueprintCallable引用实现接口的对象的 C++或蓝图可以调用使用 BlueprintCallable 说明符的函数。

BlueprintImplementableEvent:使用 BlueprintImplementableEvent 的函数不能在 C++中被重载,但可以在任何实现或继承接口的蓝图类中被重载。

BlueprintNativeEvent:在 C++中,可通过重载一个同名函数来实现使用 BlueprintNativeEvent 的函数,但要在末尾添加上后缀 _Implementation 。该说明符还允许在蓝图中重载实现

title:Trap.h
1
2
public:
bool ReactToTrigger_Implementation() override;
title:Trap.cpp
1
2
3
4
bool ATrap::ReactToTrigger_Implementation() const
{
return false;
}

5 确定类是否实现了接口

为了与实现接口的C++和蓝图类兼容,可以使用以下任意函数:

1
2
3
4
5
6
7
8
// 如果OriginalObject实现了UReactToTriggerInterface,则bisimplemated将为true。
bool bIsImplemented = OriginalObject->GetClass()->ImplementsInterface(UReactToTriggerInterface::StaticClass());

// 如果OriginalObject实现了UReactToTrigger,bIsImplemented将为true。
bIsImplemented = OriginalObject->Implements<UReactToTriggerInterface>();

// 如果OriginalObject实现了UReactToTriggerInterface,则ReactingObject将为非空。
IReactToTriggerInterface* ReactingObjectA = Cast<IReactToTriggerInterface>(OriginalObject);

如果 StaticClass() 函数在前缀为 I 的类中没有实现,尝试在前缀为 U 的类上使用 Cast 将失败,代码将无法编译。

6 转换类型

虚幻引擎的转换系统支持从一个接口转换到另一个接口,或者在适当的情况下,从一个接口转换到一个虚幻类型

1
2
3
4
5
IReactToTriggerInterface* ReactingObject = Cast<IReactToTriggerInterface>(OriginalObject); // 如果接口被实现,则ReactingObject将为非空。

ISomeOtherInterface* DifferentInterface = Cast<ISomeOtherInterface>(ReactingObject); // 如果ReactingObject为非空而且还实现了ISomeOtherInterface,则DifferentInterface将为非空。

AActor* Actor = Cast<AActor>(ReactingObject); // 如果ReactingObject为非空且OriginalObject为AActor或AActor派生的类,则Actor将为非空。

五、函数 UFUNCTION

UFUNCTION 是一种 C++函数,可以被虚幻引擎(UE)反射系统识别。 UObject 或蓝图函数库可将成员函数声明为 UFUNCTION,方法是将 UFUNCTION 宏放在头文件中函数声明上方的行中。

宏将支持 函数说明符 更改虚幻引擎解译和使用函数的方式。

1
2
UFUNCTION([specifier1=setting1, specifier2, ...], [meta(key1="value1", key2, ...)])
ReturnType FunctionName([Parameter1, Parameter2, ..., ParameterN1=DefaultValueN1, ParameterN2=DefaultValueN2]) [const];

可利用函数说明符将 UFUNCTION 对蓝图公开,以便开发者从蓝图资源调用或扩展 UFUNCTION,而无需更改 C++代码。

注意:子类继承父类的UFUNCITON 不需要添加 UFUNCITON 宏,否则编译错误

函数说明符

声明函数时,可以为声明添加 函数说明符,以控制函数相对于引擎和编辑器的各个方面的行为方式。

常用:

  1. BlueprintCallable :函数可从蓝图中调用,但只能通过 C++代码修改
    • 码将更改它所调用的对象的某些内容或一些其他全局状态,意味着必须调用它,必须连接白色执行线(调度它)。
    • BlueprintCallable函数的返回值会在执行这个函数的作用域(或者说执行流)中创建一个局部变量,而BlueprintPure不会
  2. BlueprintPure:函数可从蓝图中调用,但只能通过 C++代码修改
    • 不会更改它所调用的对象的任何内容或任何其他全局状态,它只需要输入,然后告诉您输出,类似于数学运算节点,不需要连接白色执行线。![[Pasted image 20230901194721.png]]
    • 每次与其输出相连的节点执行时重新计算其输出,,如果在单个节点内有进行大量计算而且其计算结果会多次使用的话,需要避免使用 BlueprintPure。
  3. BlueprintImplementableEvent:函数由其蓝图的子类实现,不应该在 C++中给出函数的实现,这会导致链接错误。
    • 如果没有返回值或输出参数,那么在蓝图中为一个事件
    • 如果它有返回值或任何输出参数,那么在蓝图中为一个函数
  4. BlueprintNativeEvent :函数提供一个 C++的默认实现,同时也可以被蓝图重载。默认实现的函数名为 函数名_Implementation
    • 如果需要,您仍然可以调用默认实现,方法是在事件或函数条目节点上 右键单击,并选择”将调用添加到父项(Add call to parent)”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//.h中声明
UFUNCTION(BlueprintNativeEvent)
void TestFunction(); //由蓝图重载
virtual void TestFunction_Implementation(); //由cpp实现

//.cpp中实现
//只需要实现名为_Implementation的默认实现
void ATestActor::TestFunction_Implementation()
{
//功能...
}

//.cpp中调用
//使用Execute_调用
Execute_TestFunction();
函数说明符 效果
BlueprintAuthorityOnly 如果在具有网络权限的机器上运行(服务器、专用服务器或单人游戏),此函数将仅从蓝图代码执行。
⭐ BlueprintCallable 此函数可在蓝图或关卡蓝图图表中执行。
BlueprintCosmetic 此函数为修饰性的,无法在专用服务器上运行。
⭐ BlueprintImplementableEvent 此函数可在蓝图或关卡蓝图图表中实现。
⭐ BlueprintNativeEvent 此函数旨在被蓝图覆盖掉,但是也具有默认原生实现。 用于声明名称与主函数相同的附加函数,但是末尾添加了_Implementation,是写入代码的位置。如果未找到任何蓝图覆盖,该自动生成的代码将调用_ Implementation 方法。
⭐ BlueprintPure Pure函数,此函数不对拥有它的对象产生任何影响,可在蓝图或关卡蓝图图表中执行。
⭐ CallInEditor 可通过细节(Details)面板中的按钮在编辑器中的选定实例上调用此函数。
⭐ Category = "TopCategory|SubCategory|Etc" 在蓝图编辑工具中显示时指定函数的类别。使用 | 运算符定义嵌套类别。
Client 此函数仅在拥有在其上调用此函数的对象的客户端上执行。用于声明名称与主函数相同的附加函数,但是末尾添加了_Implementation。必要时,此自动生成的代码将调用_ Implementation 方法。
CustomThunk UnrealHeaderTool 代码生成器将不为此函数生成thunk,用户需要自己通过 DECLARE_FUNCTION 或 DEFINE_FUNCTION 宏来提供thunk。
Exec 此函数可从游戏内控制台执行。仅在特定类中声明时,Exec命令才有效。
NetMulticast 此函数将在服务器上本地执行,也将复制到所有客户端上,无论该Actor的 NetOwner 为何。
Reliable 此函数将通过网络复制,并且一定会到达,即使出现带宽或网络错误。仅在与ClientServer配合使用时才有效。
SealedEvent 无法在子类中覆盖此函数。SealedEvent`关键词只能用于事件。对于非事件函数,请将它们声明为`static`或`final,以密封它们。
ServiceRequest 此函数为RPC(远程过程调用)服务请求。这意味着 NetMulticast 和 Reliable
ServiceResponse 此函数为RPC服务响应。这意味着 NetMulticast 和 Reliable
Server 此函数仅在服务器上执行。用于声明名称与主函数相同的附加函数,但是末尾添加了 _Implementation,是写入代码的位置。必要时,此自动生成的代码将调用 _Implementation 方法。
Unreliable 此函数将通过网络复制,但是可能会因带宽限制或网络错误而失败。仅在与ClientServer配合使用时才有效。
WithValidation 用于声明名称与主函数相同的附加函数,但是末尾需要添加_Validate。此函数使用相同的参数,但是会返回bool,以指示是否应继续调用主函数。

函数参数说明符

参数说明符 描述
Out 声明由引用传递的参数,使函数对其进行修改。
Optional 通过任选关键词可使部分函数参数变为任选,便于调用。任选参数的数值(调用方未指定)取决于函数。例如, SpawnActor 函数使用任选位置和旋转,默认为生成的 Actor 根组件的位置和旋转。添加 = [value] 参数可指定任选参数的默认值。例如: function myFunc(optional int x = -1) 。在多数情况下,如无数值被传递到任选参数,将使用变量类型的默认值或零(例如 0、false、””、none)。

Exec 控制台可调用函数

此函数可从游戏中的控制台中执行。Exec命令仅在特定类中声明时才产生作用。包括:

  • Pawns,
  • Player Controllers,
  • Player Input,
  • Cheat Managers,
  • Game Modes,
  • Game Instances,
  • overriden Game Engine classes,
  • Huds
1
2
3
4
5
6
7
8
UFUNCTION(Exec, Category = "methods")
void FunExec(float Value);


void AMyPawn::FunExec(float Value)
{
GEngine->AddOnScreenDebugMessage(-1, 1.0f, FColor::Blue, FString::Printf(TEXT("BPNative C++ Call Value:%f"), Value));
}

![[942fee0c636bb0a81e921eaa99e52bd0_MD5.png]]

六、参数 UPARAM

主要用于将 C++ 代码公开到蓝图

若要使参数通过非const引用传递并仍然显示为输入,请使用 UPARAM(ref) 宏。

![[Pasted image 20230829220832.png]]

1
2
UFUNCTION(BlueprintCallable, Category = "Example Nodes")
static void HandleTargets(UPARAM(ref) TArray<FVector>& InputLocations, TArray<FVector>& OutputLocations);

您还可以使用UPARAM()更改引脚的显示名称。例如, MakeRotator 函数使用”UPARAM()DisplayName关键字来更改绕 Z 轴的旋转、绕 X 轴的旋转、绕 Y 轴的旋转参数在蓝图中的显示方式。

![[54948c4fcd6856e0a406abb1d203171e_MD5.jpg]]

1
2
3
4
5
6
/** 根据采用度数的旋转值制作一个旋转体{绕Z轴的旋转、绕X轴的旋转、绕Y轴的旋转} */
UFUNCTION(BlueprintPure, Category="Math|Rotator", meta=(Keywords="construct build rotation rotate rotator makerotator", NativeMakeFunc))
static FRotator MakeRotator(
UPARAM(DisplayName="X (Roll)") float Roll,
UPARAM(DisplayName="Y (Pitch)") float Pitch,
UPARAM(DisplayName="Z (Yaw)") float Yaw);

七、元数据说明符

文档:
虚幻引擎元数据说明符 | 虚幻引擎5.2文档 (unrealengine.com)

声明类、接口、结构体、列举、列举值、函数,或属性时,可添加 元数据说明符 来控制其与引擎和编辑器各方面的交互方式
每一种类型的数据结构或成员都有自己的元数据说明符列表。

[!warning]
Metadata 只存在于编辑器中。请不要编写能够访问到 Metadata 的游戏逻辑。

要添加元数据说明符,需使用单词 meta,后接说明符列表。如有必要,可以将它们的值添加到 UCLASSUENUMUINTERFACEUSTRUCTUFUNCTION 或 UPROPERTY 宏,如下所示:

1
{UCLASS/UENUM/UINTERFACE/USTRUCT/UFUNCTION/UPROPERTY}(SpecifierX, meta=(MetaTag1="Value1", MetaTag2, ..), SpecifierY)

属性说明符

DisplayName 别名,可以便于蓝图变量搜索,如果变量名不好记的话

EditCondition 是否可编辑:支持 bool、比较等条件判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
UPROPERTY(EditAnywhere,BlueprintReadWrite, meta = (DisplayName="UseOffset1、2 标志位") )
bool bUseOffset;

UPROPERTY(EditAnywhere, meta = (DisplayName = "ContitionValue 控制 offset3"))
int32 ContitionValue;

UPROPERTY(EditAnywhere, meta = (DisplayName = "ContitionColorBits 控制 offset4"))
EColorBits1 ContitionColorBits;

UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "bUseOffset"))
float Offset1;

UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "!bUseOffset"))
float Offset2;

UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "ContitionValue>0"))
float Offset3;

UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "ContitionColorBits==EColorBits1::ECB_Green"))
float Offset4;

![[9eeca8d2381f93f451dfd5c02844ce29_MD5.png]]

**BindWidget**: 和 UMG 同名同类型控件绑定

1
2
3
4
5
UPROPERTY(EditAnywhere, meta = (BindWidget))
UImage* TCanvas;

UPROPERTY(EditAnywhere, BlueprintReadWrite , meta = (BindWidget))
UButton* Btn_Mass;

UMETA :扩展宏

  • cpp meta里的中文在蓝图经常会乱码
    • 解决方法一:高级保存选择 Unicode 65001
    • 解决办法二:使用UMETA中的DisplayName
  • 可用于Enum的元素别名
  • 可用于结构体的元素别名
  • 可用于Datatable的别名
1
2
3
4
5
6
7
8
UPROPERTY(EditAnywhere)
FName ChineseName UMETA(DisplayName="中文名");

UPROPERTY(EditAnywhere)
float Weight UMETA(DisplayName = "体重") = 65.0f;

UPROPERTY(EditAnywhere)
EColorBits1 FavoriteColorBits UMETA(DisplayName = "最喜欢的颜色")=EColorBits1::ECB_Blue;

![[b8afbd1280f8a8633fc10e17329ba58a_MD5.png]]

[AllowPrivateAccess]:允许从蓝图中访问标记为 BluperintReadOnlyBlueprintReadWrite 的私有成员

1
2
3
4
//不加AllowPrivateAccess就会编译错误
private:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MyClass|Test",meta=(AllowPrivateAccess=true))
int32 TestCPP;

函数说明符

ExpandEnumAsExecs

1
2
3
4
5
6
7
8
9
10
UENUM(BlueprintType)
enum class BranchOutput : uint8
{
Branch0,
Branch1,
Branch2,
};

UFUNCTION(BlueprintCallable, Category = "methods", Meta = (ExpandEnumAsExecs = "Branches"))
void FunExpandEnumAsExecs(int32 Input, BranchOutput& Branches);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void AMyActor::FunExpandEnumAsExecs(int32 Input, BranchOutput& Branches)
{
if (Input == 0)
{
Branches = BranchOutput::Branch0;
}
else if(Input == 1)
{
Branches = BranchOutput::Branch1;
}
else
{
Branches = BranchOutput::Branch2;
}
}

![[9d2428a628a890cc0469f887bcfc294e_MD5.png]]

九、断言

assert 的关键特性之一是不存在于发布代码中,这意味着不但不会影响发布产品的性能,也没有任何副作用对 assert 最简单的理解就是:”断言”必须一律为 true,否则程序会停止运行。

虚幻引擎提供 assert 等同项的三个不同族系:checkverify 和 ensure。各个功能的行为略有不同,但它们都是开发期间使用的诊断工具,目标大致相同。

Check

Check 族系最接近基础 assert,因为当第一个参数得出的值为 false 时,此族系的成员会停止执行,且默认不会在发布版本中运行。以下 Check 宏可用:

1
2
3
4
5
6
7
8
9
10
11
12
//当Expression为以下值时触发断言
//空指针
//0
//false
check(Expression);
check(Expression && "Expression can not be NULL");

//附加报错文本
checkf(Expression ,TEXT("Expression can not be NULL"))

//代码执行到这一行就触发断言
checkNoEntry();
参数 行为
check 或 checkSlow Expression 若 Expression 为 false,停止执行
checkf 或 checkfSlow ExpressionFormattedText... 若 Expression 为 false,则停止执行并将 FormattedText 输出到日志
checkCode Code 在运行一次的 do-while 循环结构中执行 Code;主要用于准备另一个 Check 所需的信息
checkNoEntry (无) 若此行被 hit,则停止执行,类似于 check(false),但主要用于应不可到达的代码路径
checkNoReentry (无) 若此行被 hit 超过一次,则停止执行
checkNoRecursion (无) 若此行被 hit 超过一次而未离开作用域(递归),则停止执行
unimplemented (无) 若此行被 hit,则停止执行,类似于 check(false),但主要用于应被覆盖而不会被调用的虚拟函数

[!NOTE] 此行被 hit 的含义
这句话意思就是代码执行到这一行

Verify

若某个函数执行操作,然后返回 bool 来说明该操作是否成功,则应使用 Verify 而非 Check 来确保该操作成功。
因为在发布版本中 Verify 将忽略返回值,但仍将执行操作。而 Check 在发布版本中根本不调用该函数,所以行为才会有所不同。

1
2
// 使用Verify而非Check,因为表达式需要先执行操作———设置网格体。
verify((Mesh = GetRenderMesh()) != nullptr);
参数 行为
verify 或 verifySlow Expression 若 Expression 为 false,停止执行
verifyf 或 verifyfSlow ExpressionFormattedText... 若 Expression 为 false,则停止执行并将 FormattedText 输出到日志

Ensure

Ensure 宏的表达式计算得出的值为 false,将报告崩溃,仍会继续运行(即可以在编辑器点击继续运行)

为避免崩溃报告器收到太多通知,Ensure 宏在每次引擎或编辑器会话中仅报告一次。若实际情况需要 Ensure 宏在每次表达式计算得值为 false 时都报告一次,则使用”Always”版本的宏。

1
2
3
4
5
6
7
8
9
10
11
12
// 这行代码捕获了在产品发布版本中可能出现的小错误。
// 此错误较小,无需停止执行便可解决。
// 虽然该bug已修复,但开发者仍然希望了解之前是否曾经出现过此bug。
void AMyActor::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
// 确保bWasInitialized为true,然后再继续。若为false,则在日志中记录该bug尚未修复。
if (ensureMsgf(bWasInitialized, TEXT("%s ran Tick() with bWasInitialized == false"), *GetActorLabel()))
{
//(执行一些需要已正确初始化AMyActor的操作。)
}
}
参数 行为
ensure Expression Expression 首次为 false 时通知崩溃报告器
ensureMsgf ExpressionFormattedText... Expression 首次为 false 时通知崩溃报告器并将 FormattedText 输出到日志
ensureAlways Expression Expression 为 false 时通知崩溃报告器
ensureAlwaysMsgf ExpressionFormattedText... Expression 为 false 时通知崩溃报告器并将 FormattedText 输出到日志

创建蓝图 API:提示和技巧

在虚幻引擎中将C++暴露给蓝图 | 虚幻引擎5.2文档 (unrealengine.com)
程序员创建对蓝图公开的 API 时需要考虑以下几点:

  • 可选参数便于在蓝图中处理:
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 将字符串显示到日志中,也可选择显示到屏幕上。
* 如 Print To Log 为 true,它将显示在 Output Log 窗口中。否则它将被记录为"Verbose",通常不会显示。
*
* @param InString 登出字符串
* @param bPrintToScreen 是否将输出显示到屏幕上
* @param bPrintToLog 是否将输出保存到日志中
* @param bPrintToConsole 是否将输出显示到控制台
* @param TextColor 是否将输出显示到控制台
*/
UFUNCTION(BlueprintCallable, meta=(WorldContext="WorldContextObject", CallableWithoutWorldContext, Keywords = "log print", AdvancedDisplay = "2"), Category="Utilities|String")
static void PrintString(UObject* WorldContextObject, const FString& InString = FString(TEXT("Hello")), bool bPrintToScreen = true, bool bPrintToLog = true, FLinearColor TextColor = FLinearColor(0.0,0.66,1.0));
  • 在带大量返回参数的函数和返回结构体的函数之间优先前者。以下片段显示如何在节点上创建多个输出引脚:
1
2
UFUNCTION(BlueprintCallable, Category = "Example Nodes")
static void MultipleOutputs(int32& OutputInteger, FVector& OutputVector);
  • 可在现有函数上添加新参数,但如果要进行移除或变更,则需要否决原始函数并添加一个新函数。必须使用否决元数据,使新函数的信息显示在蓝图中:
1
2
UFUNCTION(BlueprintCallable, Category="Collision", meta=(DeprecatedFunction, DeprecationMessage = "Use new CapsuleOverlapActors", WorldContext="WorldContextObject", AutoCreateRefTerm="ActorsToIgnore"))
static ENGINE_API bool CapsuleOverlapActors_DEPRECATED(UObject* WorldContextObject, const FVector CapsulePos, float Radius, float HalfHeight, EOverlapFilterOption Filter, UClass* ActorClassFilter, const TArray<AActor*>& ActorsToIgnore, TArray<class AActor*>& OutActors);
  • 如果函数需要接受枚举,考虑将”expand enum as execs”用作元数据,可使节点更易于使用。
1
2
UFUNCTION(BlueprintCallable, Category = "DataTable", meta = (ExpandEnumAsExecs="OutResult", DataTablePin="CurveTable"))
static void EvaluateCurveTableRow(UCurveTable* CurveTable, FName RowName, float InXY, TEnumAsByte<EEvaluateCurveTableResult::Type>& OutResult, float& OutXY);
  • 许多完成耗时较长的操作(如 move here)均为隐藏函数。
1
2
3
4
5
6
7
8
9
/**
* 执行带延迟的隐藏操作。
*
* @param WorldContext 世界背景。
* @param Duration 延迟长度。
* @param LatentInfo 隐藏操作。
*/
UFUNCTION(BlueprintCallable, Category="Utilities|FlowControl", meta=(Latent, WorldContext="WorldContextObject", LatentInfo="LatentInfo", Duration="0.2"))
static void Delay(UObject* WorldContextObject, float Duration, struct FLatentActionInfo LatentInfo );
  • 如有可能,考虑将函数放入共享库。便于在多个类之间使用,避开”target”引脚。
1
class DOCUMENTATIONCODE_API UTestBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
  • 尽可能将节点标记为纯,可避免在节点上使用连线的执行引脚。
1
2
3
/* 在 0 和 最大 - 1 之间返回一致分配的随机数 */
UFUNCTION(BlueprintPure, Category="Math|Random")
static int32 RandomInteger(int32 Max);
  • 将一个函数标记为 const 也可使蓝图节点不带执行引脚:
1
2
3
4
5
6
/**
* 获得 actor 到世界的转换。
* @return 从 actor 空间转换到世界空间的转换。
*/
UFUNCTION(BlueprintCallable, meta=(DisplayName = "GetActorTransform"), Category="Utilities|Transformation")
FTran