命名规范
在 Unreal Engine 中,类名的前缀有严格的命名规则。通常来说,类的前缀应该根据其类型来定义:
U
用于UObject
类(例如UObject
、AActor
)。A
用于继承自Actor
的类。F
用于普通结构体类。I
用于接口
反射原理
在 Unreal Engine 中,反射系统通过一套预编译的机制和元数据来实现查找对应反射类的功能。反射的核心是 UClass
,它代表了一个具体的类类型,而这些类的元数据则由编译器在编译时生成并绑定到类的二进制文件中。
反射类的查找机制:
-
编译时生成反射信息:
- 在 Unreal Engine 中,反射信息并不是直接从文件系统中查找的,而是在编译时由宏(如
UCLASS()
,UPROPERTY()
,UFUNCTION()
)生成的。 - 这些宏会通过
GENERATED_BODY()
或GENERATED_UCLASS_BODY()
宏触发代码生成,生成包含类反射信息的数据结构。具体来说,UClass
类和UProperty
、UFunction
等元数据结构会在编译时由 Unreal Header Tool (UHT) 生成,并嵌入到最终的二进制文件中。
- 在 Unreal Engine 中,反射信息并不是直接从文件系统中查找的,而是在编译时由宏(如
-
反射元数据的存储位置:
- 在 Unreal Engine 的反射系统中,所有的反射信息(例如类、属性、方法)都被存储在二进制文件(.uasset 或 .dll)中。对于每个类,UHT 会生成一个与之相关的
UClass
实例,包含该类的类型信息、属性、方法等信息,并将这些信息嵌入到最终的二进制文件里。 - 反射类的定义与实现会被链接到一个“反射数据表”中,Unreal Engine 会在运行时通过查找这些数据表来查找和访问
UClass
、UProperty
、UFunction
等反射信息。
- 在 Unreal Engine 的反射系统中,所有的反射信息(例如类、属性、方法)都被存储在二进制文件(.uasset 或 .dll)中。对于每个类,UHT 会生成一个与之相关的
-
运行时查找反射类:
- 当你在运行时请求反射信息时,Unreal Engine 会使用一套全局注册表机制来查找类信息。每个
UClass
都会在某个内存位置或表中进行注册,这个表可以在运行时用于查找类和其相关的元数据。
具体来说:
- 当
UClass
被创建时,它会被注册到一个全局的UClass
树或全局UObject
注册表中。这个全局注册表是由 Unreal Engine 引擎在启动时建立的,包含了所有反射类型的信息。 StaticClass()
方法通常是用于查找一个特定类的入口点。例如,如果你调用AMyActor::StaticClass()
,引擎会查找并返回与AMyActor
类相关的UClass
实例。
UClass* MyClass = AMyActor::StaticClass();
- 当你在运行时请求反射信息时,Unreal Engine 会使用一套全局注册表机制来查找类信息。每个
-
UClass 的查找过程:
- 当你通过
StaticClass()
或其他相关的函数查找类时,Unreal Engine 会查询该类的UClass
注册表。例如,StaticClass()
会返回类的类型元数据,或者通过字符串查找对应的类。 FLinkerLoad
类负责将反射类(UClass
)信息加载到内存中。当引擎启动或加载某个模块时,它会将该模块的反射信息加载到内存中的全局类注册表中,确保能够在运行时快速查找到所有反射类。
- 当你通过
-
虚拟表 (Vtable) 和反射:
- 在运行时,通过虚拟表(Vtable)与反射系统结合,可以实现多态和动态类型信息查询。
UClass
内部会保存指向类的虚拟表的指针,从而允许通过运行时的类型信息(如反射)来查询类的属性、方法等。
- 在运行时,通过虚拟表(Vtable)与反射系统结合,可以实现多态和动态类型信息查询。
举个例子:
假设我们有一个类 AMyActor
,它的 UClass
元数据已经通过宏 UCLASS()
和 UPROPERTY()
等生成了相应的信息。
-
UClass 的注册: 当
AMyActor
类被编译时,UHT 会生成一个与该类对应的UClass
对象。这个UClass
包含该类的所有元数据(如类名、继承关系、属性和方法)。 -
通过 StaticClass() 查找: 当我们在代码中调用
AMyActor::StaticClass()
时,Unreal Engine 会查询全局的类注册表,找到AMyActor
对应的UClass
对象。这个对象会包含类的反射信息(属性、方法等)。 -
使用反射数据: 通过
UClass
对象,程序可以动态访问该类的属性、方法等元数据。这使得你能够在运行时操作类的实例,修改属性值,调用方法,甚至是动态创建对象。
总结:
反射类文件的查找并不是直接通过文件系统进行的,而是通过编译时生成的反射信息和引擎内部的全局注册表来完成的。这些反射信息被嵌入到编译后的二进制文件(如 .dll
或 .uasset
文件)中,并通过引擎在运行时的反射系统进行查找和访问。当你通过 StaticClass()
或其他相关方法查询时,实际上是通过引擎的反射注册表来定位类和获取相关的元数据。
一:头文件
#include "UObject/NoExportTyPes.h"
#include "UObject/NoExportTypes.h"
是 Unreal Engine 中的一个头文件,它用于声明一些不需要被导出到其他模块或库中的类型。通常,它包含了一些基础类的声明,例如 UObject
或者其他不需要导出到其他模块的类。
具体来说,NoExportTypes.h
头文件常用于以下几种情况:
1. 定义基础类型
NoExportTypes.h
通常包含一些 Unreal Engine 中的基础类型或类(例如 UObject
的基类)。它为在项目内部使用的类提供声明,而不需要导出到其他模块。
2. 减少编译依赖
如果你只是需要引用 UObject
或一些基础类型(如 FVector
、FRotator
等)而不需要导出这些类型到其他模块,可以包含该头文件以减少不必要的依赖。
3. 简化头文件管理
在 Unreal Engine 中,如果你创建的是一些基础类(例如派生自 UObject
的类),而这些类不会被暴露给外部模块,你就不需要在头文件中包含较重的头文件(比如 UObject/ObjectMacros.h
),从而优化编译速度和管理。
二:常见宏
UE_LOG
UE_LOG
宏的基本语法:
UE_LOG(LogCategory, LogLevel, TEXT("LogMessage"));
参数说明:
- LogCategory:指定日志的类别,通常是一个静态的日志类别(例如
LogTemp
,LogMyCategory
)。 - LogLevel:指定日志的严重级别。常见的日志级别有:
Log
: 普通日志消息,适用于常规的信息输出。Warning
: 警告信息,表示可能出现潜在问题。Error
: 错误信息,表示发生了问题或异常。Fatal
: 致命错误,通常会导致程序崩溃或终止。
- TEXT("LogMessage"):实际输出的日志内容。通常使用
TEXT
宏来支持 Unicode 字符串。
示例用法:
常用的日志类别:
LogTemp
:临时使用的日志类别。LogBlueprintUserMessages
:蓝图的用户消息日志。LogScript
:脚本相关的日志。LogAI
:AI相关的日志。LogNetworking
:网络相关的日志。
输出日志的级别:
- Log:一般信息,通常用于显示程序运行状态。
- Warning:警告,提示可能存在的潜在问题。
- Error:错误,表示遇到问题,可能需要修复。
- Fatal:致命错误,通常会导致程序崩溃。
UFUNCTION
UFUNCTION
是 Unreal Engine 中用于标记 C++ 函数的宏,它告诉 Unreal Engine 该函数可以在蓝图中调用、序列化、网络复制、或者具备其他特定功能。UFUNCTION
使得 C++ 函数能够与 Unreal 的反射系统和其他引擎特性集成。
基本语法
UFUNCTION(Modifier1, Modifier2, ...) void FunctionName();
常用的 UFUNCTION
修饰符:
- BlueprintCallable:允许函数从蓝图中调用。
- BlueprintPure:该函数是纯函数,不会修改对象的状态,可以在蓝图中作为计算节点使用。
- BlueprintImplementableEvent:声明一个事件,该事件需要在蓝图中实现。
- BlueprintNativeEvent:声明一个可以在蓝图或 C++ 中实现的事件。
- Category:用于为函数指定类别,方便在蓝图编辑器中组织。
- Server/Client/NetMulticast:用于网络相关的函数,标记函数应该在哪些端(服务器、客户端)执行。
- WithValidation:为函数添加验证逻辑。
- AllowPrivateAccess:允许在蓝图中访问私有成员函数。
例:
在蓝图中GetCurrentValue将会被归纳于ShuYuanToolClass中
当然Category并非必须要求的,即使不写也不会影响编译
如图
网络相关:Server
, Client
, NetMulticast
这些修饰符用于指定函数在网络中的执行方式。例如,在多人游戏中,你可能想要特定的函数只在服务器上运行,或者在所有客户端上同步执行。
- Server:标记该函数只能在服务器上执行。
- Client:标记该函数只能在客户端上执行。
- NetMulticast:标记该函数在服务器和所有客户端上都会执行。
UFUNCTION(Server, Reliable, WithValidation) void ServerDoSomething();
- 该函数
ServerDoSomething
只在服务器上调用,Reliable
表示该函数是可靠的,WithValidation
表示需要验证。
AllowPrivateAccess
允许在蓝图中访问私有成员函数或属性。通常用于保护函数,防止直接访问,但允许在蓝图中使用。
UFUNCTION(BlueprintCallable, Category = "MyCategory", AllowPrivateAccess = "TRUE") void SetHealth(int NewHealth);
- 使得该私有函数可以在蓝图中调用。
WithValidation
允许函数进行验证。例如,网络函数可以使用 WithValidation
来验证调用的有效性。
UFUNCTION(Server, WithValidation) void ServerDoSomething();
WithValidation
标志要求为该函数提供一个_Validate
函数,以验证是否可以执行。
完整示例
UCLASS()
class MYGAME_API AMyCharacter : public ACharacter
{
GENERATED_BODY()
public:
// BlueprintCallable 函数,允许从蓝图调用
UFUNCTION(BlueprintCallable, Category = "Health")
void SetHealth(int NewHealth);
// BlueprintPure 函数,纯计算函数,不修改状态
UFUNCTION(BlueprintPure, Category = "Health")
int GetHealth() const;
// BlueprintNativeEvent,可以在 C++ 或蓝图中实现
UFUNCTION(BlueprintNativeEvent, Category = "Combat")
void OnHit();
virtual void OnHit_Implementation();
// 网络函数,只在服务器执行
UFUNCTION(Server, Reliable, WithValidation)
void ServerHeal(int HealAmount);
void ServerHeal_Implementation(int HealAmount);
bool ServerHeal_Validate(int HealAmount);
};
三:属性
参考文档
Unreal Engine UProperties | 虚幻引擎 5.5 文档 | Epic Developer Community
属性的声明
一、属性声明格式概述
UE5 中,属性声明通过 UPROPERTY
宏实现,其基本格式为:
UPROPERTY([specifier, specifier, ...], [meta(key=value, key=value, ...)]) Type VariableName;
specifier
:属性修饰符,控制属性的行为(如可见性、复制规则等)。meta
:元数据,用于编辑器、蓝图、序列化等场景的额外配置。Type
:属性类型(如int32
、FString
、AActor*
等)。VariableName
:属性变量名。
二、meta
元数据的作用
meta
是 UPROPERTY
的可选部分,用于为属性提供 编辑器行为 和 运行时逻辑 的额外配置。其核心作用包括:
- 编辑器显示控制
- 定义属性在 细节面板 中的显示方式(如分组、排序、工具提示)。
- 蓝图集成
- 控制属性在 蓝图 中的可见性、可编辑性及默认值。
- 序列化与数据验证
- 配置属性的序列化行为(如保存/加载)及运行时校验规则。
- 高级功能扩展
- 支持自定义逻辑(如回调函数、条件约束)。
三、常用 meta
键值对及其作用
以下是一些常用的 meta
键值对及其具体用途:
键 | 值 | 作用 |
---|---|---|
Category | 字符串(如 "Gameplay" ) | 将属性分组到指定的分类中,便于在细节面板中查找。 |
ToolTip | 字符串(如 "玩家生命值" ) | 为属性添加工具提示,鼠标悬停时显示。 |
DisplayName | 字符串(如 "生命值" ) | 在编辑器中显示的自定义名称,替代变量名。 |
ClampMin | 数值(如 0 ) | 限制属性的最小值(适用于数值类型)。 |
ClampMax | 数值(如 100 ) | 限制属性的最大值(适用于数值类型)。 |
UIMin | 数值(如 0 ) | 在细节面板中设置滑块的最小值。 |
UIMax | 数值(如 100 ) | 在细节面板中设置滑块的最大值。 |
EditCondition | 布尔表达式(如 bIsEditable ) | 控 制属性的可编辑性(如根据其他属性动态启用/禁用)。 |
BlueprintReadOnly | true /false | 在蓝图中设置为只读属性。 |
ExposeOnSpawn | true /false | 在生成对象时(如 SpawnActor )将属性暴露为可配置参数。 |
AllowPrivateAccess | true /false | 允许蓝图访问私有属性。 |
四、meta
的典型应用场景
1. 编辑器显示优化
UPROPERTY(EditAnywhere, Category = "Gameplay", meta = (DisplayName = "生命值", ToolTip = "玩家的当前生命值"))
int32 Health;
- 效果:在细节面板中显示为 “生命值”,鼠标悬停时显示提示信息。
2. 数值范围限制
UPROPERTY(EditAnywhere, meta = (ClampMin = 0, ClampMax = 100, UIMin = 0, UIMax = 100))
float Stamina;
- 效果:限制
Stamina
的取值范围,并在细节面板中显示滑块。
3. 条件编辑控制
UPROPERTY(EditAnywhere, meta = (EditCondition = "bIsEditable"))
FString CustomName;
- 效果:仅当
bIsEditable
为true
时,CustomName
可编辑。
4. 蓝图集成
UPROPERTY(EditAnywhere, BlueprintReadOnly, meta = (ExposeOnSpawn = true))
AActor* TargetActor;
- 效果:在生成对象时,
TargetActor
可作为参数配置,且在蓝图中只读。
效果如下
五、meta
的高级用法
-
自定义元数据
通过UCLASS
或UFUNCTION
定义自定义元数据,扩展引擎功能。UPROPERTY(EditAnywhere, meta = (CustomKey = "CustomValue")) FString CustomProperty;
-
动态元数据
在运行时通过反射获取元数据,实现动态逻辑。if (Property->HasMetaData("CustomKey")) { FString Value = Property->GetMetaData("CustomKey"); }
整型
若需要在蓝图中使用需要添加宏UPROPERTY(),如下
若该变量只是用作纯函数运算使用则可不必添加宏UPROPERTY()
作为位掩码
整数属性现在可以位掩码形式公开给编辑器。要将整数属性标记为位掩码,只需在meta分段中添加"bitmask"即可,如下所示:
/*~ BasicBits appears as a list of generic flags in the editor, instead of an integer field. */
UPROPERTY(EditAnywhere, Meta = (Bitmask))
int32 BasicBits;
关于位掩码
一、什么是位掩码?
位掩码(Bitmask)是一种 利用二进制位存储和操作多个布尔标志 的技术。它通过将一个整数(如 int32
或 int64
)的每一位(bit)视为一个独立的标志,实现高效的数据存储与操作。
核心特点
- 二进制表示:每个标志对应一个二进制位,
1
表示启用,0
表示禁用。 - 紧凑存储:多个标志存储在一个整数中,节省内存。
- 高效操作:通过位运算(如
&
、|
、~
)快速读取、设置和清除标志。
二、位掩码的用途
- 状态管理
- 示例:用位掩码存储角色的状态(如是否跳跃、是否攻击、是否隐身等)。
enum class ECharacterState : int32 { None = 0, IsJumping = (1 << 0), IsAttacking = (1 << 1), IsInvisible = (1 << 2) }; int32 CharacterState = static_cast<int32>(ECharacterState::IsJumping) | static_cast<int32>(ECharacterState::IsAttacking);
- 示例:用位掩码存储角色的状态(如是否跳跃、是否攻击、是否隐身等)。
- 选项配置
- 示例:用位掩码存储游戏设置的选项(如是否启用音效、是否显示字幕等)。
enum class EGameOptions : int32 { None = 0, EnableSound = (1 << 0), ShowSubtitles = (1 << 1), FullscreenMode = (1 << 2) }; int32 GameOptions = static_cast<int32>(EGameOptions::EnableSound) | static_cast<int32>(EGameOptions::ShowSubtitles);
- 示例:用位掩码存储游戏设置的选项(如是否启用音效、是否显示字幕等)。
- 权限控制
- 示例:用位掩码表示用户的权限(如读、写、执行等)。
enum class EUserPermissions : int32 { None = 0, Read = (1 << 0), Write = (1 << 1), Execute = (1 << 2) }; int32 UserPermissions = static_cast<int32>(EUserPermissions::Read) | static_cast<int32>(EUserPermissions::Write);
- 示例:用位掩码表示用户的权限(如读、写、执行等)。
三、为什么要使用位掩码?
- 节省内存
- 多个布尔标志存储在一个整数中,避免为每个标志分配单独的内存空间。
- 提高性能
- 位运算(如
&
、|
、~
)是 CPU 的原生操作,速度极快。
- 位运算(如
- 简化代码
- 通过枚举和位运算,代码更简洁易读。
- 灵活扩展
- 添加新标志只需增加一个枚举值,无需修改数据结构。
- 编辑器支持
- 在 UE5 中,位掩码属性可以在编辑器中直观地操作,提升开发效率。
蓝图中效果如下
你也可以让蓝图可调用函数的整型参数表现为位掩码,方法是在参数的 UPARAM
指定器上添加 Bitmask
元标签(不需要值)。
/*~ You can set MyFunction using a generic list of flags instead of typing in an integer value. */
UFUNCTION(BlueprintCallable)
void MyFunction(UPARAM(meta=(Bitmask)) int32 BasicBitsParam)
为了自定义位标记名称,首先必须使用"bitflags"元标记来创建UENUM:
UENUM(Meta = (Bitflags))
enum class EColorBits
{
ECB_Red,
ECB_Green,
ECB_Blue
};
作为另一种声明方式,你可以使用 ENUM_CLASS_FLAGS
在定义完枚举类型后,将其变成一个位掩码。为了在编辑器中使用标志选择器(flag selector),我们还必须添加元字段 UseEnumValuesAsMaskValuesInEditor
并将其设置为 true
。关键的区别在于,这个方法直接使用掩码值,而不是比特数。使用此方法制作的等效枚举类型看起来像这样:
UENUM(Meta = (Bitflags, UseEnumValuesAsMaskValuesInEditor = "true"))
enum class EColorBits
{
ECB_Red = 0x01,
ECB_Green = 0x02,
ECB_Blue = 0x04
};
ENUM_CLASS_FLAGS(EColorBits);
创建该UENUM后,可以使用"BitmaskEnum"元标记来引用它,如:
/*~ This property lists flags matching the names of values from 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)。
字符串
简单的理解这3类字符串
FString:就是平常作为参数还在变量的string数据类型
FName:可以理解为全局的const string
FText:例如在不同语言版本中中文角色名称为 书鸢, 英文为Shu Yuan
但是在代码内他们都是FText CharName变量;
详细见
虚幻引擎中的字符串处理 | 虚幻引擎 5.5 文档 | Epic Developer Community
关于初始化
- 字符串字面量类型
- 写法一:
"MyFstring"
是普通的 C 风格字符串字面量(const char*
)。 - 写法二:
TEXT("MyFstring")
是 UE5 的宏,将字符串字面量转换为宽字符(TCHAR*
)。
- 写法一:
- 字符编码
- 写法一:使用窄字符编码(通常是 ANSI 或 UTF-8)。
- 写法二:使用宽字符编码(通常是 UTF-16),确保跨平台兼容性。
- UE5 兼容性
- 写法一:在 UE5 中可能引发字符编码问题,尤其是在非英文字符场景下。
- 写法二:推荐使用,确保字符串在 UE5 中正确处理(如本地化、跨平台支持)。
适用场景
- 写法一
- 场景:简单的英文字符串初始化,且不涉及跨平台或本地化需求。
- 风险:非英文字符可能导致乱码或运行时错误。
- 写法二
- 场景:推荐用于所有 UE5 项目,尤其是涉及多语言、跨平台或复杂字符串处理的场景。
- 优点:确保字符编码一致性,避免潜在问题。
总结
维度 | 写法一 | 写法二 |
---|---|---|
字符串类型 | 窄字符(const char* ) | 宽字符(TCHAR* ) |
字符编码 | ANSI 或 UTF-8 | UTF-16 |
UE5 兼容性 | 可能引发编码问题 | 推荐使用,确保兼容性 |
适用场景 | 简单英文字符串初始化 | 所有 UE5 项目,尤其是复杂场景 |
建议:在 UE5 开发中,优先使用 TEXT("MyFstring")
写法,确保代码的健壮性和跨平台兼容性。
示例如下
蓝图效果图
关于FText
一、FText
的核心特性
FText
是 UE5 中用于处理 本地化文本 的类,其核心特性包括:
- 本地化支持:自动根据当前语言环境加载对应的文本内容。
- 不可变性:
FText
对象创建后不可修改,确保线程安全。 - 格式化功能:支持动态插入变量(如数字、字符串)。
- 性能优化:通过共享数据减少内存占用。
二、FText
的常用方法
1. 创建 FText
对象
- 直接创建:
FText MyText = FText::FromString(TEXT("Hello, World!"));
- 本地化文本:
FText MyLocalizedText = NSLOCTEXT("MyNamespace", "MyKey", "Default Text");
MyNamespace
:命名空间,用于区分不同模块的文本。MyKey
:文本的唯一标识符。Default Text
:默认文本(当本地化文件未找到时使用)。
2. 格式化文本
FText::Format
:动态插入变量。FText FormattedText = FText::Format( NSLOCTEXT("MyNamespace", "Greeting", "Hello, {0}!"), FText::FromString(TEXT("Unreal")) );
{0}
:占位符,替换为第二个参数的值。
3. 文本比较
EqualTo
:比较两个FText
是否相同。if (Text1.EqualTo(Text2)) { // 文本相同 }
4. 转换为字符串
ToString
:将FText
转换为FString
。FString StringText = MyText.ToString();
-
如下:
三、FText
的典型应用场景
- UI 文本显示
- 在
UMG
中使用FText
显示本地化文本。TextBlock->SetText(NSLOCTEXT("UI", "Welcome", "Welcome to Unreal Engine!"));
- 在
- 日志与调试信息
- 使用
FText
输出本地化日志。UE_LOG(LogTemp, Log, TEXT("%s"), *MyText.ToString());
- 使用
- 动态文本生成
- 根据游戏状态生成动态文本。
FText ScoreText = FText::Format( NSLOCTEXT("Game", "Score", "Score: {0}"), FText::AsNumber(PlayerScore) );
- 根据游戏状态生成动态文本。
- 多语言支持
- 通过本地化文件实现多语言切换。
[MyNamespace] MyKey="Hello, World!"
- 通过本地化文件实现多语言切换。
四、注意事项与最佳实践
- 避免频繁转换
- 尽量减少
FText
与FString
之间的转换,以提升性能。
- 尽量减少
- 合理使用命名空间
- 为不同模块的文本分配独立的命名空间,避免冲突。
- 默认文本清晰
- 在
NSLOCTEXT
中提供明确的默认文本,便于调试和维护。
- 在
- 本地化文件管理
- 使用
Localization Dashboard
工具管理本地化文件,确保文本一致性。
- 使用
总结:FText
的核心价值
- 本地化支持:轻松实现多语言适配。
- 线程安全:不可变性确保多线程环境下的安全性。
- 动态文本生成:通过格式化功能满足复杂需求。
- 性能优化:共享数据机制减少内存占用。
通过合理使用 FText
,开发者可以高效处理 UE5 项目中的文本需求,提升用户体验与代码质量。
四:元
五:扩展
GC回收机制
UE(Unreal Engine)的GC(Garbage Collection)回收机制用于自动管理内存,确保不再使用的对象能被回收,以防止内存泄漏。它的主要功能是定期检查引擎中的所有对象并销毁不再需要的对象,以便释放内存。
UE的GC回收机制主要包括以下几个部分:
-
引用计数(Reference Counting):UE通过引用计数来跟踪对象的生命周期。每当有一个新的引用指向对象时,引用计数增加;当引用离开作用域时,引用计数减少。当对象的引用计数降到零时,意味着该对象不再被使用,可以被垃圾回收。
-
标记-清除算法(Mark-and-Sweep):GC的回收算法通常基于标记-清除策略。首先,从根对象(如全局变量)开始,标记所有活动的对象(仍然被引用的对象)。然后,清除那些未被标记的对象,这些对象是垃圾,可以被回收。
-
延迟回收:UE通常并不会在每次调用GC时都清除所有未使用的对象,而是根据一定的策略延迟回收。这避免了每次GC调用时都产生过大的性能开销。
-
内存池:为了减少频繁的内存分配和释放,UE通常会使用内存池技术来分配和回收内存。内存池管理了对象的生命周期,避免了频繁的内存操作带来的性能瓶颈。
-
手动控制:虽然GC机制是自动的,但开发者也可以通过手动调用GC的相关函数来控制回收时机,例如
CollectGarbage()
函数来触发手动垃圾回收。
总结来说,UE的GC回收机制旨在通过引用计数、标记-清除算法、内存池等手段,自动管理内存并回收不再使用的对象,从而减少内存泄漏问题并优化游戏的性能
关于UObject
Uobject New出来的对象,不进行特殊标记(例如UPROPERTY)将会被GC回收
方法一:进行资产管理
方法二:挂载到根部上作为引用,将不会被GC回收,直至从根上被删除且未被其他地方引用
方法三:通过构造
关于UObject的构造函数
在Unreal Engine中,UObject
派生类的构造函数设计存在特殊规则,这两个构造函数的区别及使用场景如下:
一、默认构造函数和引擎初始化构造函数的核心区别
-
默认构造函数
USyObjectObject::USyObjectObject() { UE_LOG(LogTemp, Warning, TEXT("Complete initialization")); }
- 触发场景:仅在非引擎管理的初始化流程中调用(如静态对象构造或手动
new
操作)。 - 风险:绕过了Unreal的初始化系统(
FObjectInitializer
),可能导致组件、默认属性或网络复制的配置缺失。
- 触发场景:仅在非引擎管理的初始化流程中调用(如静态对象构造或手动
-
引擎初始化构造函数
USyObjectObject::USyObjectObject(const FObjectInitializer& ObjectInitializer) { UE_LOG(LogTemp, Warning, TEXT("Complete Engine initialization")); }
- 触发场景:通过
NewObject
、CreateDefaultSubobject
或资源加载等引擎托管的方式创建对象时调用。 - 关键作用:通过
FObjectInitializer
传递引擎的初始化上下文,确保对象与引擎系统(如序列化、垃圾回收)正确集成。
- 触发场景:通过
二、为什么必须使用引擎初始化构造函数?
-
引擎初始化的强制性
Unreal要求所有UObject
派生类通过FObjectInitializer
构造函数完成初始化。该参数提供了:- 子对象管理:通过
CreateDefaultSubobject
创建组件或嵌套对象。 - 属性初始化:自动应用
UPROPERTY
的默认值(如EditAnywhere
配置)。 - 序列化支持:确保对象数据在保存/加载时正确处理。
- 子对象管理:通过
-
默认构造函数的局限性
若仅实现默认构造函数:- 组件丢失:无法通过
CreateDefaultSubobject
添加组件。 - 属性未初始化:
UPROPERTY
的默认值可能未被正确赋值。 - 崩溃风险:违反引擎的对象生命周期管理规则,可能导致内存错误或崩溃。
- 组件丢失:无法通过
-
编译警告与规范
Unreal Header Tool(UHT)会强制生成带FObjectInitializer
的构造函数。若手动声明,需显式调用父类构造函数:USyObjectObject::USyObjectObject(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) // 必须传递ObjectInitializer给父类 { ... }
三、实际应用场景示例
-
动态创建对象
如图:正确方式:通过NewObject触发引擎初始化构造函数 USyObjectObject* Obj = NewObject<USyObjectObject>(GetTransientPackage());
-
错误用法
错误方式:直接new操作调用默认构造函数 无法通过编译
USyObjectObject* Obj = new USyObjectObject();
- 后果:对象未注册到引擎系统,可能无法被垃圾回收或正确序列化
关于引擎构造函数中FObjectInitializer参数
的核心作用
FObjectInitializer
是 UE 中用于 UObject
派生类对象构造过程 的核心工具,其核心作用可概括为:
- 管理子对象(Subobjects)的创建与初始化
- 通过
CreateDefaultSubobject
方法创建并注册子对象(如组件、嵌套对象),确保其生命周期与父对象绑定。 - 示例:在
AActor
派生类中创建UStaticMeshComponent
。
- 通过
- 设置属性的默认值
- 覆盖类默认对象(CDO, Class Default Object)中定义的
UPROPERTY
值,支持动态初始化逻辑。
- 覆盖类默认对象(CDO, Class Default Object)中定义的
- 维护对象构造上下文
- 传递模板对象(Archetype)、外部包(Outer)等上下文信息,确保对象在引擎系统中的正确注册。
为什么必须使用 FObjectInitializer
而非默认构造函数?
- 引擎内部依赖
- UE 要求所有
UObject
派生类必须通过FObjectInitializer
构造,以维护对象与引擎系统(垃圾回收、序列化、反射)的关联。
- UE 要求所有
- 子对象管理的唯一途径
- 直接调用
new
或默认构造函数会绕过CreateDefaultSubobject
,导致子对象未注册到引擎,引发内存泄漏或功能失效。
- 直接调用
- 属性初始化的正确性
FObjectInitializer
确保属性值从 CDO 或蓝图父类继承,而默认构造函数可能跳过此流程。
典型使用场景与代码示例
1. 创建组件(Component)
AMyActor::AMyActor(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { // 创建并注册组件 MeshComponent = ObjectInitializer.CreateDefaultSubobject<UStaticMeshComponent>(this, TEXT("Mesh")); MeshComponent->SetCollisionProfileName(UCollisionProfile::BlockAll_ProfileName); }
- 关键点:
CreateDefaultSubobject
必须在构造函数中调用,且每个子对象需唯一命名(如TEXT("Mesh")
)。- 父类构造函数需显式传递
ObjectInitializer
(Super(ObjectInitializer)
)。
2. 动态覆盖属性默认值
AMyCharacter::AMyCharacter(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer .DoNotCreateDefaultSubobject(TEXT("Capsule")) // 禁用父类默认组件 .SetDefaultSubobjectClass<UMyCustomMovementComponent>(TEXT("Movement"))) // 替换移动组件 { Health = ObjectInitializer.GetArchetype<AMyCharacter>()->Health; // 从 CDO 继承 Health }
- 方法链:通过链式调用配置初始化行为(如禁用/替换组件)。
与引擎机制的深度关联
- 序列化与蓝图编辑
FObjectInitializer
创建的子对象会被序列化到资产中,支持蓝图编辑器的可视化操作。
- 网络同步
- 在多人游戏中,通过
FObjectInitializer
注册的组件可被自动复制(需配合Replicated
属性)。
- 在多人游戏中,通过
- 垃圾回收(GC)
- 子对象通过
FObjectInitializer
注册后,GC 系统会将其与父对象关联,避免提前释放。
- 子对象通过
为什么智能指针不适用于UObject
一、生命周期管理冲突
- 智能指针的引用计数模型
传统智能指针(如std::shared_ptr
、TSharedPtr
)依赖引用计数自动释放资源,而 UObject的生命周期由UE垃圾回收(GC)系统控制。两者同时介入会导致:- 双重所有权冲突:引用计数与GC标记可能互相干扰,引发对象被提前释放或内存泄漏。
- 悬空指针风险:若GC已回收对象,但智能指针未感知,继续访问会触发崩溃。
- GC系统的标记-清除机制
UE通过遍历根对象(Root Set)标记活跃UObject,未被引用的对象会被GC清理。而智能指针的引用计数无法被GC系统识别,导致:- 误判活跃状态:智能指针持有对象时,GC可能误认为其未被引用而错误回收。
- 循环引用陷阱:若UObject间通过智能指针形成循环引用,GC无法检测并释放,导致内存滞留。
二、UObject的特殊构造与销毁规则
- 构造限制
UObject必须通过引擎API(如NewObject
、CreateDefaultSubobject
)创建,禁止直接使用new
或智能指针包装。原因包括:- 元数据注册:UE需在构造时注册对象的反射、序列化信息。
- 子对象绑定:父子关系需通过
Outer
参数管理,智能指针无法维护此上下文。
- 销毁流程
UObject的析构由GC系统统一调度,开发者不能手动delete
。若使用智能指针:- 析构时机不可控:智能指针可能在GC未就绪时尝试释放对象,引发崩溃。
- 跨模块风险:若对象属于其他模块,手动释放会破坏模块间内存管理协议。
三、UE提供的替代方案
UPROPERTY
与GC集成
使用UPROPERTY
宏标记的成员变量会被GC跟踪,确保引用链正确性:UPROPERTY() AActor* MyActor; // GC自动管理其生命周期
- 优势:无缝兼容反射、序列化、蓝图编辑。
- 限制:仅适用于UObject派生类。
- 弱引用与观察模式
TWeakObjectPtr
:安全持有UObject弱引用,自动处理对象失效:TWeakObjectPtr<AActor> WeakActor = MyActor; if (WeakActor.IsValid()) { /* 安全访问 */ }
FSoftObjectPtr
:支持异步加载和软引用,适用于资源句柄。- 强引用控制
TStrongObjectPtr
:强制保持对象活跃状态,避免GC回收:
TStrongObjectPtr<AActor> StrongActor = MyActor; // 对象在StrongActor存在期间不会被GC回收
四、技术边界与性能考量
- 内存模型差异
UE内存分配器(如FMalloc
)与C++标准库不兼容,智能指针可能绕过引擎内存统计,导致:- 内存池碎片化:智能指针独立分配内存,破坏UE的内存优化策略。
- 调试工具失效:引擎内置的内存分析工具无法追踪智能指针管理的内存块。
- 跨模块安全性
UObject可能归属特定模块(如游戏模块、插件模块),智能指针的析构若发生在模块卸载后,会引发未定义行为。而GC系统通过模块卸载前的全局清理确保安全性。
五、例外场景与变通方案
-
非UObject对象
对非UObject的纯C++类(如工具类、数据结构),可自由使用TSharedPtr
、TUniquePtr
,因其不受GC约束。 -
混合管理策略
若需临时持有UObject,可结合FGCObject
接口手动注册到GC:class MyHolder : public FGCObject { TSharedPtr<AActor> SafePointer; // 需手动管理 void AddReferencedObjects(FReferenceCollector& Collector) override { Collector.AddReferencedObject(SafePointer.Get()); } };
- 代价:增加代码复杂度,仅适用于特殊需求。