前言

本篇为UE使用过程中关于某些API 或者 某些底层机制的Common 处理。

某些东西深入研究可以多开一篇文章。

FSoftObjectPath

在 Unreal Engine(UE)中,FSoftObjectPath 是一种用于间接引用资产(Asset)的核心机制,它通过存储资源的路径字符串而非直接加载资源,实现按需加载和内存优化。以下是其核心概念、用法及技术细节:

🔍 一、FSoftObjectPath 核心概念

  1. 作用与原理
    1. 路径存储:存储资源的完整路径(如 StaticMesh'/Game/Meshes/Cube.Cube'),但不加载资源本身4,6
    2. 按需加载:通过调用 TryLoad() 或异步接口显式加载资源,避免启动时内存占用过高。
    3. 软引用类型:属于软引用(Soft Reference),与硬引用(UObject*)相比,不强制资源常驻内存6
  2. 路径格式解析 资源路径的典型结构:
  3. [资源类型]’[分区]/[路径]/[包名].[对象名]’
  4. 示例: StaticMesh'/Game/Meshes/Cube.Cube'
    1. 分区/Game(项目资源)或 /Engine(引擎资源)5
    2. 包名:磁盘上的 .uasset 文件名(如 Cube)。
    3. 对象名:资源文件内的具体对象名(默认与包名相同)5

⚙️ 二、代码实现与用法

1. 声明与编辑器配置

// 头文件声明

UPROPERTY(EditAnywhere, Category=”Assets”)

FSoftObjectPath MeshPath; // 可接受任意资源类型

UPROPERTY(EditDefaultsOnly, meta=(AllowedClasses=”Texture”)) // 限制为纹理类

FSoftObjectPath TexturePath;

  • 编辑器支持:在 UE 编辑器中,属性面板会显示资源选择器,允许直接拾取资源4,6

