背景和内容提要
虚幻引擎的蓝图系统长期存在争议,有人认为其提升开发效率,也有人批评其性能不佳。本文聚焦蓝图在运行时(CPU)层面的性能表现,深入分析各类基础用法的实际开销、底层实现原理及优化策略,并结合真实项目案例进行说明。
核心目标是明确:蓝图的主要问题是否在于性能?其性能是否差到无法用于正式项目?进而为合理使用蓝图提供依据。
本文重点不在UMG、动画蓝图等高级应用场景,也不涉及内存优化,而是围绕事件、函数、节点调用等直接影响CPU性能的基础操作展开。
Animation Blueprints
蓝图的各种用法
本文所指“用法”特指与运行时性能直接相关的底层操作,主要包括以下几类:
事件
包括Begin Play、Tick、输入事件、碰撞事件等内置事件,以及自定义事件、C++中声明的BlueprintNativeEvent和BlueprintImplementableEvent、事件分发器等。事件驱动是蓝图逻辑的核心机制,其中Tick因高频调用常被视为性能隐患。
函数
涵盖蓝图内定义的函数、C++中标记为BlueprintCallable的函数、接口契约函数等。函数调用是模块化逻辑的重要手段,频繁出现在代码与蓝图交互场景中。
宏和折叠图表
用于组织复杂蓝图逻辑,提升可读性。仅限蓝图内部使用,在编译时展开为原始节点,不支持跨域调用。
节点
包含基于Blueprint Function Library的同步节点、基于UBlueprintAsyncActionBase的异步节点,以及常用内置节点如ForEach、Switch、Cast、PrintString等。这些节点广泛用于实现具体功能,性能差异显著。
委托回调
常用于异步操作完成后触发指定逻辑,是实现延迟执行的关键机制之一。
属性读写
涉及对蓝图或C++定义属性的访问,尤其在跨域调用时可能带来不可忽视的开销。
跨域交互
现代虚幻项目常结合C++、蓝图与脚本语言(如C# Mono/CoreCLR)协同开发,形成“引擎(C++)、脚本、蓝图”三类主体间的调用关系。本文以C#为例分析跨域调用带来的额外开销。
掌握上述各类用法的性能特征,是优化蓝图整体表现的基础。
各用法的性能数据
以下性能数据基于UE5.4 Development环境下的实测结果,采用10000次调用取平均值的方式获取,重点关注单次及千次调用耗时。
蓝图相关的用法和性能
概览图(建议收藏)
测试覆盖了不同调用者与被调用者组合下的函数、事件、节点、属性读写等操作,并包含典型节点如ForEach、IsValid、Switch、Cast、Sequence、PrintString的专项测试。
特殊节点的调用和执行
“节点 - PrintString”开销最大,单次达54,469ns(0.054ms),千次调用耗时54.47ms,虽仅在Development模式生效,但仍需谨慎使用。
“节点 - ForEach - 10次”单次耗时11,228ns,千次约11.23ms;即使空数组遍历,千次仍需0.98ms,印证其高开销特性。
其他节点执行开销:IsValid 569ns,Cast 220ns,Switch 106.25ns,Sequence 67.66ns。注意此类为包含执行逻辑的总耗时。
节点的调用
脚本调用异步节点单次耗时7,963ns,千次7.96ms,开销显著。C++定义的异步节点调用开销为2,779ns,千次2.78ms,表明其本身机制成本较高。
脚本同步节点调用开销为1,651ns,千次1.65ms;C++同步节点则低至181ns,千次0.18ms。参数数量影响较小。
事件调用
脚本调用C++定义的BlueprintNativeEvent耗时1,026ns,BlueprintImplementableEvent为718ns;C++调用后者仅34ns。
脚本调用蓝图自定义事件耗时1,346ns,C++调用为756ns,蓝图直接调用最低,为602ns。
属性读写
脚本通过GetPropertyValue/SetPropertyValue读写UObject属性单次耗时1,328ns,Int属性784ns。
蓝图读写原生属性仅30ns左右,C++读写自身属性不足3ns,开销极低。
函数型调用
C++直接调用函数开销约3ns,脚本直接调用函数约6ns。
脚本调用蓝图函数耗时1,008ns;C++/蓝图调用脚本函数分别为607ns/550ns;C++调用蓝图函数454ns,蓝图调用305ns。
蓝图宏与折叠图表调用开销约为300ns级,仅限蓝图内部调用。
委托调用
脚本调用脚本委托单次577ns,C++调用C++委托仅18ns。
小结
- 节点执行开销远大于调用开销。
- 异步节点调用开销大,同步节点次之。
- 调用脚本中定义的蓝图元素开销更高。
- 事件调用开销普遍高于函数。
- ForEach和PrintString性能代价显著。
- 脚本中定义的属性读写开销大。
- 蓝图读写C++或自身属性开销可控。
性能测试原始数据
原始数据包含万次调用总耗时、平均单次耗时等,部分数值与汇总表略有差异属正常波动。
切换至CoreCLR后性能有明显提升。
各用法的实现原理
说明
本节简述关键机制,详细原理可参考相关技术文章。
关于蓝图虚拟机
所有蓝图相关操作均需经由蓝图虚拟机解析字节码执行,构成核心开销来源。
关于 JIT 编译
使用C#脚本时,Mono虚拟机的JIT编译会引入额外开销,首次执行尤为明显。
节点
节点编译为虚拟机指令执行。同步节点调用代理函数,异步节点还需处理Active状态切换等流程。
ProcessEvent
绝大多数蓝图调用最终都会进入ProcessEvent,依赖蓝图虚拟机执行。
CallUFunction
CallUFunction与CallParentUFunction底层均通过ProcessEvent实现。
ProcessDelegate
委托调用本质仍是ProcessEvent。
Broadcast
广播机制同样基于ProcessEvent。
InternalCall
可绕过蓝图虚拟机直接调用C++接口,但无法跳过JIT和Mono虚拟机。
基于数据和原理的性能优化
基于前文的数据的优化
根据性能数据可得出以下优化建议:
- 避免在节点中编写重度业务逻辑,优先从设计层面降低复杂度。
- 减少同步节点数量,控制并行异步节点的使用频率。
- 慎用脚本中定义的蓝图函数、事件及属性,降低调用频次。
- 事件调用开销高于函数,避免滥用继承式事件。
- 禁用不必要的PrintString输出,尽量避免使用ForEach,必要时封装为节点。
- 无需过度担忧蓝图对C++属性的读写性能。
- 杜绝在Tick中执行高频蓝图逻辑,尤其是跨域调用。
关键是节点中的业务逻辑的复杂度
实际性能瓶颈往往不在调用本身,而在节点内部的业务逻辑复杂度。应优先审视逻辑实现方式,而非归咎于蓝图或虚拟机性能。
关于”Nativizing Blueprints“
该功能已在UE5中废弃,高性能计算应通过手写C++实现。
关于加载耗时
若加载缓慢,检查是否存在对重型资源的硬引用(如Cast操作),考虑预加载、异步加载或分帧处理。
项目中的实际应用案例
说明
项目中未在Tick中执行重逻辑,性能问题主要表现为瞬时卡顿,通常发生在事件触发、蓝图函数调用或异步节点激活时。
多数情况下,卡顿并非源于蓝图调用本身的开销,而是其所触发的重度业务逻辑所致。优化方向应聚焦于重构高耗时操作,降低执行频率。
因涉及内部信息,具体案例略。
本文的性能测试用例
测试环境
基于UE5.4 Development版本,Editor环境下测试。脚本使用Mono,后续将补充CoreCLR对比数据。
测试原则
采用原子化测试,针对每种用法独立测量。执行10000次取平均值,重点关注单次及千次调用耗时,排除具体业务逻辑干扰。
测试准备
构建C++、脚本(C#)、蓝图三类测试主体,涵盖函数、事件、属性、同步/异步节点等测试对象,并设置专用性能测量工具类。
测试步骤
通过按键触发,依次执行C++、脚本、蓝图侧的性能测试函数,自动输出各项耗时数据。
注意
C#脚本首次执行存在JIT开销,本文数据已通过多次测量取平均值以减小误差。
总结
蓝图核心用法包括函数调用、事件触发、节点执行等,底层依赖蓝图虚拟机解释执行,跨域调用还会引入额外开销。
性能方面,若规避Tick和ForEach滥用,主要瓶颈通常不在蓝图调用本身,而在于节点内嵌的业务逻辑复杂度。异步节点调用开销最大,其次为同步节点和事件调用。
尽管蓝图性能并非首要顾虑,但仍需合理使用,结合C++处理高性能需求,并关注可维护性与调试效率。

