跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
C++AI算法

UE C++行为树实现AI敌人逻辑与第三人称角色源码

UE C++中使用行为树实现 AI 敌人的巡逻、感知玩家及射击逻辑。通过黑板存储玩家位置与可见性状态,利用导航系统生成随机巡逻点,并集成第三人称角色类处理移动、武器切换、瞬移技能及伤害判定。包含完整的头文件与源文件代码示例。

蜜桃汽水发布于 2026/3/16更新于 2026/5/114 浏览
UE C++行为树实现AI敌人逻辑与第三人称角色源码

一、获取玩家的当前位置

代码实现功能:在 AI 敌人运行过程中,持续不断地获取玩家的当前位置,并将这个坐标更新到 AI 敌人的黑板中。

BTService_PlayerLocation.h

#pragma once #include "CoreMinimal.h" #include "BehaviorTree/Services/BTService_BlackboardBase.h" #include "BTService_PlayerLocation.generated.h"
UCLASS()
class CRYPTRAIDER_API UBTService_PlayerLocation : public UBTService_BlackboardBase {
 GENERATED_BODY()
public:
 UBTService_PlayerLocation();
protected:
 virtual void TickNode(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory,float DeltaSeconds)override;
}; 

BTService_PlayerLocation.cpp

#include "BTService_PlayerLocation.h" #include "BehaviorTree/BlackboardComponent.h" #include "Kismet/GameplayStatics.h" #include "GameFramework/Pawn.h"
UBTService_PlayerLocation::UBTService_PlayerLocation() {
 NodeName = "Update Player Location";
}
void UBTService_PlayerLocation::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) {
 Super::TickNode(OwnerComp,NodeMemory,DeltaSeconds);
 APawn* PlayerPawn = UGameplayStatics::((),);
  (PlayerPawn == ) {
 ;
 }
 OwnerComp.()->((),PlayerPawn->());
}
GetPlayerPawn
GetWorld
0
if
nullptr
return
GetBlackboardComponent
SetValueAsVector
GetSelectedBlackboardKey
GetActorLocation

GetSelectedBlackboardKey():获取在 UE 编辑器中为这个节点指定的那个黑板 Key;

PlayerPawn->GetActorLocation():获取玩家当前的 3D 坐标。

二、检测是否'看见'玩家

代码实现功能:如果 AI 敌人看见了,就更新黑板记录玩家对象;如果没看见(被遮挡或超出视野),就清除黑板上的记录。

BTService_PlayerLocationIfSeen.h

#pragma once #include "CoreMinimal.h" #include "BehaviorTree/Services/BTService_BlackboardBase.h" #include "BTService_PlayerLocationIfSeen.generated.h"
UCLASS()
class CRYPTRAIDER_API UBTService_PlayerLocationIfSeen : public UBTService_BlackboardBase {
 GENERATED_BODY()
public:
 UBTService_PlayerLocationIfSeen();
protected:
 virtual void TickNode(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory,float DeltaSeconds)override;
}; 

BTService_PlayerLocationIfSeen.cpp

#include "BTService_PlayerLocationIfSeen.h" #include "BehaviorTree/BlackboardComponent.h" #include "Kismet/GameplayStatics.h" #include "GameFramework/Pawn.h" #include "AIController.h"
UBTService_PlayerLocationIfSeen::UBTService_PlayerLocationIfSeen() {
 NodeName = "Update Player Location";
}
void UBTService_PlayerLocationIfSeen::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) {
 Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
 APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
 if (PlayerPawn == nullptr) {
 return;
 }
 if (OwnerComp.GetAIOwner() == nullptr) {
 return;
 }
 if (OwnerComp.GetAIOwner()->LineOfSightTo(PlayerPawn)) {
 OwnerComp.GetBlackboardComponent()->SetValueAsObject(GetSelectedBlackboardKey(), PlayerPawn);
 } else {
 OwnerComp.GetBlackboardComponent()->ClearValue(GetSelectedBlackboardKey());
 }
}