2. 资源加载方式

  • 同步加载(适合小资源):
  • UObject* LoadedObject = MeshPath.TryLoad(); UStaticMesh* Mesh = Cast(LoadedObject); if (Mesh) { /* 使用网格 */ }
  • 注意:大资源同步加载可能导致卡顿4
  • 异步加载(推荐): 需结合 FStreamableManager 实现:
  • FStreamableManager& Streamer = …; Streamer.RequestAsyncLoad( MeshPath.ToString(), this { UStaticMesh* Mesh = Cast(MeshPath.ResolveObject()); // 资源加载完成后的回调 } );

3. 动态设置路径

// 代码中动态修改路径

MeshPath.SetPath(TEXT(“StaticMesh’/Game/Meshes/Sphere.Sphere’”));

⚖️ 三、软引用 vs. 硬引用

特性 软引用(FSoftObjectPath) 硬引用(UObject*)
内存占用 不自动加载资源,内存占用低 资源常驻内存
加载时机 显式调用 TryLoad() 时加载 父对象加载时自动加载
适用场景 大型资源(如地图、高精度模型) 高频使用的小资源(如材质)
GC 影响 卸载后资源可被回收 持有引用时资源不会被 GC 释放

💡 四、实际应用场景

  1. 动态资源加载
    1. 根据玩家进度加载不同关卡资源,减少初始包体积5
    2. 示例:开放世界游戏中按区域加载地形网格。
  2. 资源热更新 通过重定向路径到下载的新资源包,实现热更新6
  3. 避免循环依赖 软引用可解耦模块间依赖,例如:Gameplay 模块通过路径引用 Asset 模块的资源,无需编译依赖。

⚠️ 五、常见问题与优化

  1. 路径失效
    1. 原因:资源移动或重命名后路径未更新。
    2. 解决方案:使用 重定向器(Redirector) 或编辑器工具 Fix Up Redirectors
  2. 异步加载卡顿
    1. 优化策略:分帧加载(如每帧加载 1 个资源),或预加载到内存池4
  3. 类型安全
    1. 使用
    2. TSoftObjectPtr
    3. 替代
    4. FSoftObjectPath
    5. ,限制资源类型:
    6. TSoftObjectPtr SafeTexturePath; // 仅允许纹理类

💎 总结

  • 核心价值FSoftObjectPath 通过路径解耦资源引用,是优化内存与加载性能的关键工具。
  • 最佳实践:
    • 大型资源用异步加载 + 回调;
  • 高频小资源用硬引用;
    • 动态场景用 TSoftObjectPtr 确保类型安全4,6
  • 调试工具:控制台命令 Obj List 查看加载的资源状态。

深入参考:

Actor生命周期

https://dev.epicgames.com/documentation/zh-cn/unreal-engine/unreal-engine-actor-lifecycle

img

Unreal Engine 中 Actor 的生命周期管理是其游戏对象系统的核心。下面我将 Actor 的几种主要生成路径及其生命周期阶段用表格形式整理出来,并补充一些关键细节和注意事项。

🧭 总览:Actor 的四种主要生成路径

路径名称 触发场景 特点
从磁盘加载 (Load from Disk) 关卡加载(LoadMap)或流关卡加载(AddToWorld)时。 适用于在编辑器中预先放置好的静态 Actor
在编辑器中播放 (Play in Editor, PIE) 在编辑器内点击“Play”按钮启动游戏时。 Actor 是从编辑器中的原始对象复制而来,避免影响原始资产。
动态生成 (Spawning) 运行时通过 UWorld::SpawnActor 函数动态创建。 最常见的运行时生成方式,用于敌人、道具、特效等。
延迟生成 (Deferred Spawning) 运行时通过 UWorld::SpawnActorDeferred 函数动态创建。 允许在调用蓝图构造脚本之前设置Actor的属性。
  1. 从磁盘加载 (Load from Disk)

这是指那些在编辑器中预先放置好、随关卡一起从磁盘加载的 Actor 的生命周期路径。8

阶段 描述 常见用途与注意事项
从磁盘加载 Actor 及其所在的包/关卡从硬盘被读取到内存中。 硬盘读取速度(SSD vs HDD)影响加载时间。6
PostLoad Actor 反序列化完成后调用。与 PostActorCreated 互斥。 在此处理自定义版本迁移和数据修复。是加载路径独有的阶段。8
InitializeActorsForPlay 引擎内部函数,准备 Actor 以开始游戏。
RouteActorInitialize 为未初始化的 Actor 执行初始化路由。 包含 Stream Level 的加载。8
PreInitializeComponents 在 Actor 自身的组件初始化之前调用。 可进行一些初始化所需的准备工作,例如配表。7
InitializeComponent 在 Actor 的每个组件上调用。 是组件自身的初始化点,按注册顺序调用。
PostInitializeComponents Actor 的所有组件都已初始化后调用。 此时可以安全地访问和操作其他组件。
BeginPlay 关卡正式开始游戏时调用。 开始执行游戏逻辑,如启动计时器、绑定事件。1,4
  1. 在编辑器中播放 (Play in Editor - PIE)

此路径与“从磁盘加载”非常相似,但源不同,主要用于调试。8

阶段 描述 与“从磁盘加载”路径的主要差异
复制Actor 将编辑器中的原始 Actor 复制到一个新的、专用于PIE的世界中。 源是编辑器中的现有对象,而非磁盘上的包文件。
PostDuplicate 复制完成后调用。 替代了 PostLoad 阶段,是PIE路径独有的阶段。8
InitializeActorsForPlay 同“从磁盘加载”路径。
RouteActorInitialize 同“从磁盘加载”路径。
PreInitializeComponents 同“从磁盘加载”路径。
InitializeComponent 同“从磁盘加载”路径。
PostInitializeComponents 同“从磁盘加载”路径。
BeginPlay 同“从磁盘加载”路径。
  1. 动态生成 (Spawning)

这是游戏运行时动态创建 Actor(如生成敌人、子弹)最常用的方式。9

阶段 描述 常见用途与注意事项
SpawnActor 调用 UWorld::SpawnActor 函数。 需提供有效的 UClass、位置变换和生成参数。9
PostSpawnInitialize 引擎内部初始化流程。
PostActorCreated Actor 对象在内存中创建后立即调用。与 PostLoad 互斥。 可在此执行类似构造函数的初始化行为。8
ExecuteConstruction 执行 Actor 的构建逻辑。
OnConstruction (主要在蓝图中使用)构建脚本被调用。 蓝图Actor在此创建组件并初始化蓝图变量。8
PostActorConstruction Actor 构建完成。
PreInitializeComponents 同“从磁盘加载”路径。
InitializeComponent 同“从磁盘加载”路径。
PostInitializeComponents 同“从磁盘加载”路径。
OnActorSpawned 在 UWorld 上广播生成事件。 其他系统可监听此事件,得知某个Actor已生成完毕。8
BeginPlay 同“从磁盘加载”路径。
  1. 延迟生成 (Deferred Spawning)

这种生成方式允许你在 Actor 的蓝图构造脚本运行之前配置其属性。8

阶段 描述 与“动态生成”路径的主要差异
SpawnActorDeferred 调用 UWorld::SpawnActorDeferred 函数。 生成一个未完成构建的Actor实例,并返回其指针。8
(执行SpawnActor内的步骤) 同“动态生成”路径,直到 PostActorCreated。
自定义初始化 在 PostActorCreated 后,你有机会使用这个有效的、但不完整的 Actor 实例进行自定义初始化设置(例如,设置其暴露(Expose on Spawn)的属性)。 这是延迟生成的核心目的,在运行构建脚本前配置属性。
FinishSpawningActor 调用此函数以最终完成Actor的生成过程。 此函数会继续执行“动态生成”路径中 ExecuteConstruction 及之后的所有步骤。8

⛔ 生命周期的结束 (End of Life)

无论通过哪种路径生成,Actor的销毁过程都是统一的。8

阶段 描述 调用时机与常见操作
Destroy 手动调用,表示希望销毁Actor。游戏仍在继续。 Actor被标记为 PendingKill 并从关卡的Actor数组中移除。8
EndPlay 核心的清理函数。保证Actor生命终结时被调用。 必须在此进行所有重要的清理工作: • 解除事件绑定 • 清除定时器 (GetWorld()->GetTimerManager().ClearAllTimersForObject(this))2 • 释放动态分配的资源2 • 网络复制的清理
(原因) 调用 EndPlay 的常见原因:8 • 显式调用 Destroy • PIE会话结束 • 关卡转换(无缝旅行或加载新地图) • 包含该Actor的流关卡被卸载 • Actor的生存期(Lifespan)到期 • 应用程序关闭
OnDestroy 较旧的遗留函数,响应 Destroy 调用。 建议将清理逻辑移至 EndPlay,因为 EndPlay 在更多情况下(如关卡转换)会被调用。8
标记待销毁 Actor被标记为 RF_PendingKill 此后,尝试获取该Actor的指针可能会失败或返回空。8
垃圾回收 (Garbage Collection) 引擎的垃圾回收系统会在一段时间后真正从内存中释放该对象。8
BeginDestroy 对象有机会释放内存和处理多线程资源。 大多数游戏性清理应在 EndPlay 中完成,此处处理底层资源释放。8
IsReadyForFinishDestroy GC过程调用此函数,询问对象是否可被永久释放。 返回 false 可以延迟实际销毁,直到下一个GC周期。8
FinishDestroy 对象被销毁,是释放内部数据结构的最后机会。 内存释放前的最后一次调用。8

💎 总结与建议

理解 Actor 的生命周期对于编写稳定、无内存泄漏的 UE 代码至关重要。

  • 初始化逻辑的位置:根据需求选择正确的初始化阶段。PostLoad 用于版本迁移,PostActorCreated 用于 C++ 构造函数逻辑,InitializeComponents 用于组件初始化,PostInitializeComponents 用于依赖其他组件的逻辑,BeginPlay 用于最终的游戏逻辑开始。
  • 清理逻辑的位置:**EndPlay 是你最重要的朋友**。几乎所有针对游戏性的清理工作,如解绑事件、清除计时器、释放引用等,都应在此进行。不要依赖析构函数或 OnDestroy
  • 生成方式的选择
    • 静态放置 → 从磁盘加载
    • 运行时动态生成 → SpawnActor
    • 运行时动态生成且需在构建前设置“Expose on Spawn”属性 → SpawnActorDeferred
  • 警惕关卡转换和流关卡卸载:这些情况也会触发 EndPlay,确保你的 Actor 能正确处理,防止出现“关卡切换后报错”的问题。

希望这份详细的表格能帮助你更好地驾驭 Unreal Engine 中的 Actor。

ESlateVisibility

UE.ESlateVisibility.SelfHitTestInvisible 是 Unreal Engine 中用于控制 UI 控件可见性与交互性的一个枚举值。简单来说,它让控件可见但无法与之交互(例如点击、悬停),同时不影响其子控件的交互性。

下面是一个快速对比表格,帮你了解 ESlateVisibility 的主要枚举值:

可见性状态 是否可见 是否占用布局空间 自身是否可交互 子控件是否可交互
Visible
SelfHitTestInvisible
HitTestInvisible
Hidden
Collapsed

💡 关键细节

  • “不响应事件”:指的是鼠标点击、触摸、悬停等用户输入事件会直接穿透该控件,被其下方或后方的其他控件接收2,4
  • “不影响子控件”:这是 SelfHitTestInvisibleHitTestInvisible 的主要区别。如果父控件设置为 HitTestInvisible,则其所有子控件也会变得不可交互。而 SelfHitTestInvisible 仅禁用控件自身的交互,子控件保持原有的交互状态2,6,7

🛠 使用方法

在蓝图中: 在控件细节面板的 “Appearance” 部分,直接找到 “Visibility” 下拉菜单选择即可。

在 C++ 中

1
2
// 假设 YourWidget 是某个控件指针
YourWidget->SetVisibility(ESlateVisibility::SelfHitTestInvisible);

在 UnLua 中(如果项目使用了Lua绑定):

1
2
local visibleType = UE.ESlateVisibility.SelfHitTestInvisible
self.YourWidget:SetVisibility(visibleType)

(用法参考了 VisibleHidden 的设定 1

🧩 主要应用场景

  • 装饰性UI元素:显示一些无需交互的图片、背景、装饰图案等。
  • 视觉遮罩或提示:需要半透明遮罩层来提示用户某个区域,但又不想阻挡用户与下层UI的交互。
  • 禁用状态视觉表现:有时希望控件在禁用时不仅变灰,而且鼠标事件能穿透它,可能会用到此选项。

💎 实用建议

  1. HitTestInvisible 区分:关键在于你是否希望子控件可交互。如果需要子控件交互,用 SelfHitTestInvisible;如果希望父子控件都不可交互,用 HitTestInvisible
  2. Hidden/Collapsed 区分HiddenCollapsed完全不可见2,4。如果你的控件需要可见不拦截输入,就应选择 SelfHitTestInvisibleHitTestInvisible
  3. 事件穿透的另一种方法:有时为了实现更复杂的点击事件穿透(例如,仅让按钮的透明部分穿透点击),除了设置 SelfHitTestInvisible,还可能需配合调整点击检测(Hit Test) 的相关设置或使用透明材质8

希望这些信息能帮助你更好地理解和使用 ESlateVisibility.SelfHitTestInvisible

10.13

BVM

蓝图虚拟机

https://zhuanlan.zhihu.com/p/54174686

https://zhuanlan.zhihu.com/p/688017291

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// Evaluatable expression item types.
// 可评估表达式项类型枚举,用于标识脚本代码中不同类型的操作和常量[6](@ref)
enum EExprToken
{
// ============================================================================
// 变量与上下文操作
// ============================================================================

// Variable references.
EX_LocalVariable = 0x00, // 访问局部变量(函数内定义的变量)
EX_InstanceVariable = 0x01, // 访问对象实例变量(类的成员变量)
EX_DefaultVariable = 0x02, // 访问类上下文中的默认变量
// = 0x03, // 保留值,未使用
EX_Return = 0x04, // 从函数返回操作
// = 0x05, // 保留值,未使用

// ============================================================================
// 流程控制操作
// ============================================================================

EX_Jump = 0x06, // 无条件跳转到代码中的本地地址
EX_JumpIfNot = 0x07, // 条件跳转(当表达式为false时跳转)
// = 0x08, // 保留值,未使用
EX_Assert = 0x09, // 断言检查,用于调试和验证条件
// = 0x0A, // 保留值,未使用
EX_Nothing = 0x0B, // 空操作(无操作指令)
// = 0x0C, // 保留值,未使用
// = 0x0D, // 保留值,未使用
// = 0x0E, // 保留值,未使用

// ============================================================================
// 赋值操作
// ============================================================================

EX_Let = 0x0F, // 为变量赋值(任意大小的值)
// = 0x10, // 保留值,未使用
// = 0x11, // 保留值,未使用
EX_ClassContext = 0x12, // 类默认对象上下文
EX_MetaCast = 0x13, // 元类转换操作
EX_LetBool = 0x14, // 为布尔变量赋值
EX_LetObj = 0x5F, // 为对象引用指针赋值
EX_LetWeakObjPtr = 0x60, // 为弱对象指针赋值
EX_LetValueOnPersistentFrame = 0x64, // 在持久帧上赋值

// ============================================================================
// 函数调用相关
// ============================================================================

EX_EndParmValue = 0x15, // 可选函数参数默认值的结束标记
EX_EndFunctionParms = 0x16, // 函数调用参数结束标记
EX_Self = 0x17, // 获取当前对象自身(this/self指针)
EX_Context = 0x19, // 通过对象上下文调用函数
EX_Context_FailSilent = 0x1A, // 通过对象上下文调用函数(上下文为NULL时可静默失败)
EX_VirtualFunction = 0x1B, // 虚函数调用(带参数)
EX_FinalFunction = 0x1C, // 预绑定函数调用(带参数)
EX_CallMath = 0x68, // 调用本地调用空间中的静态纯函数
EX_InterfaceContext = 0x51, // 通过本地接口变量调用函数

// ============================================================================
// 常量值
// ============================================================================

EX_IntConst = 0x1D, // 整型常量
EX_FloatConst = 0x1E, // 浮点数常量
EX_StringConst = 0x1F, // 字符串常量
EX_ObjectConst = 0x20, // 对象常量
EX_NameConst = 0x21, // 名称常量
EX_RotationConst = 0x22, // 旋转常量
EX_VectorConst = 0x23, // 向量常量
EX_ByteConst = 0x24, // 字节常量
EX_IntZero = 0x25, // 整型零值(优化用)
EX_IntOne = 0x26, // 整型一值(优化用)
EX_True = 0x27, // 布尔真值(true)
EX_False = 0x28, // 布尔假值(false)
EX_TextConst = 0x29, // FText常量(本地化文本)
EX_NoObject = 0x2A, // 空对象引用
EX_TransformConst = 0x2B, // 变换常量
EX_IntConstByte = 0x2C, // 单字节整型常量(优化存储)
EX_NoInterface = 0x2D, // 空接口引用(类似于EX_NoObject,但用于接口)
EX_UnicodeStringConst = 0x34, // Unicode字符串常量
EX_Int64Const = 0x35, // 64位整型常量
EX_UInt64Const = 0x36, // 64位无符号整型常量
EX_AssetConst = 0x67, // 资源常量

// ============================================================================
// 类型转换操作
// ============================================================================

EX_DynamicCast = 0x2E, // 安全的动态类转换
EX_PrimitiveCast = 0x38, // 基本类型转换操作符
EX_ObjToInterfaceCast = 0x52, // 对象引用到本地接口变量的转换
EX_CrossInterfaceCast = 0x54, // 接口变量引用到本地接口变量的转换
EX_InterfaceToObjCast = 0x55, // 接口变量引用到对象的转换

// ============================================================================
// 结构体和数组操作
// ============================================================================

EX_StructConst = 0x2F, // 任意UStruct常量
EX_EndStructConst = 0x30, // UStruct常量结束标记
EX_SetArray = 0x31, // 设置任意数组的值
EX_EndArray = 0x32, // 数组结束标记
EX_StructMemberContext = 0x42, // 用于访问结构体内属性的上下文表达式
EX_ArrayConst = 0x65, // 数组常量
EX_EndArrayConst = 0x66, // 数组常量结束标记
EX_ArrayGetByRef = 0x6B, // 通过引用获取数组元素

// ============================================================================
// 委托操作
// ============================================================================

EX_LetMulticastDelegate = 0x43, // 为多播委托赋值
EX_LetDelegate = 0x44, // 为委托赋值
EX_InstanceDelegate = 0x4B, // 对委托或普通函数对象的常量引用
EX_AddMulticastDelegate = 0x5C, // 向多播委托的目标中添加委托
EX_ClearMulticastDelegate = 0x5D, // 清除多播目标中的所有委托
EX_BindDelegate = 0x61, // 将对象和名称绑定到委托
EX_RemoveMulticastDelegate = 0x62, // 从多播委托的目标中移除委托
EX_CallMulticastDelegate = 0x63, // 调用多播委托

// ============================================================================
// 高级流程控制
// ============================================================================

EX_Skip = 0x18, // 可跳过表达式(条件执行)
EX_PushExecutionFlow = 0x4C, // 将地址推送到执行流栈供后续执行
EX_PopExecutionFlow = 0x4D, // 继续执行之前推送到执行流栈的最后一个地址
EX_ComputedJump = 0x4E, // 计算跳转(由整数值指定代码中的本地地址)
EX_PopExecutionFlowIfNot = 0x4F, // 条件执行流弹出(条件不成立时执行)
EX_SwitchValue = 0x69, // 开关值(多分支选择)

// ============================================================================
// 调试和诊断
// ============================================================================

EX_Breakpoint = 0x50, // 断点(仅在编辑器中有效,否则行为类似EX_Nothing)
EX_WireTracepoint = 0x5A, // 跟踪点(仅在编辑器中有效)
EX_Tracepoint = 0x5E, // 跟踪点(仅在编辑器中有效)
EX_InstrumentationEvent = 0x6A, // 插装事件(用于性能分析)

// ============================================================================
// 其他操作
// ============================================================================

EX_SkipOffsetConst = 0x5B, // 代码跳转偏移常量
EX_EndOfScript = 0x53, // 脚本代码的最后一个字节
EX_DeprecatedOp4A = 0x4A, // 已弃用的操作(保留用于向后兼容)

EX_Max = 0x100, // 枚举最大值(用于边界检查)
};

UE4/UE5 的蓝图虚拟机(Blueprint Virtual Machine)是一个设计精巧的系统,它将可视化的节点图转化为可执行的游戏逻辑。其核心在于 将蓝图节点编译成字节码(Bytecode),然后通过一个轻量级的解释器(即虚拟机)来逐条执行这些指令

为了帮助你快速构建起对蓝图虚拟机工作流程的整体认知,可以参考下面的这张核心流程图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
flowchart TD
A[蓝图节点图<br>(UEdGraph)] --> B[编译<br>(Compilation)]

subgraph B [编译阶段]
B1[节点展开与优化] --> B2[生成中间表示<br>(FBlueprintCompiledStatement)] --> B3[生成字节码<br>(EExprToken 序列)]
end

B --> C[字节码存储<br>(UFunction::Script)]
C --> D[运行时执行]

subgraph D [虚拟机执行核心]
D1[创建栈帧(FFrame)] --> D2[循环读取字节码] --> D3[查找GNatives函数指针] --> D4[执行原生函数]
end

D4 --> E[逻辑执行结果]

🔁 从节点到字节码:编译过程

蓝图虚拟机执行的第一步,是将可视化的节点图转换为它能够理解的指令序列。

  1. 节点图处理与优化:编译器(主要由 FKismetCompilerContext驱动)首先会对节点图进行分析和优化。这包括将宏等复合节点展开成基础节点,并通过依赖分析移除未被任何事件节点连接的“死代码”。
  2. 生成中间表示:处理后的节点图不会被直接翻译成字节码,而是先被转换成一种线性的中间表示——FBlueprintCompiledStatement列表。这个列表可以看作是一种平台无关的“汇编指令”,它清晰地描述了操作的顺序和逻辑,为后续的优化和代码生成奠定了基础。值得一提的是,蓝图编译器跳过了传统编译器构建抽象语法树(AST)的步骤,因为节点图本身就已经是一种结构化的、可视化的程序表示。
  3. 字节码生成:最后,一个称为“VM后端”(FKismetCompilerVMBackend)的组件会遍历中间表示列表,为每一条语句生成对应的虚拟机字节码。这些字节码是 EExprToken枚举中定义的数字指令,例如 EX_CallMath表示调用一个数学函数。生成的字节码会存储在 UFunction对象的 Script数组成员中。

⚙️ 运行时核心:虚拟机如何执行字节码

当游戏运行时(例如,一个 Event BeginPlay被触发),虚拟机的执行引擎便开始工作。

  1. 创建栈帧:执行一个蓝图函数,首先会创建一个栈帧(FFrame)。这个栈帧是执行上下文的核心,它包含了执行所需的所有关键信息:
    1. UObject* Object: 执行此函数的对象实例(即 this指针)。
    2. UFunction* Node: 当前正在执行的函数对象。
    3. uint8* Code: 指向当前要执行的字节码的指针(相当于程序计数器PC)。
    4. uint8* Locals: 指向该函数局部变量内存块的指针。
    5. FFrame* PreviousFrame: 指向上一个栈帧的指针,用于维护调用栈。
  2. 指令分派与执行:执行引擎通过一个循环不断地读取并执行字节码。核心的步进函数是 FFrame::Step()。它的工作流程非常简单高效:
1
2
3
4
void FFrame::Step(UObject* Context, RESULT_DECL) {
int32 B = *Code++; // 1. 读取当前字节码,并移动Code指针
(GNatives[B])(Context, *this, RESULT_PARAM); // 2. 查找并执行对应的原生函数
}
  1. 参数与返回值传递:字节码中不仅包含操作指令,还嵌入了执行所需的数据(如函数参数、常量值)。虚拟机通过 FFrame上的 ReadInt(), ReadFloat(), ReadObject()等方法从 Code流中读取这些数据。函数返回值则通过 RESULT_PARAM指针传递。

🔄 蓝图与C++的相互调用

蓝图虚拟机的强大之处在于它和C++反射系统的无缝集成。

  • C++调用蓝图函数:当C++代码调用一个蓝图中实现的函数(如 BlueprintImplementableEvent)时,引擎会通过 UObject::ProcessEvent找到对应的 UFunction。如果该函数包含蓝图字节码,就会创建 FFrame并进入上述的虚拟机执行流程。
  • 蓝图调用C++函数:当蓝图调用一个暴露给蓝图的C++函数(BlueprintCallable)时,对应的字节码(如 EX_FinalFunction)会指向一个由Unreal Header Tool(UHT)自动生成的 execYourFunctionName静态函数。这个函数负责从栈帧(FFrame)中解析出参数,然后调用你编写的C++函数本体。

💎 总结与特点

总而言之,UE蓝图虚拟机的实现可以概括为:“节点图即代码,FFrame为上下文,GNatives是心脏,C++函数为四肢”。它不是一个独立的、沉重的模块,而是深度构建在UE本身强大的UObject系统(反射、垃圾回收、序列化)之上的一种轻量级、高效的脚本执行环境。

希望这份总结能帮助你透彻地理解蓝图虚拟机的运作机制。如果你对某个特定细节(比如编译优化或异步节点的处理)还想深入了解,我们可以继续探讨。

蓝图底层

https://zhuanlan.zhihu.com/p/438501014

Delegate

委托

https://zhuanlan.zhihu.com/p/460092901

https://zhuanlan.zhihu.com/p/575671003

Delegate的一些简单梳理

宏定义

  • 1.单播委托: TDelegate模板类,通常用DECLARE_DELEGATEDECLARE_DELEGATE_XXXXParams宏进行声明,后面的宏表示带XXXX个参数
  • 2.多播委托: TMulticastDelegate模板类,通常用DECLARE_MULTICAST_DELEGATEDECLARE_MULTICAST_DELEGATE_<Num>Params宏进行声明,后面的宏表示带XXXX个参数
  • 3.动态委托: 继承于TBaseDynamicDelegate,TBaseDynamicMulticastDelegate模板类,通常用DECLARE_DYNAMIC_DELEGATEDECLARE_DYNAMIC_MULTICAST_DELEGATE宏进行声明,分别表示单播与多播,另外还有包含参数个数的宏如DECLARE_DYNAMIC_DELEGATE_OneParam,其本质集成了UObject的反射系统,让其可以注册蓝图实现的函数及支持序列化存储到本机,UFunction只需给函数名称及蓝图支持是其最大的特性,概念上并未逃出前两类,性能和功能弱于前两类。
  • \4. Event: 其继承多播,不单独讨论了
委托类型 绑定函数数量 返回值支持 序列化/蓝图支持 主要用途
单播委托 1个 ✅ 支持 ❌ 不支持 一对一回调,需要返回值时使用
多播委托 多个 ❌ 无返回值 ❌ 不支持 一对多通知,如普通事件分发
动态委托 1个或多个 仅动态单播支持 ✅ 支持 与蓝图系统交互,支持序列化

TDelegate

TDelegate是UE提供的单播委托,声明委托时目前限定了最多可携带9个参数。支持多种可调用对象(普通函数,成员函数,仿函数,lambda等)到声明函数类型进行转换,同时转换的时还支持额外预保存payload参数,类似std:bind一样。TDelegate委托自己没有成员,使用了策略模式继承于基类FDelegateBase。基类FDelegateBase通过new两次重载提供对委托实例的内存分配,并且提供释放及访问方法,这样将委托本体与真正实例解耦开,委托本身不直接保存实例对象,实例由于有多种类型,通过中间层也可以很好的解决了类型擦除与转换的问题,委托本体的size也比较小,只有12字节,由于对齐原因,占16字节。

委托系统的核心是 IDelegateInstance接口。对于每一种可绑定对象(如全局函数、UObject成员函数、Lambda表达式等),都有一个对应的 TBaseXXXDelegateInstance类来实现此接口(例如 TBaseStaticDelegateInstance, TBaseUObjectMethodDelegateInstance)。这就像定义了一套通用的“插座”标准,无论电器是哪种品牌(函数类型),只要插头符合标准就能使用。

当你调用 BindUObject()BindStatic()等方法时,委托内部会通过 Placement new 在预分配的内存上创建对应的委托实例对象,并将函数指针、对象指针等信息存储起来。

模板类里定义了一系列的创建函数(Create)和绑定函数(Bind),用于代理的构造,还实现了执行代理的方法——Execute,用于代理的执行。

CreateDelegateInstance函数:这是TDelegate的父类TBaseDelegate的一个方法。这里面的实现比较巧妙,大概就是通过Allocate函数把扩大自己内部指针的内存,使得自己TBaseDelegate内部能够放下一个T***DelegateInstance,并且对新申请的内存空间进行类型转换

img

T***DelegateInstance

委托系统通过不同的绑定方式,智能地管理对象生命周期,防止悬空指针:

  • BindRaw:直接绑定原始 C++ 对象指针。最高效也最危险,如果对象被销毁后调用委托,会导致程序崩溃。使用时需谨慎。
  • BindUObject****/ BindSP:绑定 UObject或智能指针对象。委托内部会持有对象的弱引用。在执行时,可以通过 ExecuteIfBound()安全地检查对象是否依然有效,避免访问已销毁的对象。
  • **BindLambda**:可以安全地捕获上下文。使用 BindWeakLambda时,同样能对捕获的 UObject进行有效性检查。

T***DelegateInstance大致有如下几种,代表不同的策略

TBaseUFunctionDelegateInstance,接收UFunction

TBaseSPMethodDelegateInstance,接收共享引用类型的类的成员函数

TBaseRawMethodDelegateInstance,接收普通类型的类的成员函数

TBaseUObjectMethodDelegateInstance,接收UObject类型的类的成员函数

TBaseStaticDelegateInstance,接收普通C++函数或类的静态函数

TBaseFunctorDelegateInstance,接收Lambda函数

TWeakBaseFunctorDelegateInstance,接收类成员Lambda函数

他们有个父类是TCommonDelegateInstanceState

而这个TCommonDelegateInstanceState才是真正保存绑定函数和函数所在类的地方,包括UserObject记录函数所在类的实例、MethodPtr记录绑定函数、Payload记录参数等

img

Payload

UE 委托支持 Payload 机制,允许你在绑定时就固定一部分参数。当触发委托时,这些预先绑定的 Payload 参数会排在执行时传入的参数之前,一起传递给目标函数。

1
2
3
4
5
6
7
8
// 假设有函数:void MyFunc(int RuntimeParam, float PreBoundParam, FString PreBoundString);
DECLARE_DELEGATE_OneParam(FMyDelegate, int32); // 运行时只接受一个int参数

FMyDelegate Delegate;
// 绑定时传入两个Payload参数:12.0f 和 "Hello"
Delegate.BindStatic(MyFunc, 12.0f, FString(TEXT("Hello")));
// 触发时,参数这样传递:MyFunc(100, 12.0f, "Hello");
Delegate.ExecuteIfBound(100);

Payload 数据通常使用 TTuple进行存储,并在调用时通过模板技巧展开。需要注意的是,动态委托不支持 Payload 功能

多播

多播代理是一个单独的类型TMulticastDelegate,父类是TMulticastDelegateBase,其实非常简单,里面有一个单播代理的数组InvocationList

同样Bind变成了Add

每次Add一个单播代理对象,会加到数组的最后

在Broadca实际上是以倒序方式依次ExecuteIfSafe每一个单播代理

关键注意事项

  • 性能考量:委托调用涉及虚函数表查找等开销,应避免在性能敏感的循环中频繁调用。
  • 生命周期管理:务必注意绑定对象的作用域。优先使用 BindUObjectBindSP等能提供弱引用检查的方式,并在可能的情况下使用 ExecuteIfBound()
  • 动态委托的代价:动态委托因为支持序列化和蓝图,其性能通常低于静态和多播委托。

希望这份详细的解释能帮助你更好地理解和使用 UE 的委托系统。如果你对某个具体的绑定方式或使用场景有更进一步的疑问,我很乐意继续探讨。

Lyra

https://blog.csdn.net/shuanger_/article/details/136324453

Flush

https://zhuanlan.zhihu.com/p/650581265

在虚幻引擎 (Unreal Engine) 中,Flush(刷新)机制是一套用于协调异步操作、确保数据一致性的核心设计模式。它广泛存在于引擎的各个子系统,其核心思想是:让一个线程(通常是游戏线程)等待另一个线程(如渲染线程、加载线程)完成特定的异步任务,然后再继续执行,从而避免因数据不同步而导致的错误或卡顿

为了让你快速了解其应用场景,下表总结了 Flush 机制在不同系统中的核心作用:

系统领域 核心 Flush 操作 主要作用
网络与回放 FlushNetDormancy 唤醒处于休眠状态的 Actor,强制其立即进行一次网络更新,确保客户端状态同步。
资源与对象管理 异步加载 Flush (如 SetActorLabel触发) 在编辑器下,强制完成所有正在进行的异步资源加载,可能导致卡顿。
媒体播放 FlushSinks/ Flush 在视频跳转时清空解码器内部缓存,确保从新位置开始解码,可能引起短暂卡顿。
渲染与实例更新 FlushInstanceUpdateCommands 等待渲染线程完成实例化网格(如 ISMC)的数据更新,确保游戏线程后续操作基于最新数据。

💡 深入理解 Flush 的工作机制

Flush 机制通常基于 “请求-响应”模式。你可以将其理解为一个线程间的同步令牌

  1. 请求发起:当游戏线程需要确保某个异步操作(如渲染数据更新)完成时,会创建一个 FFlushRequest 类的实例。这个实例内部包含一个标志(如布尔值 bCompleted)和一个同步事件(FEvent)。
  2. 状态标记:游戏线程将该请求发送给异步工作线程(如渲染线程),然后可以选择等待。工作线程处理完任务后,会调用请求的 MarkCompleted() 方法,原子性地标记该请求已完成。
  3. 等待与继续:游戏线程通过调用 WaitForCompletion() 方法阻塞自身,直到检测到请求被标记为完成。这确保了在刷新点之后,所有必要的数据都已准备就绪。

由于其阻塞特性,不合理地使用 Flush 会带来性能问题。例如,在视频播放器中,每一帧都调用 Flush 来清空样本接收器(FlushSinks)可能会带来不必要的性能开销。

🛠️ 实践中的注意事项与优化策略

  • 性能权衡:Flush 是阻塞操作。频繁调用或在关键性能路径上不当使用,会迫使游戏线程空闲等待,导致帧率下降或卡顿。因此,要避免在每帧或紧密循环中调用。
  • 网络休眠中的谨慎使用:对于设置为 DORM_DormantAll的 Actor,在修改其同步属性之前调用 FlushNetDormancy是最佳实践。如果修改后才调用,在某些复杂情况下(如修改休眠中的 FastArray),更改可能无法正确同步。
  • 编辑器与运行时差异:在编辑器模式下,一些操作(如 SetActorLabel)会触发异步加载的 Flush,这可能在 PIE 模式下是不必要的,从而引起卡顿。在打包后的游戏中,这类开销通常不存在。
  • 替代方案:在设计系统时,可以考虑使用事件驱动回调函数等非阻塞方式来进行线程间通知,减少对强制刷新(Flush)的依赖。

💎 总结

总而言之,Flush 机制是虚幻引擎保障多线程数据安全的基石之一。理解它的原理和应用场景,能帮助你有意识地避免性能陷阱,并正确地在需要确保数据一致性的关键时刻使用它。

希望这些信息能帮助你更好地理解虚幻引擎中的 Flush 机制。如果你有特定的应用场景想深入了解,我们可以继续探讨。

数学

欧拉角:

  • FRotator:

    • pitch():俯仰,将物体绕X轴旋转(localRotationX)

    • yaw():航向,将物体绕Y轴旋转(localRotationY)

    • roll():横滚,将物体绕Z轴旋转(localRotationZ)

UE底层能把欧拉角转换为四元数 以免万向节

UE默认旋转顺序为 Yaw → Pitch → Roll

四元数封装在 **FQuat**中

  • FQuat::Slerp():球面插值(平滑角速度)。
  • FQuat::FastLerp():快速线性插值(性能优化,轻微精度损失)

QA

actor与uobject的关系 gc 委托有哪些种类 蓝图通信方式 UE有哪些线程 自定义事件与函数的区别 GAS c++的类型转换 静态多态和动态多态 虚继承 右值引用和移动语义 用过哪些脚本语言 大世界流式关卡加载有卡顿怎么解决 同一范围内有大量行为树运行如何优化 游戏内存溢出如何解决