今天要做一個簡單但有互動性的敵人 AI,遵循 「接近 → 攻擊 → 退開 → 觀察(Strafe)」 的策略循環,讓敵人看起來像有思考,而不是單純機械重複動作。
C++ 新增:
BTTask_Attack.h/.cpp
BTTask_StepBack.h/.cpp
BTTask_Strafe.h/.cpp
BTTask_Attack.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_Attack.generated.h"
UCLASS()
class ITHOME30DAYS_API UBTTask_Attack : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTask_Attack();
protected:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
UPROPERTY(EditAnywhere, Category="AI")
float AttackEffect= 300.f;
};
BTTask_Attack.cpp
(暫時沒有功能)#include "BTTask_Attack.h"
#include "GameFramework/Character.h"
#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"
UBTTask_Attack::UBTTask_Attack()
{
NodeName = "Attack";
}
EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
// 取得控制的 Pawn
AAIController* AICon = OwnerComp.GetAIOwner();
ACharacter* Enemy = Cast<ACharacter>(AICon->GetPawn());
AActor* Target = Cast<AActor>(OwnerComp.GetBlackboardComponent()->GetValueAsObject("Target"));
if (!Enemy || !Target) return EBTNodeResult::Failed;
return EBTNodeResult::Succeeded;
}
BTTask_StepBack.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_StepBack.generated.h"
UCLASS()
class ITHOME30DAYS_API UBTTask_StepBack : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTask_StepBack();
protected:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
UPROPERTY(EditAnywhere, Category="AI")
float StepBackDistance = 300.f;
};
BTTask_StepBack.cpp
#include "BTTask_StepBack.h"
#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
UBTTask_StepBack::UBTTask_StepBack()
{
NodeName = "Step Back";
}
EBTNodeResult::Type UBTTask_StepBack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
AAIController* AICon = OwnerComp.GetAIOwner();
ACharacter* Enemy = Cast<ACharacter>(AICon->GetPawn());
AActor* Target = Cast<AActor>(OwnerComp.GetBlackboardComponent()->GetValueAsObject("Target"));
if (!Enemy || !Target) return EBTNodeResult::Failed;
FVector Dir = (Target->GetActorLocation() - Enemy->GetActorLocation()).GetSafeNormal();
FVector BackLoc = Enemy->GetActorLocation() - Dir * StepBackDistance;
AICon->MoveToLocation(BackLoc);
return EBTNodeResult::Succeeded;
}
沿面對玩家反方向後退
OwnerComp.GetBlackboardComponent()->GetValueAsObject("Target") 此為 Blackboard 的 Key
BTTask_Srafe.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_Strafe.generated.h"
UCLASS()
class ITHOME30DAYS_API UBTTask_Strafe : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTask_Strafe();
uint16 GetInstanceMemorySize() const;
protected:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds);
void DoStrafe(UBehaviorTreeComponent& OwnerComp);
UPROPERTY(EditAnywhere, Category="AI")
float StrafeDistance = 200.f;
UPROPERTY(EditAnywhere, Category="AI")
int MaxStrafeAmount = 3;
};
BTTask_Srafe.cpp
#include "BTTask_Strafe.h"
#include "AIController.h"
#include "GameFramework/Character.h"
#include "Kismet/KismetMathLibrary.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Navigation/PathFollowingComponent.h"
struct FStrafeTaskMemory
{
int32 RemainingStrafes;
};
UBTTask_Strafe::UBTTask_Strafe()
{
NodeName = "Strafe Random Times";
bNotifyTick = true; // 允許 Tick
}
uint16 UBTTask_Strafe::GetInstanceMemorySize() const
{
return sizeof(FStrafeTaskMemory);
}
EBTNodeResult::Type UBTTask_Strafe::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
FStrafeTaskMemory* Memory = (FStrafeTaskMemory*)NodeMemory;
// 隨機決定次數 (1~3)
Memory->RemainingStrafes = FMath::RandRange(1, MaxStrafeAmount);
DoStrafe(OwnerComp);
return EBTNodeResult::InProgress;
}
void UBTTask_Strafe::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
AAIController* AICon = OwnerComp.GetAIOwner();
if (!AICon) return;
if (AICon->GetMoveStatus() == EPathFollowingStatus::Idle)
{
FStrafeTaskMemory* Memory = (FStrafeTaskMemory*)NodeMemory;
if (Memory->RemainingStrafes > 0)
{
DoStrafe(OwnerComp);
}
else
{
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}
}
}
void UBTTask_Strafe::DoStrafe(UBehaviorTreeComponent& OwnerComp)
{
AAIController* AICon = OwnerComp.GetAIOwner();
ACharacter* Enemy = Cast<ACharacter>(AICon->GetPawn());
AActor* Target = Cast<AActor>(OwnerComp.GetBlackboardComponent()->GetValueAsObject("Target"));
if (!Enemy || !Target) return;
FVector Dir = (Target->GetActorLocation() - Enemy->GetActorLocation()).GetSafeNormal();
FVector RightVec = UKismetMathLibrary::Cross_VectorVector(Dir, FVector::UpVector);
float Side = (FMath::RandBool() ? 1.f : -1.f);
FVector StrafeLoc = Enemy->GetActorLocation() + RightVec * Side * StrafeDistance;
AICon->MoveToLocation(StrafeLoc);
FStrafeTaskMemory* Memory = (FStrafeTaskMemory*)OwnerComp.GetNodeMemory(this, OwnerComp.GetActiveInstanceIdx());
Memory->RemainingStrafes--;
}
隨機次數內,圍繞玩家做平移(Strafe)。
此使用 FStrafeTaskMemory 做次數儲存,如果使用 declare 的 MaxStrafeAmount,會導致所有在場景內的 Enemy AI 受影響。
Sequence 行為執行順序為從左到右
這樣敵人就會執行: 到達玩家位置->攻擊->後撤->觀察->重複。