第15章 C++与蓝图的交互与扩展
C++与蓝图的交互是UE5开发中的重要部分,它结合了C++的高性能和蓝图的易用性。本章将深入探讨C++与蓝图的交互技术,包括C++类的蓝图可访问性、蓝图函数库、蓝图可调用函数、C++扩展蓝图等高级功能。
15.1 C++与蓝图的关系
C++与蓝图是UE5开发中的两种主要编程语言,它们各有优势,相互补充。
15.1.1 C++的优势
- 高性能:C++代码直接编译为机器码,执行效率高
- 底层访问:可以直接访问UE5引擎的底层API
- 复杂逻辑:适合实现复杂的游戏逻辑和算法
- 内存管理:更精确的内存控制
- 跨平台:支持多种平台编译
15.1.2 蓝图的优势
- 可视化编程:直观的节点式编程界面
- 快速迭代:无需编译,即时预览效果
- 易用性:适合设计师和非程序员使用
- 快速原型:快速创建游戏原型
- 热重载:支持运行时修改和调试
15.1.3 最佳实践
- C++与蓝图结合使用:将高性能和底层逻辑用C++实现,将游戏逻辑和快速迭代部分用蓝图实现
- 分层设计:使用C++实现核心系统,使用蓝图实现游戏内容
- 接口设计:设计清晰的C++接口供蓝图调用
- 性能优化:将性能瓶颈部分用C++重写
15.2 创建蓝图可访问的C++类
15.2.1 定义C++类
- 创建新的C++类
- 打开UE5编辑器→文件→新建C++类
- 选择父类(如Actor、Character、Pawn等)
- 命名并保存
-
自动生成.h和.cpp文件
-
设置类的蓝图可访问性 ```cpp // 头文件 (.h) #pragma once
#include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "MyActor.generated.h" // 必须包含此宏
UCLASS(Blueprintable, BlueprintType) // 蓝图可创建和使用 class MYPROJECT_API AMyActor : public AActor { GENERATED_BODY()
public: // 构造函数 AMyActor(); }; ```
- 类的宏定义
- UCLASS():定义UE5类
- Blueprintable:蓝图可以基于此类创建
- BlueprintType:蓝图可以使用此类作为变量类型
- Blueprintable, BlueprintType:蓝图既可以基于此类创建,也可以使用此类作为变量类型
15.2.2 定义蓝图可访问的属性
- 属性宏定义 ```cpp UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "MyActor") float Health;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category = "MyActor") float MaxHealth;
UPROPERTY(BlueprintReadWrite, EditDefaultsOnly, Category = "MyActor") UStaticMeshComponent* MeshComponent; ```
- 属性参数
- BlueprintReadWrite:蓝图可以读写该属性
- BlueprintReadOnly:蓝图只能读取该属性
- EditAnywhere:在编辑器的任何地方都可以编辑
- EditDefaultsOnly:只能在蓝图默认设置中编辑
- EditInstanceOnly:只能在实例中编辑
- VisibleAnywhere:在编辑器的任何地方都可见
- VisibleDefaultsOnly:只能在蓝图默认设置中可见
- VisibleInstanceOnly:只能在实例中可见
- Category:属性分类
15.2.3 定义蓝图可访问的函数
- 函数宏定义 ```cpp UFUNCTION(BlueprintCallable, Category = "MyActor") void SetHealth(float NewHealth);
UFUNCTION(BlueprintPure, Category = "MyActor") float GetHealthPercentage() const;
UFUNCTION(BlueprintImplementableEvent, Category = "MyActor") void OnHealthChanged(float OldHealth, float NewHealth);
UFUNCTION(BlueprintNativeEvent, Category = "MyActor") void TakeDamage(float Damage); ```
- 函数参数
- BlueprintCallable:蓝图可以调用此函数
- BlueprintPure:纯函数,不修改对象状态,无输出执行引脚
- BlueprintImplementableEvent:蓝图必须实现此事件,C++中无实现
- BlueprintNativeEvent:C++中有默认实现,蓝图可以覆盖
-
Category:函数分类
-
实现BlueprintNativeEvent ```cpp // 头文件 (.h) UFUNCTION(BlueprintNativeEvent, Category = "MyActor") void TakeDamage(float Damage);
// 源文件 (.cpp) void AMyActor::TakeDamage_Implementation(float Damage) { Health = FMath::Max(0.0f, Health - Damage); OnHealthChanged(Health + Damage, Health); } ```
15.3 创建蓝图函数库
蓝图函数库是一组静态函数的集合,供蓝图调用,无需创建实例。
15.3.1 创建蓝图函数库
- 定义蓝图函数库类 ```cpp // 头文件 (.h) #pragma once
#include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "MyBlueprintFunctionLibrary.generated.h"
UCLASS() class MYPROJECT_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary { GENERATED_BODY() public: UFUNCTION(BlueprintCallable, Category = "MyBlueprintFunctionLibrary") static FVector CalculateDirection(FVector StartLocation, FVector EndLocation);
UFUNCTION(BlueprintPure, Category = "MyBlueprintFunctionLibrary")
static float CalculateDistance(FVector StartLocation, FVector EndLocation);
}; ```
- 实现蓝图函数库函数 ```cpp // 源文件 (.cpp) #include "MyBlueprintFunctionLibrary.h"
FVector UMyBlueprintFunctionLibrary::CalculateDirection(FVector StartLocation, FVector EndLocation) { return (EndLocation - StartLocation).GetSafeNormal(); }
float UMyBlueprintFunctionLibrary::CalculateDistance(FVector StartLocation, FVector EndLocation) { return FVector::Dist(StartLocation, EndLocation); } ```
15.3.2 使用蓝图函数库
- 在蓝图中调用
- 打开蓝图编辑器
- 右键点击工作区→函数库→MyBlueprintFunctionLibrary
- 选择要调用的函数
-
连接节点使用
-
最佳实践
- 将通用功能放在蓝图函数库中
- 函数命名清晰,参数含义明确
- 避免函数过于复杂,保持单一职责
15.4 C++调用蓝图函数
C++可以调用蓝图中实现的函数和事件。
15.4.1 调用BlueprintImplementableEvent
-
定义事件
cpp // 头文件 (.h) UFUNCTION(BlueprintImplementableEvent, Category = "MyActor") void OnActorSpawned(); -
在C++中调用 ```cpp // 源文件 (.cpp) void AMyActor::BeginPlay() { Super::BeginPlay();
// 调用蓝图实现的事件 OnActorSpawned(); } ```
-
在蓝图中实现
- 打开基于AMyActor的蓝图
- 找到"OnActorSpawned"事件
- 实现事件逻辑
15.4.2 调用BlueprintNativeEvent
-
定义事件
cpp // 头文件 (.h) UFUNCTION(BlueprintNativeEvent, Category = "MyActor") void OnActorDamaged(float Damage); -
C++中实现默认逻辑
cpp // 源文件 (.cpp) void AMyActor::OnActorDamaged_Implementation(float Damage) { // 默认实现 UE_LOG(LogTemp, Warning, TEXT("Actor damaged: %f"), Damage); } -
在C++中调用
cpp // 源文件 (.cpp) void AMyActor::TakeDamage(float Damage) { // 调用事件(蓝图可能覆盖) OnActorDamaged(Damage); } -
在蓝图中覆盖
- 打开基于AMyActor的蓝图
- 找到"OnActorDamaged"事件
- 右键点击→覆盖
- 实现自定义逻辑
15.4.3 调用蓝图接口
- 定义接口 ```cpp // 头文件 (.h) #pragma once
#include "CoreMinimal.h" #include "UObject/Interface.h" #include "MyInterface.generated.h"
UINTERFACE(Blueprintable) // 蓝图可实现 class UMyInterface : public UInterface { GENERATED_BODY() };
class IMYPROJECT_API IMyInterface { GENERATED_BODY() public: UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "MyInterface") void Interact(); }; ```
- 在C++类中实现接口 ```cpp // 头文件 (.h) UCLASS(Blueprintable) class MYPROJECT_API AMyInteractableActor : public AActor, public IMyInterface { GENERATED_BODY() public: // 实现接口 virtual void Interact_Implementation() override; };
// 源文件 (.cpp) void AMyInteractableActor::Interact_Implementation() { UE_LOG(LogTemp, Warning, TEXT("Interact with actor: %s"), *GetName()); } ```
- 在蓝图中实现接口
- 打开蓝图→类设置→接口→添加接口
- 选择"MyInterface"
-
实现"Interact"事件
-
在C++中调用接口
cpp // 源文件 (.cpp) void AMyPlayerCharacter::InteractWithActor(AActor* Actor) { if (Actor && Actor->Implements<UMyInterface>()) { IMyInterface::Execute_Interact(Actor); } }
15.4.4 使用动态委托
动态委托可以在C++中绑定蓝图函数。
- 定义动态委托 ```cpp // 头文件 (.h) DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnHealthChangedDelegate, float, NewHealth);
UCLASS(Blueprintable) class MYPROJECT_API AMyActor : public AActor { GENERATED_BODY() public: UPROPERTY(BlueprintAssignable, Category = "MyActor") FOnHealthChangedDelegate OnHealthChanged;
UFUNCTION(BlueprintCallable, Category = "MyActor")
void SetHealth(float NewHealth);
}; ```
-
实现动态委托
cpp // 源文件 (.cpp) void AMyActor::SetHealth(float NewHealth) { Health = NewHealth; OnHealthChanged.Broadcast(Health); // 广播委托 } -
在蓝图中绑定委托
- 打开基于AMyActor的蓝图
- 在事件图表中,从MyActor节点拖拽出"OnHealthChanged"
- 选择"Add Dynamic"
- 连接到要执行的函数
15.5 C++扩展蓝图
C++可以扩展蓝图的功能,包括添加新的节点、修改现有节点的行为等。
15.5.1 创建自定义蓝图节点
- 定义自定义节点 ```cpp // 头文件 (.h) #pragma once
#include "CoreMinimal.h" #include "Kismet/BlueprintFunctionLibrary.h" #include "MyCustomNodes.generated.h"
UCLASS() class MYPROJECT_API UMyCustomNodes : public UBlueprintFunctionLibrary { GENERATED_BODY() public: UFUNCTION(BlueprintCallable, Category = "MyCustomNodes", meta = (DisplayName = "Custom Move Actor", Keywords = "move actor position", WorldContext = "WorldContextObject")) static void CustomMoveActor(UObject WorldContextObject, AActor Actor, FVector NewLocation, float Duration); }; ```
- 实现自定义节点 ```cpp // 源文件 (.cpp) #include "MyCustomNodes.h" #include "Components/TimelineComponent.h"
void UMyCustomNodes::CustomMoveActor(UObject WorldContextObject, AActor Actor, FVector NewLocation, float Duration) { if (!Actor || !WorldContextObject) return;
UWorld* World = WorldContextObject->GetWorld();
if (!World) return;
// 创建时间线组件
UTimelineComponent* TimelineComponent = NewObject<UTimelineComponent>(Actor);
TimelineComponent->RegisterComponent();
// 设置时间线参数
FVector StartLocation = Actor->GetActorLocation();
FOnTimelineVector TimelineUpdate;
TimelineUpdate.BindUObject(Actor, &AActor::SetActorLocation);
// 创建曲线
FRuntimeFloatCurve Curve;
Curve.AddKey(0.0f, 0.0f);
Curve.AddKey(Duration, 1.0f);
// 设置时间线
TimelineComponent->AddInterpVector(FVectorCurve(Curve), TimelineUpdate, FName("Move"));
TimelineComponent->SetLooping(false);
TimelineComponent->SetPlayRate(1.0f / Duration);
// 播放时间线
TimelineComponent->PlayFromStart();
} ```
- 在蓝图中使用自定义节点
- 打开蓝图编辑器
- 右键点击工作区→MyCustomNodes→Custom Move Actor
- 设置参数并连接节点
15.5.2 使用蓝图编译器扩展
蓝图编译器扩展可以修改蓝图的编译过程,添加自定义检查和优化。
- 定义蓝图编译器扩展 ```cpp // 头文件 (.h) #pragma once
#include "CoreMinimal.h" #include "KismetCompilerModule.h"
class FMyBlueprintCompilerExtension : public IBlueprintCompilerExtension { public: virtual void ProcessBlueprint(UBlueprint Blueprint, const FKismetCompilerOptions& CompileOptions, FCompilerResultsLog& Results) override; virtual void PostCompile(UBlueprint Blueprint, const FKismetCompilerOptions& CompileOptions, FCompilerResultsLog& Results) override; }; ```
- 实现蓝图编译器扩展 ```cpp // 源文件 (.cpp) void FMyBlueprintCompilerExtension::ProcessBlueprint(UBlueprint Blueprint, const FKismetCompilerOptions& CompileOptions, FCompilerResultsLog& Results) { // 在编译过程中处理蓝图 Results.Note(FString::Printf(TEXT("Processing blueprint: %s"), *Blueprint->GetName())); }
void FMyBlueprintCompilerExtension::PostCompile(UBlueprint Blueprint, const FKismetCompilerOptions& CompileOptions, FCompilerResultsLog& Results) { // 在编译完成后处理蓝图 Results.Note(FString::Printf(TEXT("Post-processing blueprint: %s"), *Blueprint->GetName())); } ```
- 注册蓝图编译器扩展
cpp // 源文件 (.cpp) class FMyProjectModule : public IModuleInterface { public: virtual void StartupModule() override { // 注册蓝图编译器扩展 IBlueprintCompilerModule& CompilerModule = FModuleManager::LoadModuleChecked<IBlueprintCompilerModule>("KismetCompiler"); CompilerModule.GetCompilationManager().RegisterCompilerExtension(MakeShareable(new FMyBlueprintCompilerExtension())); } };
15.6 蓝图原生化 (Blueprint Nativization)
蓝图原生化将蓝图代码转换为C++代码,然后编译为机器码,提高性能。
15.6.1 启用蓝图原生化
- 项目设置
- 打开项目设置→引擎→蓝图→蓝图原生化
- 设置原生化模式(禁用、仅编辑器、仅游戏、编辑器和游戏)
-
设置原生化类型(运行时、静态)
-
蓝图设置
- 打开蓝图→类设置→原生化→启用原生化
- 设置原生化选项
15.6.2 蓝图原生化的优势
- 性能提升:蓝图代码转换为C++代码,执行效率提高
- 内存减少:减少蓝图虚拟机的内存占用
- 启动速度:提高游戏启动速度
- 保护代码:隐藏蓝图实现细节
15.6.3 注意事项
- 编译时间:增加编译时间
- 调试难度:调试原生化后的代码较困难
- 兼容性:某些蓝图功能可能不支持原生化
- 迭代速度:失去蓝图的快速迭代优势
15.7 调试C++与蓝图交互
15.7.1 调试C++代码
- 设置断点:在C++代码中设置断点
- 启动调试:点击UE5编辑器的调试按钮
- 单步执行:使用调试工具单步执行代码
- 查看变量:在调试窗口查看变量值
- 调用堆栈:查看函数调用堆栈
15.7.2 调试蓝图代码
- 设置断点:在蓝图节点上右键点击→添加断点
- 启动调试:点击UE5编辑器的调试按钮
- 单步执行:使用调试工具单步执行蓝图
- 查看变量:在调试窗口查看变量值
- 调用堆栈:查看蓝图调用堆栈
15.7.3 调试C++与蓝图交互
- C++调用蓝图:在C++调用蓝图的代码处设置断点,然后在蓝图中设置断点
- 蓝图调用C++:在蓝图调用C++的节点处设置断点,然后在C++代码中设置断点
- 查看日志:使用UE_LOG输出调试信息
- 使用DrawDebug函数:在3D视图中绘制调试信息
15.8 性能优化
15.8.1 C++性能优化
- 减少蓝图调用:减少C++与蓝图之间的函数调用次数
- 批量操作:使用批量操作代替单个操作
- 避免字符串操作:减少字符串的创建和拼接
- 使用值类型:优先使用值类型(如FVector、FRotator)而非引用类型
- 减少动态分配:避免在热路径中使用new/delete
15.8.2 蓝图性能优化
- 使用蓝图原生化:将频繁执行的蓝图原生化为C++代码
- 减少蓝图复杂度:简化蓝图逻辑,减少节点数量
- 避免在Tick事件中执行繁重计算:使用定时器或事件驱动
- 使用纯函数:优先使用BlueprintPure函数
- 减少蓝图更新频率:降低蓝图的Tick频率
15.8.3 内存优化
- 减少对象创建:复用对象,避免频繁创建和销毁
- 使用对象池:管理常用对象的创建和销毁
- 优化数据结构:选择合适的数据结构
- 减少引用:避免循环引用和不必要的引用
15.9 高级话题
15.9.1 热重载
热重载允许在运行时修改C++代码并立即生效,无需重启游戏。
- 启用热重载
- 打开项目设置→引擎→热重载→启用热重载
-
设置热重载选项
-
使用热重载
- 修改C++代码
- 编译代码(Ctrl+Shift+B)
- 游戏会自动重载修改后的代码
15.9.2 反射系统
UE5的反射系统允许在运行时访问类型信息和元数据。
- 反射宏
- UCLASS:类反射
- UPROPERTY:属性反射
- UFUNCTION:函数反射
- UENUM:枚举反射
-
USTRUCT:结构体反射
-
使用反射系统 ```cpp // 获取类信息 UClass* ActorClass = AActor::StaticClass();
// 获取属性信息
for (TFieldIterator
// 获取函数信息
for (TFieldIterator
15.9.3 元数据
元数据提供了关于类、属性和函数的额外信息,用于UE5编辑器和蓝图系统。
- 常用元数据
- DisplayName:在编辑器中显示的名称
- Keywords:搜索关键词
- Category:分类
- Tooltip:提示信息
- WorldContext:世界上下文
-
BlueprintInternalUseOnly:仅蓝图内部使用
-
使用元数据 ```cpp UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "MyActor", meta = (DisplayName = "Actor Health", Tooltip = "Current health of the actor", ClampMin = "0.0", ClampMax = "100.0")) float Health;
UFUNCTION(BlueprintCallable, Category = "MyActor", meta = (DisplayName = "Heal Actor", Keywords = "heal health restore", Tooltip = "Restores health to the actor")) void Heal(float Amount); ```
15.10 案例分析
15.10.1 案例一:角色控制系统
- 需求分析
- 实现角色的移动、跳跃、攻击等功能
- 需要高性能和精确控制
-
设计师需要调整参数和快速迭代
-
实现方案
- 使用C++实现核心移动系统和物理碰撞
- 使用蓝图实现角色的具体行为和参数调整
-
设计清晰的C++接口供蓝图调用
-
代码示例 ```cpp // C++核心移动系统 UCLASS(Blueprintable) class MYPROJECT_API AMyCharacter : public ACharacter { GENERATED_BODY() public: UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Character", meta = (ClampMin = "100.0", ClampMax = "500.0")) float MaxHealth;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Character", meta = (ClampMin = "0.0", ClampMax = "100.0")) float AttackDamage;
UFUNCTION(BlueprintCallable, Category = "Character") void Attack();
UFUNCTION(BlueprintImplementableEvent, Category = "Character") void OnAttackHit(AActor* HitActor); };
// 实现攻击功能 void AMyCharacter::Attack() { // 执行攻击动画 PlayAnimMontage(AttackAnimMontage);
// 检测碰撞
FHitResult HitResult;
FVector StartLocation = GetActorLocation() + GetActorForwardVector() * 50.0f;
FVector EndLocation = StartLocation + GetActorForwardVector() * 200.0f;
FCollisionQueryParams Params;
Params.AddIgnoredActor(this);
if (GetWorld()->LineTraceSingleByChannel(HitResult, StartLocation, EndLocation, ECC_Pawn, Params))
{
AActor* HitActor = HitResult.GetActor();
if (HitActor)
{
// 调用蓝图事件
OnAttackHit(HitActor);
// 应用伤害
if (IMyDamageInterface* DamageInterface = Cast<IMyDamageInterface>(HitActor))
{
DamageInterface->TakeDamage_Implementation(AttackDamage);
}
}
}
} ```
- 蓝图扩展
- 基于C++类创建蓝图
- 调整MaxHealth和AttackDamage参数
- 实现OnAttackHit事件,添加特效和音效
- 创建不同角色的蓝图变体
15.10.2 案例二:物品系统
- 需求分析
- 实现物品的创建、使用、管理等功能
- 支持多种物品类型(武器、道具、消耗品等)
-
需要灵活的物品配置和扩展
-
实现方案
- 使用C++实现物品的核心系统和接口
- 使用蓝图实现具体的物品类型和效果
-
使用数据表格存储物品属性
-
代码示例 ```cpp // 物品数据结构 USTRUCT(BlueprintType) struct FItemData { GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item") FName ItemID;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item") FString ItemName;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item") FString ItemDescription;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item") UTexture2D* Icon;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Item") int32 MaxStackSize; };
// 物品基类 UCLASS(Blueprintable, Abstract) class MYPROJECT_API AMyItem : public AActor { GENERATED_BODY() public: UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Item") FItemData ItemData;
UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Item")
void Use(AActor* User);
};
// 物品系统
UCLASS(Blueprintable)
class MYPROJECT_API UMyInventorySystem : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Inventory")
TArray
UFUNCTION(BlueprintCallable, Category = "Inventory")
bool AddItem(FItemData Item);
UFUNCTION(BlueprintCallable, Category = "Inventory")
bool RemoveItem(FName ItemID);
UFUNCTION(BlueprintPure, Category = "Inventory")
int32 GetItemCount(FName ItemID) const;
}; ```
- 蓝图扩展
- 创建具体的物品蓝图(如HealthPotion、Sword等)
- 实现Use事件,添加物品使用效果
- 使用数据表格配置物品属性
- 创建物品的拾取和使用逻辑
思考与练习
- C++与蓝图的各自优势是什么?如何结合使用以提高开发效率?
- 如何创建蓝图可访问的C++类?包括设置类的蓝图可访问性、定义蓝图可访问的属性和函数。
- 什么是蓝图函数库?如何创建和使用蓝图函数库?
- 如何在C++中调用蓝图函数和事件?包括BlueprintImplementableEvent、BlueprintNativeEvent和蓝图接口。
- 如何在蓝图中调用C++函数和事件?包括普通函数、蓝图函数库和接口函数。
- 蓝图原生化的优势和注意事项是什么?如何启用蓝图原生化?
- 如何调试C++与蓝图的交互?包括调试工具和方法。
- 性能优化的方法有哪些?包括C++性能优化和蓝图性能优化。
- 反射系统和元数据的作用是什么?如何使用它们?
- 分析一个C++与蓝图结合的案例,包括需求分析、实现方案和代码示例。