전 시간에는 상대가 나를 보면 쫓아오게 하는 로직까지 구현하였다. 이번 시간엔 동작을 좀더 유연하게 만들면서 지정한 위치를 패트롤 하는 로직을 만들어 보겠다.
일단 AI 가 지정한 위치를 가게 하려면 무엇이 필요할까? 바로 이정표다. 우리는 이 이정표를 Actor 를 상속받은 클래스와 BTTask 로 정의하여 사용하도록 하겠다. 먼저, C++ 클래스를 생성해 주도록 하겠다. Actor 를 상속받게 클릭 해준 뒤 다음을 눌러 이름은 PatrolPath 로 붙여주도록 하겠다.
그 뒤 들어가 다음과 같이 코드를 고쳐준다. 아래는 헤더 파일 이다.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PatrolPath.generated.h"
UCLASS()
class AITEST_API APatrolPath : public AActor
{
GENERATED_BODY()
public:
APatrolPath();
FVector GetPatrolPoint(int const index) const;
int Num() const;
private:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AI", meta=(MakeEditWidget="true", AllowPrivateAccess="true"))
TArray<FVector> PatrolPoints;
};
보면 TArray 로 FVector 의 배열인 PatrolPoints 를 만들었다. PatrolPath 는 한개 이상의 경로를 지정해 줄 것이기 때문에 배열로 지점들을 생성할 수 있게 만드는 것이다. 그리고 함수 GetPatrolPoint 와 Num 을 통해 지점의 위치, 지점이 몇번째 인지 알려준다. 아래는 Cpp 파일이다.
#include "PatrolPath.h"
APatrolPath::APatrolPath()
{
PrimaryActorTick.bCanEverTick = false;
}
FVector APatrolPath::GetPatrolPoint(int const index) const
{
return PatrolPoints[index];
}
int APatrolPath::Num() const
{
return PatrolPoints.Num();
}
이로써 PatolPath 는 FVector 의 위치를 가진 Point 와 몇 번째인지 알려주는 Num 으로 이루어지게 되었다. 그렇다면 이를 토대로 Task 를 만들어보자. 우리가 만들어야하는 BTTask 는 총 두개인데 하나는 FindPathPoint 로 PathPoint 의 지점을 찾는 Task 와 IncrementPathIndex 로 다음에 가야할 Point 를 찾는 Task 를 만들어 보겠다.
Behavior Tree 의 예상 진행은 "패트롤 포인트의 위치를 가져오고 -> 그곳으로 움직인 뒤 -> 패트롤 포인트의 인덱스를 하나 늘린다" 이다.
BTTask 를 C++ 클래스로 만들텐데 이는 전과 동일한 방법으로 BTTask_BlackBoardBase 를 상속받아 만들어 준다. 이름은 BTTask_FindPathPoint 로 지어준다.
그 뒤 다음과 같이 코드를 작성해준다. 아래는 헤더파일이다.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/Tasks/BTTask_BlackboardBase.h"
#include "BTTask_FindPathPoint.generated.h"
UCLASS()
class AITEST_API UBTTask_FindPathPoint : public UBTTask_BlackboardBase
{
GENERATED_BODY()
public:
explicit UBTTask_FindPathPoint(FObjectInitializer const& ObjectInitializer);
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
private:
UPROPERTY(EditAnywhere, Category = "Blackboard", meta = (AllowPrivateAccess = "true"))
FBlackboardKeySelector PatrolPathVectorKey;
};
다른 BTTask 와 같은 로직을 public 에 추가해주고 private 에는 PatrolPathVectorKey 를 저장해줄 KeySelector 를 선언해준다. 아래는 Cpp 파일이다.
#include "BTTask_FindPathPoint.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "NPCAIController.h"
#include "NPC.h"
UBTTask_FindPathPoint::UBTTask_FindPathPoint(FObjectInitializer const& ObjectInitializer):
UBTTask_BlackboardBase{ ObjectInitializer }
{
NodeName = TEXT("Find Path Point");
}
EBTNodeResult::Type UBTTask_FindPathPoint::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
// attemp to get the NPC's Controller
if (auto* const cont = Cast<ANPCAIController>(OwnerComp.GetAIOwner()))
{
// attemp to get the blackboard component from the behavior Tree
if (auto* const bc = OwnerComp.GetBlackboardComponent())
{
// get the current patrol path index from the blackboard
auto const Index = bc->GetValueAsInt((GetSelectedBlackboardKey()));
// get the NPC
if (auto* npc = Cast<ANPC>(cont->GetPawn()))
{
// get the current patrol path vector from the NPC - this is local to the patrol path actor
auto const Point = npc->GetPatrolParth()->GetPatrolPoint(Index);
// convert the local vector to a global point
auto const GlobalPoint = npc->GetPatrolParth()->GetActorTransform().TransformPosition(Point);
bc->SetValueAsVector(PatrolPathVectorKey.SelectedKeyName, GlobalPoint);
// finish with success
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
return EBTNodeResult::Succeeded;
}
}
}
return EBTNodeResult::Failed;
}
코드 자체는 복잡하지 않다. 가야할 PatrolPoint 의 인덱스를 가져온 뒤 PatrolPoint 의 좌표를 가져온다. 그 뒤 그 값을 반환하여 주고 Task가 끝나게 된다. Behavior Tree 에서는 Move To 태스크가 이미 구현되어 있기에 따로 그곳으로 이동하는 로직을 만들어줄 필요는 없다. FindPatrolPoint 에서 반환된 좌표값을 Move To 태스크가 움직여 줄 테니.
이제 다른 Task 인 IncrementPathIndex 를 만들어 보겠다. 위와 동일하게 BTTask_BlackBoardBase 를 상속받아 C++ 클래스를 만들어 준다.
그 뒤 다음과 같이 코드를 작성해준다. 아래는 헤더파일이다.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/Tasks/BTTask_BlackboardBase.h"
#include "BTTask_IncrementPathIndex.generated.h"
UCLASS()
class AITEST_API UBTTask_IncrementPathIndex : public UBTTask_BlackboardBase
{
GENERATED_BODY()
public:
explicit UBTTask_IncrementPathIndex(FObjectInitializer const& ObjectInitializer);
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
private:
enum class EDirectionType {Forward, Reverse};
EDirectionType Direction = EDirectionType::Forward;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AI", meta=(AllowPrivateAccess = "true"))
bool bBiDirectional = false;
};
여기선 privat 에 EDirectionType 이 나오게 되는데 enum class 즉, 열거형으로 선언해 준다. enum 에 대해서는 다음에 다루도록 하겠다. EDirectionType 은 두가지의 열거자를 가지는데 Forward, Reverse 이다. 이들은 나중에 인스턴스 값으로 사용될 것이며 Forward 면 PathPoint 에 끝에 도달하면 다시 0번부터 시작하고 Reverse 면 역순으로 돌아가게 된다.
bool 형을 통해 Forward 인지 Reverse 인지 판단하게 된다.
아래는 Cpp 파일이다.
#include "BTTask_IncrementPathIndex.h"
#include "NPCAIController.h"
#include "NPC.h"
#include "BehaviorTree/BlackboardComponent.h"
UBTTask_IncrementPathIndex::UBTTask_IncrementPathIndex(FObjectInitializer const& ObjectInitializer) :
UBTTask_BlackboardBase{ObjectInitializer}
{
NodeName = TEXT("Increment Path Index");
}
EBTNodeResult::Type UBTTask_IncrementPathIndex::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
// try and get the AI controller
if (auto* const cont = Cast<ANPCAIController>(OwnerComp.GetAIOwner()))
{
// try and get the NPC
if (auto* const NPC = Cast<ANPC>(cont->GetPawn()))
{
// try and get the blackboard
if (auto* const bc = OwnerComp.GetBlackboardComponent())
{
// get number of patrol points and min and max indices
auto const NoOfPoints = NPC->GetPatrolParth()->Num();
auto const MinIndex = 0;
auto const MaxIndex = NoOfPoints - 1;
auto Index = bc->GetValueAsInt(GetSelectedBlackboardKey());
// change direction if we are at the first or last index if we are in bidirectional mode
if (bBiDirectional)
{
if (Index >= MaxIndex && Direction == EDirectionType::Forward)
{
Direction = EDirectionType::Reverse;
}
else if (Index == MinIndex && Direction == EDirectionType::Reverse)
{
Direction = EDirectionType::Forward;
}
}
//write new value of index to blackboard
bc->SetValueAsInt(GetSelectedBlackboardKey(),
(Direction == EDirectionType::Forward ? ++Index : --Index) % NoOfPoints);
// finish with success
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
return EBTNodeResult::Succeeded;
}
}
}
return EBTNodeResult::Failed;
}
이번엔 문장이 좀 길었는데 영어로 주석을 달아놓았다. 보면 NPC 에서 GetPatrolPath 를 가져와 사용하는데 이는 NPC 인스턴스에 GetPatrolPath() 함수를 구현할 것이다. 이는 어떤 PatrolPath 액터를 사용할 것인지 선택하는 인스턴스 이다. MinIndex 는 0 이고 MaxIndex 는 총 Points 값에서 1을 빼준다. 이는 0 부터 시작하기 때문이다. 아래의 코드를 보면 Index 가 MaxIndex 에 도달할 시 Forward 할지 Reverse 할지 선택한다. 이를 토대로 bc 에 Value 를 집어넣을 때 3항 방정식으로 Forward 면 ++ 해주고 아니라면 -- 해준다. % NoOfPoint 를 하는 이유는 이래야 Loop 하기 때문이다. (여기서 Loop 한다는 의미는 끝에 도달해도 다시 처음으로 가던지 되돌아가던지 한다는 의미이다.) 이렇게 코드를 작성한 뒤 NPC 헤더와 Cpp 파일을 약간 수정해주자.
아래는 NPC의 헤더파일이다.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BehaviorTree.h"
#include "CppAICharacterBase.h"
#include "PatrolPath.h"
#include "NPC.generated.h"
UCLASS()
class AITEST_API ANPC : public ACppAICharacterBase, public ICombatInterface
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
ANPC();
// Called every frame
virtual void Tick(float DeltaTime) override;
// Called to bind functionality to input
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
UBehaviorTree* GetBehaviorTree() const;
APatrolPath* GetPatrolParth() const;
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="AI", meta=(AllowPrviateAccess="true"))
UBehaviorTree* Tree;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI", meta = (AllowPrviateAccess = "true"))
APatrolPath* PatrolPath;
};
기존에 PatrolPath 를 추가하였다. 그리고 GetPatrolPath 를 통해 이를 가져올 수 있게 만들겠다. (Parth 는 오타이다.)
// Fill out your copyright notice in the Description page of Project Settings.
#include "NPC.h"
#include "NPCAIController.h"
// Sets default values
ANPC::ANPC()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = false;
}
// Called when the game starts or when spawned
void ANPC::BeginPlay()
{
Super::BeginPlay();
}
// Called every frame
void ANPC::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
// Called to bind functionality to input
void ANPC::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
}
UBehaviorTree* ANPC::GetBehaviorTree() const
{
return Tree;
}
APatrolPath* ANPC::GetPatrolParth() const
{
return PatrolPath;
}
이렇게 해주면 이제 NPC 블루프린트에서 PatrolPath 의 액터를 선택할 수 있게 된다. 그리고 저번 시간에 AI 의 움직임이 딱딱했는데 이를 해결해줄 ChasePlayer 라는 새로운 BTTask 로 플레이어를 쫓아 올 수 있게 만들어 주겠다. C++ 클래스 생성에서 BTTask_BlackBoardBase 를 상속받은 새로운 BTTask 인 BTTask_ChasePlayer 를 만들어 준다. 그리고 아래와 같이 코드를 작성해 주자.
아래는 BTTask_ChasePlayer 의 헤더이다.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/Tasks/BTTask_BlackboardBase.h"
#include "BTTask_ChasePlayer.generated.h"
UCLASS()
class AITEST_API UBTTask_ChasePlayer : public UBTTask_BlackboardBase
{
GENERATED_BODY()
public:
explicit UBTTask_ChasePlayer(FObjectInitializer const& ObjectInitializer);
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};
아주 간단한 로직이기에 private 에 무엇인가를 추가할 필요는 없다. 다음은 Cpp 파일이다.
#include "BTTask_ChasePlayer.h"
#include "NPCAIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Blueprint/AIBlueprintHelperLibrary.h"
UBTTask_ChasePlayer::UBTTask_ChasePlayer(FObjectInitializer const& ObjectInitializer) :
UBTTask_BlackboardBase{ ObjectInitializer }
{
NodeName = TEXT("Chase Player");
}
EBTNodeResult::Type UBTTask_ChasePlayer::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
//get target location from blackboard via the NPC'S Controller
if (auto* const cont = Cast<ANPCAIController>(OwnerComp.GetAIOwner()))
{
auto const PlayerLocation = OwnerComp.GetBlackboardComponent()->GetValueAsVector(GetSelectedBlackboardKey());
// move to the Player's Location
UAIBlueprintHelperLibrary::SimpleMoveToLocation(cont, PlayerLocation);
//finish with Success
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
return EBTNodeResult::Succeeded;
}
return EBTNodeResult::Failed;
}
간단하게 Move To Task 를 대신해줄 Task 이다. BlueprintHelperLibrary 에 SimpleMoveToLocation 이 존재하니 이를 가져다 사용한다.
이제 언리얼 에디터를 켜 새로운 Behavior Tree 를 만들어주자. 전에 만들었던 것 처럼 콘텐츠 드로우에서 우클릭 -> 인공지능 -> 비헤비어 트리 를 선택한 뒤 이름은 BT_NPC_PatrolPath 로 지어주겠다.
그 뒤 다음과 같이 세팅한다.
Chasing Player 오른쪽으로 길게 뻗어있는 노드는 아직 사용할 것이 아니기에 신경은 안써도 된다. 설정들을 살펴보기 전에 BlackBoard 로 들어가 몇개의 키값을 만들어 준다.
Int 값 인 PatrolPathIndex 와 Vector 값 인 PatrolPathPoint 를 만들어준다. 그 뒤 다시 비헤비어 트리로 돌아가 다음과 같이 설정들을 바꾸어 주자. 아래의 Leap 부터 왼쪽에서 오른쪽 순으로 보여주겠다.
여기서 Bi Directional 이라는 값이 체크 되어있는데 이는 체크를 해제하면 인덱스의 끝에 도달할 때 왔던 길을 되돌아가게 된다. 체크가 되어있다면 다시 0번 인덱스부터 다시 Patrol 을 시작한다.
Can't See Player 데코레이터의 모습이다. 여기서 관찰자 중단을 Both 로 설정하면 패트롤 지점의 끝에 도달하지 않아도 플레이어를 인식해 쫓아오게 만들 수 있다. 전에 만들었던 BT_NPC 에도 이렇게 적용해주자.
Can See Player 데코레이터도 동일하게 관찰자 중단을 Both 로 둔다. 이제 PatrolPath Actor 을 World 에 배치한 뒤 이를 실험해보자. 월드에 PatrolPath Actor 을 배치한 뒤 Top View 로 시점을 전환해 봐보자. 그럼 다음과 같이 보일 것이다.
이 네모난 PatrolPoints 는 옆의 디테일 창에 AI 하위의 PatrolPoints 들을 추가하면 하나 씩 생기는데 이를 계속 추가해 5개 정도 만들고 월드에 각각 배치해준다.
총 5개의 포인트를 만들어 주었다. 이제 NPC 의 블루프린트를 복제해 하나를 더 만들고 그 뒤 디테일에서 AI 안의 Tree 에 새로만든 BT_NPC_PatrolPath 를 선택해주고 아래의 PatrolPath 는 월드에 배치한 BP_PatrolPath 를 선택해준다. 그 뒤 실행을 시켜보면 NPC 가 PatrolPoints 를 잘 쫓아간다!
그리고 쫓아오는 모습도 자연스럽게 지점으로 이동중에 플레이어를 쫓아온다. 오늘은 NPC 가 정해진 루트를 이동하게 하는 것과 쫓아오는 로직을 바꾸어 자연스럽게 만들어 보았다. 다음은 NPC 가 공격을 하는 로직을 구현해 보도록 하겠다.
'Unreal > 따라해보기' 카테고리의 다른 글
블루프린트로 채팅 만들기(1) - Unreal Engine (0) | 2025.03.18 |
---|---|
AI 적 만들어 보기 (5) - Unreal Engine (C++) (0) | 2025.03.06 |
AI 적 만들어 보기 (4) - Unreal Engine (C++) (0) | 2025.03.05 |
AI 적 만들어보기 (2) - Unreal Engine(C++) (0) | 2025.02.18 |
AI 적 만들어보기(1) - Unreal Engine(C++) (0) | 2025.02.17 |