SetValueAsObject:这里存的是玩家对象本身,而不是单纯的坐标;

LineOfSightTo(PlayerPawn):AAIController 自带,从 AI 敌人的眼睛位置向目标(玩家)发射射线;

ClearValue:LineOfSightTo 返回 false,黑板上的 TargetActor 就会被清空(设为 null)。

三、敌人简单巡逻逻辑

代码实现功能:让 AI 在一定范围内围绕'出生点',寻找的下一个目标点,并进行移动

BTTask_FindRandomLocation.h

#pragma once #include "CoreMinimal.h" #include "BehaviorTree/Tasks/BTTask_BlackboardBase.h" #include "BTTask_FindRandomLocation.generated.h"
UCLASS()
class CRYPTRAIDER_API UBTTask_FindRandomLocation : public UBTTask_BlackboardBase {
 GENERATED_BODY()
public:
 UBTTask_FindRandomLocation();
private:
 virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
 UPROPERTY(EditAnywhere, Category = "AI") float PatrolRadius = 1000.f;
};

BTTask_FindRandomLocation.cpp

#include "BTTask_FindRandomLocation.h" #include "BehaviorTree/BlackboardComponent.h" #include "AIController.h" #include "NavigationSystem.h"
UBTTask_FindRandomLocation::UBTTask_FindRandomLocation() {
 NodeName = "Find Random Location";
}
EBTNodeResult::Type UBTTask_FindRandomLocation::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) {
 Super::ExecuteTask(OwnerComp, NodeMemory);
 if (OwnerComp.GetAIOwner() == nullptr) return EBTNodeResult::Failed;
 UNavigationSystemV1* NavSystem = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
 if (NavSystem == nullptr) return EBTNodeResult::Failed;
 FVector Origin = OwnerComp.GetBlackboardComponent()->GetValueAsVector(TEXT("StartLocation"));
 FNavLocation ResultLocation;
 if (NavSystem->GetRandomReachablePointInRadius(Origin, PatrolRadius, ResultLocation)) {
 OwnerComp.GetBlackboardComponent()->SetValueAsVector(GetSelectedBlackboardKey(), ResultLocation.Location);
 return EBTNodeResult::Succeeded;
 }
 return EBTNodeResult::Failed;
}

NavSystem->GetRandomReachablePointInRadius(Origin, PatrolRadius, ResultLocation):在半径内寻找随机导航点;

OwnerComp.GetBlackboardComponent()->SetValueAsVector(GetSelectedBlackboardKey(), ResultLocation.Location):如果找到了,将结果写入我们选中的 Blackboard Key,并进行移动

四、命令 AI 执行'射击'

代码实现功能:连接 AI 的'大脑'(行为树)与'身体'(角色类),命令 AI 执行'射击'这个动作

BTTask_Shoot.h

#pragma once #include "CoreMinimal.h" #include "BehaviorTree/BTTaskNode.h" #include "BTTask_Shoot.generated.h"
UCLASS()
class CRYPTRAIDER_API UBTTask_Shoot : public UBTTaskNode {
 GENERATED_BODY()
public:
 UBTTask_Shoot();
private:
 virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)override;
}; 

BTTask_Shoot.cpp

#include "BTTask_Shoot.h" #include "AIController.h" #include "BP_MyCharacter.h"
UBTTask_Shoot::UBTTask_Shoot() {
 NodeName = "Shoot";
}
EBTNodeResult::Type UBTTask_Shoot::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) {
 Super::ExecuteTask(OwnerComp, NodeMemory);
 if (OwnerComp.GetAIOwner() == nullptr) {
 return EBTNodeResult::Failed;
 }
 ABP_MyCharacter* Character = Cast<ABP_MyCharacter>(OwnerComp.GetAIOwner()->GetPawn());
 if (Character == nullptr) {
 return EBTNodeResult::Failed;
 }
 Character->Shoot();
 return EBTNodeResult::Succeeded;
}

return EBTNodeResult::Succeeded:行为树收到成功信号后,会继续执行后续的逻辑;

