iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0

今天要做一個簡單但有互動性的敵人 AI,遵循 「接近 → 攻擊 → 退開 → 觀察(Strafe)」 的策略循環,讓敵人看起來像有思考,而不是單純機械重複動作。

1. 事前準備

C++ 新增:

  • BTTask_Attack.h/.cpp
  • BTTask_StepBack.h/.cpp
  • BTTask_Strafe.h/.cpp

2. Code

  • 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 受影響。

4. Behavior Tree

https://ithelp.ithome.com.tw/upload/images/20251003/20171036MyW6akBlaL.png

Sequence 行為執行順序為從左到右

完成


這樣敵人就會執行: 到達玩家位置->攻擊->後撤->觀察->重複。


上一篇
# Day 18|敵人 AI (一)
下一篇
# Day 20|敵人 AI (三) & 玩家血條
系列文
30 天用 Unreal Engine 5 C++ 開發遊戲21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言