UE基础
前言
本篇为UE使用过程中关于某些API 或者 某些底层机制的Common 处理。
某些东西深入研究可以多开一篇文章。
FSoftObjectPath
在 Unreal Engine(UE)中,FSoftObjectPath 是一种用于间接引用资产(Asset)的核心机制,它通过存储资源的路径字符串而非直接加载资源,实现按需加载和内存优化。以下是其核心概念、用法及技术细节:
🔍 一、FSoftObjectPath 核心概念
- 作用与原理
- 路径格式解析 资源路径的典型结构:
- [资源类型]’[分区]/[路径]/[包名].[对象名]’
- 示例:
StaticMesh'/Game/Meshes/Cube.Cube'
⚙️ 二、代码实现与用法
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 释放 |
💡 四、实际应用场景
- 动态资源加载
- 根据玩家进度加载不同关卡资源,减少初始包体积5。
- 示例:开放世界游戏中按区域加载地形网格。
- 资源热更新 通过重定向路径到下载的新资源包,实现热更新6。
- 避免循环依赖 软引用可解耦模块间依赖,例如:
Gameplay模块通过路径引用Asset模块的资源,无需编译依赖。
⚠️ 五、常见问题与优化
- 路径失效
- 原因:资源移动或重命名后路径未更新。
- 解决方案:使用 重定向器(Redirector) 或编辑器工具 Fix Up Redirectors。
- 异步加载卡顿
- 优化策略:分帧加载(如每帧加载 1 个资源),或预加载到内存池4。
- 类型安全
- 使用
- TSoftObjectPtr
- 替代
- FSoftObjectPath
- ,限制资源类型:
- TSoftObjectPtr
SafeTexturePath; // 仅允许纹理类
💎 总结
- 核心价值:
FSoftObjectPath通过路径解耦资源引用,是优化内存与加载性能的关键工具。 - 最佳实践:
- 大型资源用异步加载 + 回调;
- 高频小资源用硬引用;
- 动态场景用
TSoftObjectPtr确保类型安全4,6。
- 动态场景用
- 调试工具:控制台命令
Obj List查看加载的资源状态。
深入参考:
- UE 官方文档:Async Asset Loading
- 实战案例:UE5 Resource Management
Actor生命周期
https://dev.epicgames.com/documentation/zh-cn/unreal-engine/unreal-engine-actor-lifecycle

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的属性。 |
- 从磁盘加载 (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 |
- 在编辑器中播放 (Play in Editor - PIE)
此路径与“从磁盘加载”非常相似,但源不同,主要用于调试。8
| 阶段 | 描述 | 与“从磁盘加载”路径的主要差异 |
|---|---|---|
| 复制Actor | 将编辑器中的原始 Actor 复制到一个新的、专用于PIE的世界中。 | 源是编辑器中的现有对象,而非磁盘上的包文件。 |
| PostDuplicate | 复制完成后调用。 | 替代了 PostLoad 阶段,是PIE路径独有的阶段。8 |
| InitializeActorsForPlay | 同“从磁盘加载”路径。 | |
| RouteActorInitialize | 同“从磁盘加载”路径。 | |
| PreInitializeComponents | 同“从磁盘加载”路径。 | |
| InitializeComponent | 同“从磁盘加载”路径。 | |
| PostInitializeComponents | 同“从磁盘加载”路径。 | |
| BeginPlay | 同“从磁盘加载”路径。 |
- 动态生成 (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 | 同“从磁盘加载”路径。 |
- 延迟生成 (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。
- “不影响子控件”:这是
SelfHitTestInvisible和HitTestInvisible的主要区别。如果父控件设置为HitTestInvisible,则其所有子控件也会变得不可交互。而SelfHitTestInvisible仅禁用控件自身的交互,子控件保持原有的交互状态2,6,7。
🛠 使用方法
在蓝图中: 在控件细节面板的 “Appearance” 部分,直接找到 “Visibility” 下拉菜单选择即可。
在 C++ 中:
1 | // 假设 YourWidget 是某个控件指针 |
在 UnLua 中(如果项目使用了Lua绑定):
1 | local visibleType = UE.ESlateVisibility.SelfHitTestInvisible |
(用法参考了 Visible 和 Hidden 的设定 1)
🧩 主要应用场景
- 装饰性UI元素:显示一些无需交互的图片、背景、装饰图案等。
- 视觉遮罩或提示:需要半透明遮罩层来提示用户某个区域,但又不想阻挡用户与下层UI的交互。
- 禁用状态视觉表现:有时希望控件在禁用时不仅变灰,而且鼠标事件能穿透它,可能会用到此选项。
💎 实用建议
- 与
HitTestInvisible区分:关键在于你是否希望子控件可交互。如果需要子控件交互,用SelfHitTestInvisible;如果希望父子控件都不可交互,用HitTestInvisible。 - 和
Hidden/Collapsed区分:Hidden和Collapsed是完全不可见的2,4。如果你的控件需要可见但不拦截输入,就应选择SelfHitTestInvisible或HitTestInvisible。 - 事件穿透的另一种方法:有时为了实现更复杂的点击事件穿透(例如,仅让按钮的透明部分穿透点击),除了设置
SelfHitTestInvisible,还可能需配合调整点击检测(Hit Test) 的相关设置或使用透明材质8。
希望这些信息能帮助你更好地理解和使用 ESlateVisibility.SelfHitTestInvisible。
10.13
BVM
蓝图虚拟机
https://zhuanlan.zhihu.com/p/54174686
https://zhuanlan.zhihu.com/p/688017291
1 | // Evaluatable expression item types. |
UE4/UE5 的蓝图虚拟机(Blueprint Virtual Machine)是一个设计精巧的系统,它将可视化的节点图转化为可执行的游戏逻辑。其核心在于 将蓝图节点编译成字节码(Bytecode),然后通过一个轻量级的解释器(即虚拟机)来逐条执行这些指令。
为了帮助你快速构建起对蓝图虚拟机工作流程的整体认知,可以参考下面的这张核心流程图:
1 | flowchart TD |
🔁 从节点到字节码:编译过程
蓝图虚拟机执行的第一步,是将可视化的节点图转换为它能够理解的指令序列。
- 节点图处理与优化:编译器(主要由
FKismetCompilerContext驱动)首先会对节点图进行分析和优化。这包括将宏等复合节点展开成基础节点,并通过依赖分析移除未被任何事件节点连接的“死代码”。 - 生成中间表示:处理后的节点图不会被直接翻译成字节码,而是先被转换成一种线性的中间表示——
FBlueprintCompiledStatement列表。这个列表可以看作是一种平台无关的“汇编指令”,它清晰地描述了操作的顺序和逻辑,为后续的优化和代码生成奠定了基础。值得一提的是,蓝图编译器跳过了传统编译器构建抽象语法树(AST)的步骤,因为节点图本身就已经是一种结构化的、可视化的程序表示。 - 字节码生成:最后,一个称为“VM后端”(
FKismetCompilerVMBackend)的组件会遍历中间表示列表,为每一条语句生成对应的虚拟机字节码。这些字节码是EExprToken枚举中定义的数字指令,例如EX_CallMath表示调用一个数学函数。生成的字节码会存储在UFunction对象的Script数组成员中。
⚙️ 运行时核心:虚拟机如何执行字节码
当游戏运行时(例如,一个 Event BeginPlay被触发),虚拟机的执行引擎便开始工作。
- 创建栈帧:执行一个蓝图函数,首先会创建一个栈帧(
FFrame)。这个栈帧是执行上下文的核心,它包含了执行所需的所有关键信息:UObject* Object: 执行此函数的对象实例(即this指针)。UFunction* Node: 当前正在执行的函数对象。uint8* Code: 指向当前要执行的字节码的指针(相当于程序计数器PC)。uint8* Locals: 指向该函数局部变量内存块的指针。FFrame* PreviousFrame: 指向上一个栈帧的指针,用于维护调用栈。
- 指令分派与执行:执行引擎通过一个循环不断地读取并执行字节码。核心的步进函数是
FFrame::Step()。它的工作流程非常简单高效:
1 | void FFrame::Step(UObject* Context, RESULT_DECL) { |
- 参数与返回值传递:字节码中不仅包含操作指令,还嵌入了执行所需的数据(如函数参数、常量值)。虚拟机通过
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
宏定义
- 1.单播委托: TDelegate模板类,通常用
DECLARE_DELEGATE及DECLARE_DELEGATE_XXXXParams宏进行声明,后面的宏表示带XXXX个参数 - 2.多播委托: TMulticastDelegate模板类,通常用
DECLARE_MULTICAST_DELEGATE及DECLARE_MULTICAST_DELEGATE_<Num>Params宏进行声明,后面的宏表示带XXXX个参数 - 3.动态委托: 继承于TBaseDynamicDelegate,TBaseDynamicMulticastDelegate模板类,通常用
DECLARE_DYNAMIC_DELEGATE及DECLARE_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,并且对新申请的内存空间进行类型转换

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记录参数等

Payload
UE 委托支持 Payload 机制,允许你在绑定时就固定一部分参数。当触发委托时,这些预先绑定的 Payload 参数会排在执行时传入的参数之前,一起传递给目标函数。
1 | // 假设有函数:void MyFunc(int RuntimeParam, float PreBoundParam, FString PreBoundString); |
Payload 数据通常使用 TTuple进行存储,并在调用时通过模板技巧展开。需要注意的是,动态委托不支持 Payload 功能。
多播
多播代理是一个单独的类型TMulticastDelegate,父类是TMulticastDelegateBase,其实非常简单,里面有一个单播代理的数组InvocationList
同样Bind变成了Add
每次Add一个单播代理对象,会加到数组的最后
在Broadca实际上是以倒序方式依次ExecuteIfSafe每一个单播代理
关键注意事项
- 性能考量:委托调用涉及虚函数表查找等开销,应避免在性能敏感的循环中频繁调用。
- 生命周期管理:务必注意绑定对象的作用域。优先使用
BindUObject和BindSP等能提供弱引用检查的方式,并在可能的情况下使用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 机制通常基于 “请求-响应”模式。你可以将其理解为一个线程间的同步令牌。
- 请求发起:当游戏线程需要确保某个异步操作(如渲染数据更新)完成时,会创建一个
FFlushRequest类的实例。这个实例内部包含一个标志(如布尔值bCompleted)和一个同步事件(FEvent)。 - 状态标记:游戏线程将该请求发送给异步工作线程(如渲染线程),然后可以选择等待。工作线程处理完任务后,会调用请求的
MarkCompleted()方法,原子性地标记该请求已完成。 - 等待与继续:游戏线程通过调用
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++的类型转换 静态多态和动态多态 虚继承 右值引用和移动语义 用过哪些脚本语言 大世界流式关卡加载有卡顿怎么解决 同一范围内有大量行为树运行如何优化 游戏内存溢出如何解决