OwnerComp.GetAIOwner()->GetPawn():通过 AI 控制器获取它当前附身的 Pawn;

Character->Shoot():调用写好的射击逻辑。

五、第三人称射击游戏角色类

代码实现功能:集成战斗、生存与高阶机动逻辑,通过动态生成与销毁 Actor 的方式构建了支持可扩展的多武器循环切换系统,利用重写的伤害处理函数与 GameMode 深度协作以管理角色的生命周期与死亡判定,同时通过多重射线检测技术实现了一个具备防穿墙和地形自适应能力的'智能瞬移'技能,从而在保证游戏物理稳定性的前提下赋予了角色灵活的战术位移手段。

BP_MyCharacter.h

#pragma once #include "CoreMinimal.h" #include "GameFramework/Character.h" #include "BP_MyCharacter.generated.h"
class AGun;
UCLASS()
class CRYPTRAIDER_API ABP_MyCharacter : public ACharacter {
 GENERATED_BODY()
public:
 // Sets default values for this character's properties
 ABP_MyCharacter();
protected:
 // Called when the game starts or when spawned
 virtual void BeginPlay() override;
public:
 void Dash();
 UFUNCTION(BlueprintPure) bool IsDead() const;
 UFUNCTION(BlueprintPure) float GetHealthPercent() const;
 // Called every frame
 virtual void Tick(float DeltaTime) override;
 // Called to bind functionality to input
 virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
 virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
 void Shoot();
 void SwitchWeapon();
private:
 void MoveForward(float AxisValue);
 void MoveRight(float AxisValue);
 void LookUpRate(float AxisValue);
 UPROPERTY(EditAnywhere) float RotationRate =10;
 /*UPROPERTY(EditDefaultsOnly) TSubclassOf<AGun> GunClass;*/
 UPROPERTY(EditDefaultsOnly, Category = "Combat") TArray<TSubclassOf<AGun>> GunClasses;
 int32 CurrentGunIndex = 0;
 UPROPERTY(EditDefaultsOnly) AGun* Gun;
 UPROPERTY(EditDefaultsOnly) float MaxHealth = 100;
 UPROPERTY(EditDefaultsOnly) float Health = 0;
 // 重置冷却的回调函数
 void ResetDash();
 // 瞬移距离 (默认 15 米)
 UPROPERTY(EditAnywhere, Category = "Abilities") float DashDistance = 500.0f;
 // 冷却时间
 UPROPERTY(EditAnywhere, Category = "Abilities") float DashCooldown = 5.0f;
 // 瞬移后的悬空高度 (增加落地感,建议 50-80)
 UPROPERTY(EditAnywhere, Category = "Abilities|Dash") float DashHeightOffset = 60.0f;
 // 是否可以使用 (冷却标志位)
 bool bCanDash = true;
 // 定时器句柄 (用于计算冷却)
 FTimerHandle DashTimerHandle;
 // 瞬移时的特效 (可选)
 UPROPERTY(EditDefaultsOnly, Category = "Abilities") UParticleSystem* DashEffect;
 UPROPERTY(EditDefaultsOnly, Category = "Abilities") USoundBase* DashSound;
}; 

BP_MyCharacter.cpp

