1. 블랙보드와 비헤이비어 트리 생성
먼저 블랙보드를 생성하여 타겟을 찾을 수 있도록 오브젝트 타입으로 타겟 변수를 생성하였습니다.
그리고 비헤이비어 트리에 블랙보드 에셋에 생성한 블랙보드를 적용하였습니다.
APredatorAIController::APredatorAIController()
{
static ConstructorHelpers::FObjectFinder<UBehaviorTree> BTAsset(TEXT("BehaviorTree'/Game/Animal/BTPredator.BTPredator'"));
if (BTAsset.Succeeded())
m_pBTAsset = BTAsset.Object;
static ConstructorHelpers::FObjectFinder<UBlackboardData> BBAsset(TEXT("BlackboardData'/Game/Animal/BBPredator.BBPredator'"));
if (BBAsset.Succeeded())
m_pBBAsset = BBAsset.Object;
}
void APredatorAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
// 사용하려는 블랙보드를 지정한다
if (UseBlackboard(m_pBBAsset, Blackboard))
{
// 행동트리를 지정하고 동작하게 한다
if (!RunBehaviorTree(m_pBTAsset))
{
}
}
}
그리고 PredatorAIController의 생성자에서 블랙보드와 비헤이비어 트리를 받아온 후에 OnPossess 함수를 이용하여 블랙보드를 지정하고 비헤이비어 트리를 동작시켰습니다.
루트에 플레이어를 탐지하는 서비스를 달아주어서 계속해서 플레이어를 탐지하도록 하였습니다.
서비스는 AI Controller가 존재하면 계속해서 실행되는 함수입니다.
2. 플레이어 감지 서비스 제작
// 매초마다 실행되는 함수
void UBTService_PlayerDetect::TickNode(UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
APredatorAIController* pController = Cast<APredatorAIController>(OwnerComp.GetAIOwner());
if (!IsValid(pController))
return;
APredatorAnimal* pPredatorAnimal = Cast<APredatorAnimal>(pController->GetPawn());
if (!IsValid(pPredatorAnimal))
return;
FCollisionQueryParams params(NAME_None, false, pPredatorAnimal);
// 생성되는 구 모형과 겹치는 채널을 resultArray에 담는 함수입니다.
TArray<FOverlapResult> resultArray;
bool bOverlap = GetWorld()->OverlapMultiByChannel(resultArray, pPredatorAnimal->GetActorLocation(),
FQuat::Identity, ECollisionChannel::ECC_GameTraceChannel4,
FCollisionShape::MakeSphere(pPredatorAnimal->GetTraceRange()), params);
// 겹쳐진 타겟이 존재하면 타겟을 세팅합니다
if (bOverlap)
{
pController->SetTarget(resultArray[0].GetActor());
}
else
{
pController->SetTarget(nullptr);
}
}
OverlapMultiByChannel을 사용해서 지정한 채널과 겹쳐지면 true를 반환하도록 하였습니다.
첫번째 인자인 resultArray는 채널을 담는 배열이고, 두번째 인자로는 몬스터의 위치를 모형의 중심으로 준 것입니다.
그리고 네번째 인자는 ECC_GameTraceChannel4는 플레이어를 탐지할 수 있도록 만들어준 트레이스 채널입니다. 해당 채널은 오직 플레이어와만 overlap이 감지되도록 설정되어있습니다.
다섯번째 인자는 구 모형으로 만들도록 설정해 준 것이고 마지막 인자는 default 값을 넣어준 것입니다.
이렇게 플레이어를 탐지하는 구 모형을 만든 뒤에 if문을 사용하여 겹쳐진 플레이어가 있다면 아까 생성해준 블랙보드의 타겟에 넣어주었습니다.
3. 몬스터 Task 제작
타겟이 세팅되면 사진의 서브트리가 실행됩니다.
해당 서브트리에서는 만약 타겟이 자신의 공격 사거리 내에 존재한다면 Attack 태스크를 실행하였고
그렇지 않다면 타겟을 쫓아가도록 하였습니다.
// Task의 상태를 반환하는 함수
EBTNodeResult::Type UBTTask_Attack::ExecuteTask(
UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
EBTNodeResult::Type eResult = Super::ExecuteTask(OwnerComp, NodeMemory);
APredatorAIController* pController = Cast<APredatorAIController>(OwnerComp.GetAIOwner());
// 컨트롤러가 타당하지 않다면 실패 처리
if (!IsValid(pController))
return EBTNodeResult::Failed;
APredatorAnimal* pPredatorAnimal = Cast<APredatorAnimal>(pController->GetPawn());
// 몬스터가 타당하지 않다면 실패 처리
if (!IsValid(pPredatorAnimal))
return EBTNodeResult::Failed;
// 공격 사거리에 들어왔을때 AttackEnd 함수를 델리게이트로 등록한다
pPredatorAnimal->AddAttackEndDelegate<UBTTask_Attack>(this, &UBTTask_Attack::AttackEnd);
// 몬스터가 살아있을 때 공격을 시작한다
pPredatorAnimal->SetAnimType(EAnimalAnim::Attack);
bAttack = true;
pPredatorAnimal->Attack(); // 몬스터 공격 상태 등록
return EBTNodeResult::InProgress;
}
void UBTTask_Attack::AttackEnd()
{
bAttack = false;
}
ExecuteTask 함수는 Task의 상태를 반환하는 함수입니다.
Task를 실행하는데 필요한 변수들의 유효성 검사를 실행하여 타당하지 않다면 Failed 처리를 합니다.
그리고 나서 공격이 끝났을 때 실행되어야 할 함수인 AttackEnd 함수를 델리게이트로 등록해주었습니다.
델리게이트 함수는 언리얼에서 제공하는 매크로 함수인데, 함수 등록을 해주면 자동으로 실행시켜줍니다.
그 다음 이제 플레이어를 공격하도록 몬스터의 상태를 공격 상태로 변경시켜주었습니다.
// 매초마다 호출되는 함수
void UBTTask_Attack::TickTask(UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory, float DeltaSeconds)
{
Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);
APredatorAIController* pController = Cast<APredatorAIController>(OwnerComp.GetAIOwner());
APredatorAnimal* pPredatorAnimal = Cast<APredatorAnimal>(pController->GetPawn());
ACharacter* pTarget = pController->GetTarget();
// 타겟이 공격 사거리에서 벗어나면 Task 실패 처리한다
if (!pTarget)
{
pController->StopMovement();
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
return;
}
// 타겟을 바라보도록 회전시킨다
FVector vDir = pTarget->GetActorLocation() - pPredatorAnimal->GetActorLocation();
vDir.Normalize(); // 방향 정보만 필요하기 때문에 Normalize를 적용한다
// 계산한 방향으로 몬스터를 강제 Yaw 회전시킨다
pPredatorAnimal->SetActorRotation(FRotator(0.f, vDir.Rotation().Yaw, 0.f));
// 공격이 끝났을 경우 종료시킨다
if (!bAttack)
{
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}
}
매초마다 호출되는 TickTask 함수를 사용하여 타겟이 공격 사거리 내에 있는지 계속해서 체크하였습니다.
공격 사거리에서 벗어나면 Task를 실패처리 하였고, 공격 사거리 내에 존재한다면 몬스터가 계속해서 타겟쪽을 바라볼 수 있도록 회전시켜주었습니다. 몬스터의 회전은 타겟의 위치에서 몬스터의 위치를 빼서 벡터를 구한 다음 그 벡터의 방향을 통해 적용해주었습니다. 그리고 마지막에는 bAttack 변수를 체크해 공격이 끝났을 경우 Task를 성공 상태로 반환해주었습니다.
오른쪽 자식 노드인 TargetMove Task의 ExecuteTask 함수는 Attack Task와 동일하게 유효성 검사만 하기 때문에 넘어가도록 하겠습니다.
void UBTTask_TargetMove::TickTask(UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory, float DeltaSeconds)
{
Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);
APredatorAIController* pController = Cast<APredatorAIController>(OwnerComp.GetAIOwner());
APredatorAnimal* pPredatorAnimal = Cast<APredatorAnimal>(pController->GetPawn());
ACharacter* pTarget = pController->GetTarget();
// 타겟이 존재하지 않는다면 멈추고 Task를 Failed 처리한다
if (!pTarget)
{
pController->StopMovement();
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
return;
}
// 몬스터가 살아있다면 이동을 한다
// 이동을 지정하는 함수
UAIBlueprintHelperLibrary::SimpleMoveToActor(pController, pTarget);
pPredatorAnimal->SetAnimType(EAnimalAnim::Run);
// 몬스터와 타겟 사이의 거리를 구한다
FVector vAILoc = pPredatorAnimal->GetActorLocation();
FVector vTargetLoc = pTarget->GetActorLocation();
vAILoc.Z = 0.f;
vTargetLoc.Z = 0.f;
float fDist = FVector::Distance(vAILoc, vTargetLoc);
// 거리가 공격 사거리 이하가 된다면 멈추고 Task의 상태를 성공으로 처리한다
if (fDist <= pPredatorAnimal->GetAttackRange())
{
pController->StopMovement();
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
return;
}
}
TargetMove 또한 매초마다 호출되는 TickTask를 사용해서 타겟이 존재하는지 계속 체크하였습니다. 타겟이 존재하지 않는다면 Task를 실패 처리하였고, 타겟이 존재한다면 몬스터를 타겟에게 이동시켰습니다. 그리고나서 몬스터와 타겟 사이의 거리를 구해서 둘 사이의 거리가 공격 사거리 이하가 되면 몬스터의 이동을 중지하고 바로 Task를 성공 처리하였습니다. 이렇게 해야 왼쪽 자식 노드인 Attack Task를 바로 실행할 수 있습니다.
왼쪽 서브트리는 타겟이 존재하지 않을 때 실행되도록 설정하였습니다.
이 서브트리에서는 몬스터가 특정 지역들을 패트롤 하도록 하였고 패트롤 지역에 도달할 때마다 2초씩 머무르도록 하였습니다.
protected:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (AllowPrivateAccess = "true"))
TArray<APatrolPoint*> PatrolPointArray;
먼저 PatrolPoint를 C++ 프로젝트로 생성한 후에 SpawnPoint 클래스에서 PatrolPoint 클래스를 담는 TArray 변수를 선언하였습니다.
그리고 에디터에서 Patrol Point를 설정하여 몬스터가 이동할 위치를 알 수 있도록 하였습니다.
void UBTTask_Patrol::TickTask(UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory, float DeltaSeconds)
{
Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);
APredatorAIController* pController = Cast<APredatorAIController>(OwnerComp.GetAIOwner());
APredatorAnimal* pPredatorAnimal = Cast<APredatorAnimal>(pController->GetPawn());
// 몬스터의 위치와 Patrol Point의 위치를 얻어온다
FVector vPredatorAnimalLoc = pPredatorAnimal->GetActorLocation();
FVector vPatrolPointLoc = pPredatorAnimal->GetPatrolPoint()->GetActorLocation();
vPatrolPointLoc.Z = vPredatorAnimalLoc.Z;
// 몬스터를 Patrol Point 위치로 이동시킨다
pController->MoveToActor(pPredatorAnimal->GetPatrolPoint(), -1.f, false, true);
pPredatorAnimal->SetAnimType(EAnimalAnim::Run);
vPredatorAnimalLoc.Z = 0.f;
vPatrolPointLoc.Z = 0.f;
// 둘 사이의 거리를 구한다
float fDist = FVector::Distance(vPredatorAnimalLoc, vPatrolPointLoc);
// 몬스터 객체가 위치에 도착했는지 판단한다
if (fDist <= 5.f)
{
// 몬스터의 이동을 멈추고 Patrol Point를 다음 것으로 설정한다
pPredatorAnimal->SetAnimType(EAnimalAnim::Idle);
pController->StopMovement();
pPredatorAnimal->NextPatrolPoint();
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}
}
TargetMove Task와 비슷하게 여기서는 몬스터가 자신이 알고있는 Patrol Point를 순서대로 이동하도록 하였습니다.
GetPatrolPoint 함수는 몬스터가 알고 있는 Patrol Point를 얻어오는 함수인데, 이를 사용해서 몬스터를 Patrol Point의 위치로 이동시켰습니다.
그리고 몬스터와 Patrol Point 사이의 거리를 확인하고 Patrol Point의 위치에 도착했는지 판단하였습니다.
만약 도착하였다면 이동을 멈추고 Patrol Point를 다음 것으로 설정하였습니다.
전체 비헤이비어 트리의 모습입니다.
다음에 해야할 일
PreyAnimal AI Controller 제작
Data Table 제작
'게임 개발 (언리얼 엔진)' 카테고리의 다른 글
UE4 생존게임 제작 - 7 (몬스터 공격 충돌 시스템 및 플레이어 상태 UI 제작) (0) | 2020.11.05 |
---|---|
UE4 생존게임 제작 - 6 (PreyAnimal AI Controller 및 Data Table 제작) (0) | 2020.11.04 |
UE4 생존게임 제작 - 4 (랜드스케이프 제작) (0) | 2020.11.01 |
UE4 생존게임 제작 - 3 (충돌 시스템 제작) (0) | 2020.10.31 |
UE4 생존게임 제작 - 2 (몬스터 제작) (0) | 2020.10.30 |