● 서론
이번에는 공격을 적중 시키기 위한 몬스터를 간단히 구현해보고 Blackboard와 Behavior Tree를 이용해서
몬스터가 랜덤한 위치를 정찰 하도록 만들어 보겠습니다.
몬스터의 구현은 앞서 구현한 캐릭터 코드에서 물리충돌과 위젯, 스텟에 관련된 코드만 가져와서 간단히
구현해 보고, FindPatrolPos라는 정찰할 목적지를 정하는 TaskNode를 구현해 보도록 하겠습니다.
몬스터 구현
몬스터의 구현을 위해 새로운 에셋을 다운받았습니다.
이후 캐릭터의 구현 코드 중 카메라와 조작에 따른 행동을 제외한 기능을 가져와
구현해 주었습니다.
참고로 몬스터의 스탯 또한 따로 스탯 컴포넌트를 생성하도록 하였습니다!
// 캡슐
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.f);
GetCapsuleComponent()->SetCollisionProfileName(TEXT("RPGCapsule"));
가장 중요한 부분은 해당 코드와 같이 콜리전을 RPGCapsule로 지정해 주어야
의도한대로 공격에 대한 충돌을 수행할 수 있을 것 입니다.
몬스터에게 성공적으로 공격판정이 들어간 모습입니다.
또한 위젯을 하나 새로 만들어서 몬스터는 다른 색의 HP를 가지도록 하였습니다.
코드 구현 전 세팅
정찰을 구현하기 위해서 AI를 만들기 위해 BlackBoard와 Behavior 트리를 이용할 것 입니다!
정찰을 시작할 위치를 정하고 해당 위치로부터 일정 범위 안의 랜덤한 위치에 목적지를 설정할
예정이기 때문에 CurrentPos와 PatrolPos를 Vector 키 타입으로 설정하여
위치를 저장할 수 있게 하였습니다.
이후 Place Actor에서 볼륨 중 Nav Mesh Bounds Volume을 생성해서 AI가 움직일 수 있는 영역을
설정 하였습니다. 사이즈를 크게 늘려서 맵 전체를 감싸도록 하였습니다.
뷰포트 화면에서 P 키를 누르면 적용된 지역을 확인 할 수 있습니다!
//.Build.cs
PublicDependencyModuleNames.AddRange( ... "AIModule", "GamePlayTasks", "NavigationSystem");
또한 해당 기능의 구현을 위해 다음과 같은 모듈들을 빌드 파일에 추가해 주어야 합니다.
AIController 구현
AIController.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "RPGAIController.generated.h"
/**
*
*/
UCLASS()
class RPG_API ARPGAIController : public AAIController
{
GENERATED_BODY()
public:
ARPGAIController();
void StartAI();
void StopAI();
protected:
// 어떤 컨트롤러가 폰에 빙의해서 조종할 때 발생되는 이벤트함수
virtual void OnPossess(APawn* InPawn) override;
private:
UPROPERTY()
TObjectPtr<UBlackboardData> BBData;
UPROPERTY()
TObjectPtr<UBehaviorTree> BTAsset;
};
몬스터의 이동을 조작하기 위해 AIController를 만들어 주었습니다.
Onpossess 함수를 오버라이드 하여 컨트롤러가 적을 조종하기 시작할 때 StartAI 함수를 실행 시키고
BBData와 BTAsset을 TObjectPtr 형태로 선언해서 블랙보드와 비헤이비어 트리를 저장할 수 있게 합니다.
AIController.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "RPGAIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BlackboardComponent.h"
ARPGAIController::ARPGAIController()
{
// BlackBoard
static ConstructorHelpers::FObjectFinder<UBlackboardData> BBDataRef(TEXT("/Script/AIModule.BlackboardData'/Game/AI/BB_RPGEnemy.BB_RPGEnemy'"));
if (BBDataRef.Succeeded())
{
BBData = BBDataRef.Object;
}
// Behavior Tree
static ConstructorHelpers::FObjectFinder<UBehaviorTree> BTAssetRef(TEXT("/Script/AIModule.BehaviorTree'/Game/AI/BT_RPGEnemy.BT_RPGEnemy'"));
if (BTAssetRef.Succeeded())
{
BTAsset = BTAssetRef.Object;
}
}
void ARPGAIController::StartAI()
{
// 블랙보드 컴포넌트 설정
UBlackboardComponent* BlackboardComp = Blackboard.Get();
if (UseBlackboard(BBData, BlackboardComp))
{
if (RunBehaviorTree(BTAsset))
{
// 블랙보드의 CurrentPos에 현재 컨트롤중인 폰의 위치를 저장
Blackboard->SetValueAsVector(FName(TEXT("CurrentPos")), GetPawn()->GetActorLocation());
}
}
}
먼저 생성자에서 블랙보드와 비헤이비어 트리를 지정해 주었습니다.
그 후 StartAI 함수를 구현하여 블랙보드 컴포넌트에 BBData를 설정해주고 BTAsset의 동작여부를 검사한 뒤
블랙보드의 값 중 아까 만든 Vector 밸류값의 CurrentPos에 대하여 몬스터의 현재 위치를 저장하도록 하였습니다.
여기서 BlackboardComp를 TObejctPtr 형식으로 선언해 보려고 했는데 UseBlackboard 함수의 두번째 인자가
UBlackboardComponent*&로 포인터의 참조형을 나타내서 사용이 불가능해 원시포인터 형식으로 선언하였습니다.
void ARPGAIController::StopAI()
{
UBehaviorTreeComponent* BTComp = Cast<UBehaviorTreeComponent>(BrainComponent);
if (BTComp)
{
BTComp->StopTree();
}
}
// AI컨트롤러가 AI캐릭터를 조종하기 시작할 때 호출
void ARPGAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
StartAI();
}
StopAI 함수를 만들어 몬스터가 죽을 때 AI가 멈추도록 합니다. BrainComponent를 BTComp로 형변환하여
형변환이 성공적으로 되었으면 AI를 정지합니다.
Onpossess함수에서 StartAI 함수를 호출해 컨트롤러가 AI캐릭터를 조종할 때 함수가 실행되도록 합니다.
Enemy.cpp
#include "RPGAIController.h"
...
ARPGEnemy::ARPGEnemy()
{
...
// AI
AIControllerClass = ARPGAIController::StaticClass();
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
...
적의 AI컨트롤러를 설정하고 맵에 배치된 몬스터와 새로 스폰된 몬스터에 대해 모두 컨트롤러를 적용받도록 합니다.
void ARPGEnemy::SetDead()
{
ARPGAIController* AIController = Cast<ARPGAIController>(Controller);
// AI 중단
if (AIController)
{
AIController->StopAI();
}
}
이후 몬스터의 SetDead 함수에서 StopAI 함수를 불러와 몬스터가 죽으면 AI를 정지하도록 합니다.
이런식으로 몬스터가 죽으면 AI를 정지시킵니다!
정찰을 위한 TaskNode 생성
BTTask_FindPatrolPos.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_FindPatrolPos.generated.h"
/**
*
*/
UCLASS()
class RPG_API UBTTask_FindPatrolPos : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTask_FindPatrolPos();
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
protected:
};
생성자와 EBTNodeResult::Type의 반환형을 가진 ExcuteTask 함수를 오버라이드 하였습니다.
EBTNodeResult::Type는 비헤이비어 트리의 성공/실패 여부를 반환합니다.
BTTask_FindPatrolPos.cpp
#include "BTTask_FindPatrolPos.h"
#include "AIController.h"
#include "NavigationSystem.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "RPGEnemyAIInterface.h"
UBTTask_FindPatrolPos::UBTTask_FindPatrolPos()
{
NodeName = TEXT("FindPatrolPos");
}
EBTNodeResult::Type UBTTask_FindPatrolPos::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
// Super 클래스의 함수 성공 여부를 저장
EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);
// 현재 컨트롤중인 Pawn을 가져오는데 실패했으면 Failed 반환
APawn* CurrentPawn = OwnerComp.GetAIOwner()->GetPawn();
if (!CurrentPawn)
{
return EBTNodeResult::Failed;
}
// 네비게이션 시스템을 가져오는데 실패했으면 Failed 반환
UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());
if (!NavSystem)
{
return EBTNodeResult::Failed;
}
// AI 인터페이스
IRPGEnemyAIInterface* AICurrentPawn = Cast<IRPGEnemyAIInterface>(CurrentPawn);
float PatrolRadius = AICurrentPawn->GetAIPatrolRadius();
if (!AICurrentPawn)
{
return EBTNodeResult::Failed;
}
// 블랙보드로 부터 CurrentPos를 받아와 저장
FVector CurrentPos = OwnerComp.GetBlackboardComponent()->GetValueAsVector(FName(TEXT("CurrentPos")));
FNavLocation PatrolPos;
// CurrentPos 위치로부터 PatrolRadius 이내에 랜덤범위를 지정해 PatrolPos에 저장
if (NavSystem->GetRandomPointInNavigableRadius(CurrentPos, PatrolRadius, PatrolPos))
{
//블랙보드의 PatrolPos에 랜덤범위 위치를 저장
OwnerComp.GetBlackboardComponent()->SetValueAsVector(FName(TEXT("PatrolPos")), PatrolPos.Location);
return EBTNodeResult::Succeeded;
}
return EBTNodeResult::Failed;
}
생성자에서 테스크 노드의 이름을 초기화 하고
ExcuteTask에서 컨트롤중인 폰과 NavySystemV1을 가져옵니다.
그리고 컨트롤중인 폰을 AI인터페이스로 형변환하여 범위에 관련된 정보들을 가져올 수 있게 합니다.
// RPGEnemyAIInterface.h
public:
virtual float GetAIPatrolRadius() = 0;
virtual float GetAIDetectRadius() = 0;
virtual float GetAIAttackRange() = 0;
인터페이스의 헤더파일은 다음과 같으며 이 중 PatrolRadius를 가져와 목표장소로 지정될 수 있는 범위를 가져옵니다.
이후 캐릭터의 위치로부터 PatrolRadius의 범위 중 한곳을 랜덤으로 지정해 PatrolPos에 저장합니다.
● 구현 결과
구현 결과인 비헤이비어 트리입니다.
3초 (+-1초) 의 대기시간을 가지고 FindPatrolPos 태스크를 실행한 뒤 PatrolPos의 위치로 움직이게 됩니다.
다음과 같이 몬스터의 정찰을 구현하였습니다.
다음 포스팅에서는 캐릭터가 몬스터의 인식범위 안으로 들어온다면
정찰을 멈추고 캐릭터쪽으로 이동하고,
공격범위 안으로 들어온다면 공격을 실행하는것을 구현해 보도록 하겠습니다.
감사합니다.
'[게임 개발] 개발 일지 > RPG' 카테고리의 다른 글
12. AI를 이용한 몬스터의 공격 (0) | 2023.09.25 |
---|---|
11. AI를 이용한 캐릭터 탐지 (0) | 2023.09.24 |
9. 캐릭터 스탯과 체력 UI 구현 (0) | 2023.09.19 |
8. 공격 판정 구현 (0) | 2023.09.15 |
7. 연속 공격 구현 (0) | 2023.09.13 |