#include "Gun.h" #include "BP_MyCharacter.h" #include "Components/CapsuleComponent.h" #include "SimpleShooterGameModeBase.h" #include "GameFramework/GameSession.h" #include "Kismet/GameplayStatics.h"
// Sets default values
ABP_MyCharacter::ABP_MyCharacter() {
 // Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
 PrimaryActorTick.bCanEverTick = true;
}
// Called when the game starts or when spawned
void ABP_MyCharacter::BeginPlay() {
 Super::BeginPlay();
 Health = MaxHealth;
 // 隐藏原本模型的骨骼(保持不变)
 GetMesh()->HideBoneByName(TEXT("weapon_r"), EPhysBodyOp::PBO_None);
 //初始化生成第一把枪(如果有的话)
 CurrentGunIndex = 0;
 if (GunClasses.Num() > 0) {
 Gun = GetWorld()->SpawnActor<AGun>(GunClasses[CurrentGunIndex]);
 if (Gun) {
 Gun->AttachToComponent(GetMesh(), FAttachmentTransformRules::KeepRelativeTransform, TEXT("WeaponSocket"));
 Gun->SetOwner(this);
 }
 }
}
// Called every frame
void ABP_MyCharacter::Tick(float DeltaTime) {
 Super::Tick(DeltaTime);
}
// Called to bind functionality to input
void ABP_MyCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) {
 Super::SetupPlayerInputComponent(PlayerInputComponent);
 PlayerInputComponent->BindAxis(TEXT("MoveForward"), this , &ABP_MyCharacter::MoveForward);
 PlayerInputComponent->BindAxis(TEXT("LookUp"), this, &APawn::AddControllerPitchInput);
 PlayerInputComponent->BindAxis(TEXT("LookUpRate"), this, &ABP_MyCharacter::LookUpRate);
 PlayerInputComponent->BindAxis(TEXT("MoveRight"), this, &ABP_MyCharacter::MoveRight);
 PlayerInputComponent->BindAxis(TEXT("LookRight"), this, &APawn::AddControllerYawInput);
 PlayerInputComponent->BindAction(TEXT("Jump"), EInputEvent::IE_Pressed,this,&ACharacter::Jump);
 PlayerInputComponent->BindAction(TEXT("Shoot"), EInputEvent::IE_Pressed, this, &ABP_MyCharacter::Shoot);
 PlayerInputComponent->BindAction(TEXT("SwitchWeapon"), EInputEvent::IE_Pressed, this, &ABP_MyCharacter::SwitchWeapon);
 PlayerInputComponent->BindAction("Dash", IE_Pressed, this, &ABP_MyCharacter::Dash);
}
float ABP_MyCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) {
 float DamageToApplied = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
 DamageToApplied = FMath::Min(Health , DamageToApplied);
 Health -= DamageToApplied;
 UE_LOG(LogTemp, Warning, TEXT("Health is %f"), Health);
 if (IsDead()) {
 ASimpleShooterGameModeBase* GameMode = GetWorld()->GetAuthGameMode<ASimpleShooterGameModeBase>();
 if (GameMode != nullptr) {
 GameMode->PawnKilled(this);
 }
 DetachFromControllerPendingDestroy();
 GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
 }
 return DamageToApplied;
}
bool ABP_MyCharacter::IsDead() const {
 return Health <= 0;
}
void ABP_MyCharacter::MoveForward(float AxisValue) {
 AddMovementInput(GetActorForwardVector() * AxisValue);
}
void ABP_MyCharacter::MoveRight(float AxisValue) {
 AddMovementInput(GetActorRightVector() * AxisValue);
}
void ABP_MyCharacter::LookUpRate(float AxisValue) {
 AddControllerPitchInput(AxisValue * RotationRate * GetWorld()->GetDeltaSeconds());
}
void ABP_MyCharacter::Shoot() {
 if (Gun != nullptr) {
 Gun->PullTrigger();
 }
}
float ABP_MyCharacter::GetHealthPercent() const {
 return Health / MaxHealth;
}
void ABP_MyCharacter::SwitchWeapon() {
 //如果没配置枪,或者只有一把枪,就不切换
 if (GunClasses.Num() <= 1) return;
 //销毁当前手中的枪
 if (Gun) {
 Gun->Destroy();
 Gun = nullptr;
 }
 //计算下一个索引
 CurrentGunIndex = (CurrentGunIndex + 1) % GunClasses.Num();
 //生成新枪
 if (GunClasses[CurrentGunIndex]) {
 Gun = GetWorld()->SpawnActor<AGun>(GunClasses[CurrentGunIndex]);
 if (Gun) {
 Gun->AttachToComponent(GetMesh(), FAttachmentTransformRules::KeepRelativeTransform, TEXT("WeaponSocket"));
 Gun->SetOwner(this);
 // UE_LOG(LogTemp, Warning, TEXT("Switched to weapon index: %d"), CurrentGunIndex);
 }
 }
}
void ABP_MyCharacter::Dash() {
 if (!bCanDash) return;
 FVector StartLocation = GetActorLocation();
 FVector ForwardVector = GetActorForwardVector();
 FVector TargetLocation = StartLocation + (ForwardVector * DashDistance);
 FVector FinalLocation = TargetLocation;
 UWorld* World = GetWorld();
 if (!World) return;
 // 防止卡墙检测
 FHitResult WallHit;
 FCollisionQueryParams Params;
 Params.AddIgnoredActor(this);
 bool bHitWall = World->LineTraceSingleByChannel(
 WallHit, StartLocation, TargetLocation, ECollisionChannel::ECC_Visibility, Params
 );
 if (bHitWall) {
 // 如果撞墙,停在墙壁前 50 单位处,防止模型穿插
 FinalLocation = WallHit.Location - (ForwardVector * 50.0f);
 }
 FVector UpVector = FVector(0, 0, 1);
 FVector CeilingCheckStart = FinalLocation;
 // 检查头顶上方
 FVector CeilingCheckEnd = FinalLocation + (UpVector * 100.0f);
 FHitResult CeilingHit;
 bool bHitCeiling = World->LineTraceSingleByChannel(
 CeilingHit, CeilingCheckStart, CeilingCheckEnd, ECollisionChannel::ECC_Visibility, Params
 );
 // 只有头顶没撞到东西,才执行'抬高'操作
 if (!bHitCeiling) {
 FinalLocation.Z += DashHeightOffset;
 }
 if (DashEffect) {
 UGameplayStatics::SpawnEmitterAtLocation(World, DashEffect, StartLocation, GetActorRotation());
 }
 TeleportTo(FinalLocation, GetActorRotation());
 if (DashEffect) {
 UGameplayStatics::SpawnEmitterAtLocation(World, DashEffect, FinalLocation, GetActorRotation());
 }
 if (DashSound) {
 UGameplayStatics::PlaySoundAtLocation(this, DashSound, StartLocation);
 }
 bCanDash = false;
 World->GetTimerManager().SetTimer(DashTimerHandle, this, &ABP_MyCharacter::ResetDash, DashCooldown, false);
}
void ABP_MyCharacter::ResetDash() {
 bCanDash = true;
}

