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 = "Updata Player Location"; } void UBTService_PlayerLocation::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) { Super::TickNode(OwnerComp,NodeMemory,DeltaSeconds); APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(),0); if (PlayerPawn == nullptr) { return; } OwnerComp.GetBlackboardComponent()->SetValueAsVector(GetSelectedBlackboardKey(),PlayerPawn->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 = "Updata 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; }