ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

UE5蓝图开发者C++进阶指南:从节点到代码的性能优化与功能扩展

2026/8/12 19:57:47 拓冰建站 浏览量
UE5蓝图开发者C++进阶指南:从节点到代码的性能优化与功能扩展 1. 项目概述为什么蓝图开发者需要拥抱C如果你已经用虚幻引擎5的蓝图系统搭建过一些功能甚至完成过一个小项目那你肯定体验过它的直观和高效。节点拖拽、连线、实时编译所见即所得这对于快速原型设计和逻辑验证来说简直是“生产力神器”。但不知道你有没有遇到过这样的场景项目规模稍微大一点蓝图图表就变得像蜘蛛网一样错综复杂难以维护或者某个需要高频调用的计算逻辑在蓝图中运行起来总觉得有点“肉”性能上不去又或者你想实现一个引擎本身没有提供的、更底层的功能却发现蓝图的节点库“够不着”了。这时C就该登场了。很多人一听到“C”就觉得是另一个世界的东西复杂、难懂、充满了指针和内存泄漏的恐惧。但对于UE5的蓝图开发者来说学习C的目标非常明确不是为了成为C语言专家而是为了“赋能”你的蓝图。你可以把C看作是蓝图的“超级后台”。在C里你可以编写高性能、可复用的核心逻辑封装成清晰简洁的节点再暴露给蓝图使用。这样蓝图依然是你搭建游戏逻辑的主要界面但它背后的“肌肉”和“骨骼”变得更加强壮和高效了。这个项目就是带你从一名熟练的蓝图使用者出发一步步掌握如何用C来扩展和自定义你的蓝图。我们不会从“Hello World”讲起而是直接从你最熟悉的蓝图场景切入告诉你如何把蓝图里的想法用C更优雅、更强大地实现出来并最终反哺到你的蓝图工作流中。这就像给你的蓝图工具箱里添加了一套精密的瑞士军刀。2. 核心理念C与蓝图的共生关系与边界划分在开始动手之前我们必须先理清C和蓝图在UE5项目中的定位这决定了我们如何分配工作以及学习C的侧重点。2.1 明确分工什么该用C什么该用蓝图这是一个经典问题没有绝对答案但有一些被广泛认可的最佳实践原则C 负责“是什么”和“核心怎么做”定义游戏框架GameMode, PlayerController, GameState 等核心类的逻辑和规则定义。实现底层系统网络同步Replication的核心逻辑、自定义的存档系统、复杂的AI决策树、物理交互的底层计算。创建高性能工具需要大量循环、复杂数学运算如寻路算法、 procedural generation、或每帧调用的逻辑。封装引擎功能将引擎的C接口或第三方C库的功能包装成蓝图友好的函数和变量。定义数据结构和枚举创建项目全局使用的、结构化的数据类型USTRUCT和枚举UENUM供蓝图和C共同使用。蓝图 负责“什么时候做”和“具体表现成什么样”组装和配置将C创建的Actor、Component在场景中实例化并设置其初始属性如网格体、材质、生命值。实现一次性或简单的游戏流程关卡特定的谜题逻辑、过场动画序列、UI界面的流程控制。调整和迭代快速调整角色的移动速度、武器的伤害值、特效的播放参数。这是蓝图迭代速度快的优势所在。实现视觉和音频反馈播放动画蒙太奇、触发粒子特效、播放音效。这些通常与时间线和事件绑定紧密用蓝图更直观。一个简单的类比C是建造房屋的钢筋混凝土结构和水电管道系统坚固、高效、定义规则而蓝图是内部的装修、家具摆放和灯光控制灵活、直观、快速调整。你不可能用蓝图去浇筑水泥也很难用C去微调一盏台灯的角度。2.2 UBT与UHT理解虚幻的构建魔法当你开始在UE5项目中编写C代码时会频繁接触到两个工具Unreal Build Tool (UBT) 和 Unreal Header Tool (UHT)。理解它们能帮你避开很多编译错误。Unreal Header Tool (UHT)代码生成器。这是UE的“魔法”核心。当你写了一个UCLASS()、UFUNCTION()或UPROPERTY()这样的宏时UHT会在编译前扫描你的头文件.h。它读取这些宏理解你想向蓝图或编辑器暴露什么然后自动生成大量的“胶水代码”.generated.h 文件。这些生成代码负责处理反射运行时获取类/属性信息、序列化存档/读档、蓝图通信、网络复制等繁琐工作。你的代码必须能被UHT正确解析否则编译会失败。常见的UHT错误包括宏格式错误、在错误的区域比如类声明末尾的GENERATED_BODY()宏之后添加普通C代码等。Unreal Build Tool (UBT)构建系统。它类似于Make或CMake但专门为虚幻项目设计。你右键点击 .uproject 文件选择“Generate Visual Studio project files”就是UBT在工作。它负责调用编译器如MSVC、链接器并管理模块.Build.cs文件之间的依赖关系。当你新增或删除了C类通常需要重新生成项目文件就是为了让UBT更新构建配置。实操心得在Visual Studio中编译失败时不要只看最后那个C编译错误。先往上翻看看有没有UHT相关的错误信息通常以“UnrealHeaderTool”开头。解决UHT错误往往是头文件宏的问题是解决编译问题的第一步。3. 开发环境搭建与第一个C类工欲善其事必先利其器。对于蓝图开发者转向C的第一步不是写代码而是把环境理顺。3.1 环境准备清单安装 Visual Studio 2022社区版免费。安装时在工作负载中务必勾选“使用C的桌面开发”和“游戏开发与C”。后者会包含Windows SDK等必要组件。安装 Unreal Engine 5通过Epic Games启动器安装建议选择稳定的发行版本如5.3, 5.4而非预览版以减少环境问题。安装 .NET FrameworkUBT需要它通常Windows系统已自带或安装VS时会附带。安装 Git可选但推荐用于版本控制。UE项目文件众多用Git管理注意配置合适的 .gitignore 文件是专业开发的基本功。3.2 创建第一个“蓝图可调用”的C类我们从一个最贴近蓝图思维的例子开始创建一个可以增加玩家得分的C组件并在蓝图中调用它。在编辑器中创建C类在内容浏览器中右键 - 新建 - 新建C类。不要选择“无”而是选择“Actor组件”Actor Component。将其命名为ScoreManagerComponent。这个操作会自动为你生成 .h 和 .cpp 文件并添加到项目编译中。解读生成的头文件打开ScoreManagerComponent.h你会看到类似下面的结构#pragma once #include CoreMinimal.h #include Components/ActorComponent.h #include ScoreManagerComponent.generated.h // 注意这是UHT生成的文件 UCLASS(ClassGroup(Custom), meta(BlueprintSpawnableComponent)) // 关键宏 class YOURPROJECT_API UScoreManagerComponent : public UActorComponent { GENERATED_BODY() public: UScoreManagerComponent(); protected: virtual void BeginPlay() override; public: virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; };UCLASS(BlueprintSpawnableComponent)这个宏是关键。它告诉引擎和UHT这个组件可以在蓝图中被创建和添加到Actor上。去掉它你在蓝图的“添加组件”下拉菜单里就找不到它了。GENERATED_BODY()必须放在类体的最开头。这是UHT生成代码的“插入点”绝对不能删除或移动。YOURPROJECT_API这是你的项目模块的导出标识符确保这个类能被其他模块使用。添加一个蓝图可调用的函数在public:区域下添加以下函数声明// ScoreManagerComponent.h public: UFUNCTION(BlueprintCallable, CategoryScore) // 关键宏 void AddScore(int32 PointsToAdd);UFUNCTION(BlueprintCallable, CategoryScore)这是核心魔法。BlueprintCallable意味着这个函数可以在蓝图中被调用就像你拖出一个节点。CategoryScore这个函数在蓝图的节点菜单中会出现在“Score”分类下方便查找。实现函数逻辑打开ScoreManagerComponent.cpp实现这个函数// ScoreManagerComponent.cpp void UScoreManagerComponent::AddScore(int32 PointsToAdd) { // 假设我们有一个存储分数的变量稍后会添加 // CurrentScore PointsToAdd; // 这里我们先打印日志验证函数被调用 UE_LOG(LogTemp, Warning, TEXT(AddScore called! Points: %d), PointsToAdd); // 在实际项目中你可能会在这里更新UI、播放音效等 // 例如OnScoreUpdated.Broadcast(CurrentScore); // 如果定义了一个委托 }编译项目在VS中按F5启动调试或CtrlF5开始执行不调试编译。UE编辑器会自动关闭并重新启动。如果编译成功你就完成了第一步在蓝图中使用在内容浏览器中创建一个新的蓝图类继承自Actor比如叫BP_Player。打开BP_Player在组件面板点击“添加组件”搜索Score Manager你应该能看到你的ScoreManagerComponent添加它。在事件图表中右键搜索“Add Score”你应该能看到Add Score节点它来自你的ScoreManagerComponent。连接一个事件比如按“E”键传入一个数值运行游戏并按键查看输出日志确认函数被调用。注意事项第一次编译C项目可能会比较慢因为UE需要编译引擎模块和你自己的模块。确保磁盘空间充足SSD最佳。如果编译失败仔细阅读输出窗口的错误信息通常能定位到语法错误或缺少头文件包含#include。4. 深入UFUNCTION与UPROPERTY连接C与蓝图的桥梁仅仅能调用函数还不够我们需要在C和蓝图之间传递数据和控制流程。UFUNCTION和UPROPERTY宏的各种参数说明符就是实现这一点的关键。4.1 UFUNCTION 的进阶用法UFUNCTION除了BlueprintCallable还有其他几个重要的说明符用于定义函数在蓝图中的行为BlueprintPure声明函数为“纯函数”。它在蓝图中显示为没有执行引脚只有输出引脚的节点。这意味着它不修改对象的状态仅根据输入返回值。适用于计算类函数如计算两点距离、获取某个属性值。使用BlueprintPure的函数在蓝图中可以被任意连接不会强制排序。UFUNCTION(BlueprintPure, CategoryMath|Helper) static float CalculateDamage(float BaseDamage, float DefenseMultiplier);BlueprintImplementableEvent声明一个蓝图可实现事件。你在C中只声明这个函数但不提供实现。具体的实现完全在蓝图中通过覆盖Override该事件来完成。这用于在C中定义一个“钩子”让蓝图设计师来决定具体行为。注意函数不能有返回值通常返回类型为void。UFUNCTION(BlueprintImplementableEvent, CategoryGameplay) void OnCharacterDied(); // C中不实现在蓝图中你可以在“我的蓝图”-“函数”部分找到“重写”Override下拉菜单选择OnCharacterDied并实现它。BlueprintNativeEvent这是最强大的一个。它声明一个蓝图可覆盖的C原生事件。你在C中提供一个默认实现函数名后加_Implementation同时蓝图也可以选择覆盖这个实现。调用时如果蓝图有覆盖则执行蓝图的否则执行C的默认实现。这是实现可扩展框架的常用手段。// 头文件声明 UFUNCTION(BlueprintNativeEvent, CategoryInteraction) bool CanInteractWith(AActor* OtherActor); // 源文件实现默认逻辑 bool AMyCharacter::CanInteractWith_Implementation(AActor* OtherActor) { // 默认逻辑检查距离和视线 return (OtherActor ! nullptr) (FVector::Distance(GetActorLocation(), OtherActor-GetActorLocation()) 500.f); }在蓝图中你同样可以“重写”这个函数添加额外的条件比如检查钥匙是否持有。实操心得BlueprintImplementableEvent和BlueprintNativeEvent的参数类型必须是“蓝图兼容”的。简单类型int, float, FString, FVector和由UCLASS,USTRUCT,UENUM定义的类型通常可以。涉及复杂C模板或指针的类型可能需要额外处理。4.2 UPROPERTY 的蓝图暴露与控制UPROPERTY宏用于将C变量暴露给蓝图、编辑器细节面板并控制其行为如复制、保存、可视化。基础暴露BlueprintReadWrite允许蓝图读写该变量。BlueprintReadOnly只允许读。UPROPERTY(EditAnywhere, BlueprintReadWrite, CategoryStats) int32 MaxHealth; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, CategoryStats) int32 CurrentHealth; // 可能只在C中修改但蓝图需要读取编辑器集成EditAnywhere在蓝图类默认值Defaults和放置在场景中的实例Instance上均可编辑。EditDefaultsOnly只能在蓝图类默认值中编辑实例中不可修改。适合定义“原型”属性。VisibleAnywhere在任何地方可见但不可编辑。VisibleDefaultsOnly仅在默认值中可见。BlueprintReadOnly配合EditAnywhere是无效的因为“可编辑”隐含了“可写”。高级控制Replicated网络复制。标记后当服务器端变量变化时会自动同步到所有客户端。需要配合GetLifetimeReplicatedProps函数。SaveGame标记该变量需要被序列化到存档中。Transient临时变量不保存不复制。常用于运行时计算缓存。meta (ClampMin0, ClampMax100)在编辑器细节面板中为数值变量添加滑块和范围限制。UPROPERTY(EditAnywhere, BlueprintReadWrite, CategoryStats, meta(ClampMin0.0, ClampMax1.0)) float CriticalChance;常见问题为什么我在蓝图中修改了EditDefaultsOnly的属性但场景中的实例没变化因为EditDefaultsOnly修改的是“蓝图资产”本身的默认值。已经放置在场景中的实例Instance保存的是它生成时的一份独立拷贝。要更新场景中的实例你需要选中它们然后在细节面板中手动修改如果属性是EditAnywhere或者重新从内容浏览器拖拽该蓝图到场景。5. 从蓝图迁移到C实战案例解析理论讲完了我们来看一个具体的迁移案例。假设你有一个蓝图BP_Door它有一个布尔变量bIsOpen一个时间轴控制门的旋转以及一个Interact事件。5.1 分析蓝图逻辑你的BP_Door蓝图可能包含一个bIsOpen变量。一个Interact事件切换bIsOpen根据状态播放时间轴开门或关门动画。时间轴更新事件根据时间轴输出值设置门的相对旋转Roll或Yaw。5.2 设计C类结构我们创建一个C类ADoorActor继承自AActor来替代它。创建C类在编辑器中选择“Actor”作为父类创建DoorActor。声明成员变量和函数DoorActor.h#pragma once #include CoreMinimal.h #include GameFramework/Actor.h #include DoorActor.generated.h UCLASS() class YOURPROJECT_API ADoorActor : public AActor { GENERATED_BODY() public: ADoorActor(); protected: virtual void BeginPlay() override; public: virtual void Tick(float DeltaTime) override; // 蓝图可调用的交互函数 UFUNCTION(BlueprintCallable, CategoryDoor) void Interact(); // 蓝图可读写的门状态 UPROPERTY(EditAnywhere, BlueprintReadWrite, CategoryDoor) bool bIsOpen; // 蓝图可实现事件当门开始打开或关闭时触发用于播放音效等 UFUNCTION(BlueprintImplementableEvent, CategoryDoor) void OnDoorStateChanged(bool bNewOpenState); private: // 内部状态是否正在动画中 bool bIsAnimating; // 动画计时器 float AnimationTimer; // 动画总时长 UPROPERTY(EditDefaultsOnly, CategoryDoor) float AnimationDuration; // 起始旋转 FRotator StartRotation; // 目标旋转开门和关门不同 FRotator TargetRotationOpen; FRotator TargetRotationClosed; // 内部更新动画的函数 void UpdateAnimation(float DeltaTime); };实现核心逻辑DoorActor.cpp#include DoorActor.h #include Components/StaticMeshComponent.h // 假设门是一个静态网格体 ADoorActor::ADoorActor() { PrimaryActorTick.bCanEverTick true; // 需要每帧Tick来更新动画 bIsOpen false; bIsAnimating false; AnimationTimer 0.0f; AnimationDuration 1.0f; // 默认1秒动画 // 创建并设置根组件静态网格体 DoorMesh CreateDefaultSubobjectUStaticMeshComponent(TEXT(DoorMesh)); RootComponent DoorMesh; // 设置默认目标旋转假设绕Z轴旋转90度开门 TargetRotationClosed FRotator::ZeroRotator; TargetRotationOpen FRotator(0.0f, 90.0f, 0.0f); } void ADoorActor::BeginPlay() { Super::BeginPlay(); StartRotation DoorMesh-GetRelativeRotation(); // 初始目标旋转取决于初始状态 // 这里简化处理实际可能需要根据bIsOpen初始值设置一个中间状态 } void ADoorActor::Tick(float DeltaTime) { Super::Tick(DeltaTime); if (bIsAnimating) { UpdateAnimation(DeltaTime); } } void ADoorActor::Interact() { if (bIsAnimating) return; // 动画中不响应 bIsOpen !bIsOpen; // 切换状态 bIsAnimating true; AnimationTimer 0.0f; // 设置动画起始和目标旋转 StartRotation DoorMesh-GetRelativeRotation(); TargetRotation bIsOpen ? TargetRotationOpen : TargetRotationClosed; // 调用蓝图事件通知状态改变 OnDoorStateChanged(bIsOpen); } void ADoorActor::UpdateAnimation(float DeltaTime) { AnimationTimer DeltaTime; float Alpha FMath::Clamp(AnimationTimer / AnimationDuration, 0.0f, 1.0f); // 使用简单的线性插值 FRotator NewRotation FMath::Lerp(StartRotation, TargetRotation, Alpha); DoorMesh-SetRelativeRotation(NewRotation); if (Alpha 1.0f) { bIsAnimating false; // 动画结束确保旋转精确到位 DoorMesh-SetRelativeRotation(TargetRotation); } }5.3 在蓝图中完成表现层现在C类ADoorActor已经处理了核心状态逻辑和插值动画。编译后你可以在内容浏览器中基于DoorActor创建一个新的蓝图类例如BP_Door_CppBased。打开这个蓝图在细节面板中为DoorMesh分配一个门的静态网格体。在事件图表中重写OnDoorStateChanged事件。在这个事件中你可以根据bNewOpenState参数播放开门或关门的音效、触发粒子特效等所有视觉、听觉反馈。其他逻辑如Interact的调用可以由玩家角色的蓝图通过射线检测到门后调用其Interact节点来完成。迁移的优势性能插值计算在C的Tick中完成比蓝图时间轴更高效。可维护性核心逻辑集中在C中清晰且易于调试。蓝图只负责表现分工明确。可复用性ADoorActor类可以被多个不同的门蓝图复用每个蓝图可以有不同的网格体、音效和特效但共享同一套开关逻辑。6. 创建自定义蓝图节点与宏库有时候你希望将一些常用的、复杂的C功能封装成一个单独的、好用的蓝图节点而不是挂在某个特定的Actor或Component上。这时就需要创建Blueprint Function Library或宏。6.1 创建蓝图函数库 (Blueprint Function Library)蓝图函数库是一个静态函数Static Function的集合可以在任何蓝图中直接调用无需类的实例。创建类新建C类父类选择BlueprintFunctionLibrary命名为MyGameplayStatics遵循引擎惯例类似UKismetSystemLibrary。声明静态函数// MyGameplayStatics.h UCLASS() class YOURPROJECT_API UMyGameplayStatics : public UBlueprintFunctionLibrary { GENERATED_BODY() public: // 一个实用的函数计算伤害考虑防御和随机暴击 UFUNCTION(BlueprintPure, CategoryGameplay|Combat, meta(DisplayNameCalculate Final Damage)) static float CalculateFinalDamage(float BaseDamage, float TargetDefense, float CriticalChance, float CriticalMultiplier); // 一个影响世界的函数在位置播放一个自定义的相机抖动 UFUNCTION(BlueprintCallable, CategoryGameplay|Camera, meta(WorldContextWorldContextObject)) static void PlayCustomCameraShake(UObject* WorldContextObject, TSubclassOfUCameraShakeBase ShakeClass, FVector Epicenter, float InnerRadius, float OuterRadius, float Falloff); };BlueprintPure因为计算伤害不改变状态。meta(DisplayName...)可以自定义节点在蓝图中的显示名称。meta(WorldContextWorldContextObject)这是一个非常实用的元数据。它告诉引擎这个函数需要一个世界上下文通常是一个World对象来执行某些操作如生成Actor、播放震动。UHT会自动为该函数添加一个名为WorldContextObject的输入引脚你只需要在蓝图中将任何一个Object通常是self连给它库函数内部就能通过GEngine-GetWorldFromContextObject获取到正确的World。实现函数// MyGameplayStatics.cpp #include MyGameplayStatics.h #include Engine/Engine.h #include Camera/CameraShakeBase.h float UMyGameplayStatics::CalculateFinalDamage(float BaseDamage, float TargetDefense, float CriticalChance, float CriticalMultiplier) { float DefenseFactor FMath::Clamp(1.0f - TargetDefense / 100.0f, 0.1f, 1.0f); float Damage BaseDamage * DefenseFactor; if (FMath::FRand() CriticalChance) { Damage * CriticalMultiplier; // 可以在这里触发一个蓝图可实现的事件来通知暴击但静态函数不能直接调用实例事件。 // 通常暴击反馈屏幕特效、音效由调用者处理。 } return Damage; } void UMyGameplayStatics::PlayCustomCameraShake(UObject* WorldContextObject, TSubclassOfUCameraShakeBase ShakeClass, FVector Epicenter, float InnerRadius, float OuterRadius, float Falloff) { UWorld* World GEngine-GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull); if (World ShakeClass) { // 这里简化了实际需要遍历玩家控制器根据距离计算震动强度 // 使用 UGameplayStatics::PlayWorldCameraShake 可能是更好的选择这里仅为示例 APlayerController* PC World-GetFirstPlayerController(); if (PC) { PC-ClientStartCameraShake(ShakeClass); } } }现在在任何蓝图中你都可以右键搜索“Calculate Final Damage”或“Play Custom Camera Shake”直接使用这些封装好的高级节点。6.2 关于蓝图宏与自定义事件蓝图宏Macro本质上是蓝图的“代码片段”复用。它可以将一组常用的节点序列打包有输入/输出。它的优势是复用和组织蓝图图表但执行时依然是在蓝图VM中解释执行没有性能提升。对于从C迁移逻辑来说我们更关注的是将计算密集型或通用逻辑移到C函数库中。自定义事件Custom Event在C中对应BlueprintImplementableEvent或BlueprintNativeEvent。它用于C代码“调用”蓝图逻辑是C驱动蓝图的逆向通道。实操心得当你发现多个蓝图中重复着同样复杂的节点序列比如一套复杂的向量计算或字符串处理首先考虑能否用C函数库实现。如果能就创建函数库。如果这个序列严重依赖蓝图特有的流程控制如延迟、时间轴、分支序列或者只是临时性的组织需求那么使用蓝图宏更合适。7. 调试与性能优化要点用C扩展蓝图后调试方式和性能考量也有所不同。7.1 C调试技巧使用UE_LOG这是最基础的调试手段。在代码中插入UE_LOG(LogTemp, Warning, TEXT(Variable: %d), MyInt);。输出会显示在编辑器的“输出日志”窗口和IDE的调试输出中。可以为不同的系统定义不同的Log Category方便过滤。在Visual Studio中调试在VS中打开你的项目解决方案.sln。确保编译配置是DebugGame Editor或Development Editor。按F5启动调试UE编辑器会自动启动。在C代码中设置断点当游戏运行到断点处时VS会中断你可以查看调用堆栈、变量值、内存等所有信息。这是定位复杂逻辑问题的终极武器。使用ensure和checkcheck(MyPtr ! nullptr);在开发版本中如果条件为假会触发断言中断程序并弹出错误对话框指出文件和行号。用于捕捉绝对不应该发生的错误。ensure(MyPtr ! nullptr);比check温和。条件为假时会在调用堆栈和输出日志中记录一次错误但程序可能继续运行取决于后续代码是否依赖这个条件。用于捕捉可能发生但不一定导致崩溃的错误。ensureMsgf(MyPtr ! nullptr, TEXT(MyPtr is null in function %s), __FUNCTION__);可以附带自定义错误信息。7.2 性能考量Tick的代价C的Tick函数虽然比蓝图Tick高效但每帧执行仍然有成本。对于大量存在的Actor如子弹、粒子效果应尽量避免使用Tick。可以使用定时器FTimerHandle或事件驱动来更新状态。蓝图与C通信开销每一次从蓝图调用C的BlueprintCallable函数或从C调用蓝图实现的事件都有一定的跨边界调用开销。虽然单次开销很小但在每帧、每个对象上都进行大量调用累积起来也会影响性能。应对策略将高频调用的逻辑完全放在C端或者将一帧内多次调用的数据打包通过一次调用传递例如使用结构体FMyData而不是多个单独的参数。合理使用BlueprintPure标记为BlueprintPure的函数在蓝图中如果其输入引脚没有变化在同一帧内可能会被缓存结果。但为了安全起见对于计算量大的纯函数也应考虑在C端缓存结果避免每帧重复计算。内存与UObject在C中NewObject或SpawnActor创建UObject和AActor时要记得管理生命周期。对于不再需要的对象调用Destroy()或让其自动垃圾回收如果没有被引用。避免内存泄漏。8. 进阶路径深入引擎模块与插件开发当你熟练掌握了用C为单个项目自定义蓝图后你可能会想将这些功能打包用于其他项目或者贡献给社区。这时就需要了解模块和插件。游戏模块Game Module你的主游戏项目本身就是一个模块在.Build.cs文件中定义。你可以将一些通用的C类如你的MyGameplayStatics库、通用的DoorActor基类放在这里。插件Plugin插件是独立于游戏项目的、可移植的功能包。它可以包含自己的内容蓝图、材质、C代码和模块。创建插件编辑器菜单工具 - 新建插件可以将你的高级功能比如一套完整的对话系统、高级库存管理封装起来方便在其他项目中一键启用。插件有自己的.uplugin描述文件和.Build.cs文件。插件中的C类同样可以用UFUNCTION和UPROPERTY暴露给蓝图。将通用性强的功能迁移到插件中是代码复用的高级形式也是迈向引擎贡献者或工具开发者的重要一步。从蓝图到C不是要抛弃蓝图而是为了与它更紧密、更高效地合作。C为你提供了深入引擎底层、构建健壮系统、优化性能的能力而蓝图则保留了快速迭代、搭建关卡逻辑和视觉表现的灵活性。掌握两者结合的艺术你就能在虚幻引擎5的开发中游刃有余从一名脚本编写者成长为真正的游戏工程师。