目录

  1. 一、获取玩家的当前位置
  2. 二、检测是否“看见”玩家
  3. 三、敌人简单巡逻逻辑
  4. 四、命令 AI 执行“射击”
  5. 五、第三人称射击游戏角色类
  • 💰 8折买阿里云服务器限时8折了解详情
  • 💰 8折买阿里云服务器限时8折购买
  • 🦞 5分钟部署阿里云小龙虾了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 命令行大模型交互工具 MCPHost 实战指南
  • Linux 系统简介
  • Oracle 数据库基础命令实战指南
  • Visual C++ 6.0 开发工具常用快捷键速查
  • AI 前端核心概念、技术栈与学习路径
  • Vivado Aurora 8B/10B IP 核配置详解
  • 10 款 AI 降重工具实测对比与选择指南
  • 从零搭建 AI 系统权限控制系统
  • Go 与 C++ 对比:性能、并发与生态差异分析
  • C++ AVL 树功能实现原理剖析
  • C++ 位运算技巧与常见算法题解
  • 黑客入门教程:从零开始掌握渗透测试与安全开发
  • 大模型训练全流程解析:从自监督学习到反向传播
  • 35 岁转行 Python 开发:职业转型的机遇与路径
  • LLaMA Factory:大语言模型微调的终极开源工具
  • LangChain 框架核心特性、生态对比与快速入门指南
  • 通义灵码实战指南:从安装配置到全栈开发落地
  • OpenClaw 和 Claude Code、Cursor、Copilot 有什么区别
  • DeepSeek 时代下的前端开发范式变革与实践指南
  • GraphMindStudio 开源工作流引擎:架构设计与 AI 智能体实践

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online

  • 随机西班牙地址生成器

    随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online