언리얼4 C++ AI Behavior Tree

Intro

  • 언리얼 C++ AI 컨트롤 구현

AIController 생성

AIController를 부모로하는 클래스를 생성하고 캐릭터 클래스에서 사용하도록 설정한다.

#include "ZombieAIController.h"

AZombie::AZombie()
{
    ...
    
    AIControllerClass = AZombieAIController::StaticClass();
    AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}

생성 옵션을 PlaceInWorldOrSpawned로 설정하면 레벨에 배치하거나 새로 생성되는 ZombieZombieAIController의 지배를 받게된다.

비헤이비어 트리 시스템

Behavior Tree는 행동을 분석하고 우선순위가 높은 행동부터 실행하도록 하는 트리 구조의 설계 기법이며 이를 이용하면 행동 패턴을 체계적으로 설계할수 있다.

블랙보드와 비헤이비어 트리 생성

1

블랙 보드는 비헤이비어 트리에 필요한 데이터 셋을 저장한다.

비헤이비어 트리 기능 사용을 위한 모듈추가

Build.cs

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "AIModule" });

AIContorller에 블랙보드와 비헤이비어 트리 추가

AZombieAIController::AZombieAIController()
{
	static ConstructorHelpers::FObjectFinder<UBlackboardData> BBObject(TEXT("BlackboardData'/Game/AI/BB_Zombie.BB_Zombie'"));
	if (BBObject.Succeeded())
	{
		BBZombie = BBObject.Object;
	}

	static ConstructorHelpers::FObjectFinder<UBehaviorTree> BTObject(TEXT("BehaviorTree'/Game/AI/BT_Zombie.BT_Zombie'"));
	if (BTObject.Succeeded())
	{
		BTZombie = BTObject.Object;
	}
}

AI 랜덤 정찰

블랙보드 키 추가

2

NPC의 기존 위치값을 저장하는 HomePos 키와 새로 이동할 위치를 저장하는 PatrolPos키를 Vector타입으로 추가

키 값 저장

AIController에 변수를 초기화하고 HomePos 값을 저장하도록함

AIController.h

static const FName HomePosKey;
static const FName PatrolPosKey;

AIController.cpp

const FName AZombieAIController::HomePosKey(TEXT("HomePos"));
const FName AZombieAIController::PatrolPosKey(TEXT("PatrolPos"));

void AZombieAIController::Possess(APawn * InPawn)
{
	Super::Possess(InPawn);
	if (UseBlackboard(BBZombie, Blackboard))
	{
		Blackboard->SetValueAsVector(HomePosKey, InPawn->GetActorLocation());
    }
 }

FindPatrolPos 태스크 추가

관련 모듈 추가

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore","AIModule","GameplayTasks" });

BTTask를 부모로 하는 FindPatrolPos 클래스 생성

비헤이비어 트리는 태스크를 실행할 때 태스크 클래스의 ExecuteTask 멤버 함수를 실행한다.

ExecuteTask함수는 다음중 하나의 값을 반환해야 한다.

  • Aborted : 테스크 실행중 중단(실패)

  • Failed : 태스크를 수행했지만 실패

  • Succeeded : 태스크를 성공적으로 수행

  • InProgress : 태스크를 수행하고있다 결과는 차후에 보고

ExcuteTask 구현

FindPatrolPos에 다음 정찰 지점을 반환하도록 구현한다.

UBTTask_FindPatrolPos::UBTTask_FindPatrolPos()
{
	NodeName = TEXT("FindPatrolPos");
}

EBTNodeResult::Type UBTTask_FindPatrolPos::ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8 * NodeMemory)
{
	EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);

	auto ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
	if (nullptr == ControllingPawn)
		return EBTNodeResult::Failed;

	UNavigationSystem* NavSystem = UNavigationSystem::GetNavigationSystem(ControllingPawn->GetWorld());
	if (nullptr == NavSystem)
		return EBTNodeResult::Failed;

	FVector Origin = OwnerComp.GetBlackboardComponent()->GetValueAsVector(AZombieAIController::HomePosKey);
	FNavLocation NextPatrol;

	if (NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.0f, NextPatrol))
	{
		OwnerComp.GetBlackboardComponent()->SetValueAsVector(AZombieAIController::PatrolPosKey, NextPatrol.Location);
		return EBTNodeResult::Succeeded;
	}

	return EBTNodeResult::Failed;
}

네비게이션 메시 배치

3

네비게이션 메시를 배치해 스스로 길을 찾게한다.

비헤이비어 트리 구성

4

이제 AI가 5초마다 정찰을 하게된다.

플레이어 추격

Target 변수 생성

5

블랙보드에 Target 변수를 생성하고 Object 타입에 기반클래스는 Player로 지정한다.

서비스 노드 생성

플레이어를 감지해 추격하기위해 BTService를 부모로 하는 Detect 클래스를 생성한다. Service 노드는 컴포짓 노드에 부착되어 반복적인 작업을 수행한다.

#include "ZombieAIController.h"
#include "FPSPlayer.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "DrawDebugHelpers.h"

UBTService_Detect::UBTService_Detect()
{
	NodeName = TEXT("Detect");
	Interval = 1.0f;
}

void UBTService_Detect::TickNode(UBehaviorTreeComponent & OwnerComp, uint8 * NodeMemory, float DeltaSeconds)
{
	Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

	APawn* ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
	if (nullptr == ControllingPawn) return;

	UWorld* World = ControllingPawn->GetWorld();
	FVector Center = ControllingPawn->GetActorLocation();
	float DetectRadius = 600.0f;

	if (nullptr == World) return;
	TArray<FOverlapResult> OverlapResults;
	FCollisionQueryParams CollisionQueryParam(NAME_None, false, ControllingPawn);
	bool bResult = World->OverlapMultiByChannel(
		OverlapResults,
		Center,
		FQuat::Identity,
		ECollisionChannel::ECC_GameTraceChannel2,
		FCollisionShape::MakeSphere(DetectRadius),
		CollisionQueryParam
	);

	if (bResult)
	{
		for (auto OverlapResult : OverlapResults)
		{
			AFPSPlayer* FPSPlayer = Cast<AFPSPlayer>(OverlapResult.GetActor());
			if (FPSPlayer && FPSPlayer->GetController()->IsPlayerController())
			{
				OwnerComp.GetBlackboardComponent()->SetValueAsObject(AZombieAIController::TargetKey, FPSPlayer); // 타겟 저장
				return;
			}
		}
	}
	else
	{
		OwnerComp.GetBlackboardComponent()->SetValueAsObject(AZombieAIController::TargetKey, nullptr);
	}
}

TickNode 함수에 OverlapMultiByChannel 함수로 반경내 캐릭터 감지기능을 넣고 감지된 캐릭터중에 플레이어 캐릭터가 있다면 BlackBoardTarget에 저장하도록 한다.

이때 마찬가지로 AIControllerTargetKey 변수를 추가해준다.

const FName AZombieAIController::TargetKey(TEXT("Target"));

서비스 추가

7

만들어진 서비스를 Selector에 추가한다.

데코레이터 추가

추격과 정찰 컴포짓에 데코레이터를 추가한다.

6

이때 On Value Change로 설정하면 키 값이 변경될때 바로 현재 노드의 실행을 취소한다. 즉 타겟이 범위안에 들어오면 바로 추격에 들어가고 범위를 벗어나면 다시 정찰하게 된다.

AI 공격

AttackRange 데코레이터 클래스 생성

공격 범위 내에 플레이어가 있는지 판단하는 데코레이터를 생성한다.

CalculateRawConditionValue 함수를 상속받아 조건이 달성됐는지 파악하도록 한다.

bool UBTDecorator_IsInAttackRange::CalculateRawConditionValue(UBehaviorTreeComponent & OwnerComp, uint8 * NodeMemory) const
{
	bool bResult = Super::CalculateRawConditionValue(OwnerComp, NodeMemory);

	auto ControllingPawn = Cast<AZombie>(OwnerComp.GetAIOwner()->GetPawn());
	if (nullptr == ControllingPawn)
		return false;

	auto Target = Cast<AFPSPlayer>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(AZombieAIController::TargetKey));
	if (nullptr == Target)
		return false;

	bResult = (Target->GetDistanceTo(ControllingPawn) <= 100); 
	return bResult;
}

Target에 플레이어를 캐스트하고 거리가 100이하가 되면 true를 반환하게된다.

공격 태스크 생성

NPC의 공격 애니메이션이 끝날때 동안 대기하도록 InProgress를 반환하고 공격이 끝났을때 태스크가 끝났다고 알려줘야한다.

FinishLatentTask 함수를 이용해 태스크가 끝났다고 알려주도록 한다.

UBTTask_Attack::UBTTask_Attack()
{
	bNotifyTick = true;
	IsAttacking = false;
}

EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);

	auto Zombie = Cast<AZombie>(OwnerComp.GetAIOwner()->GetPawn());
	if (nullptr == Zombie)
		return EBTNodeResult::Failed;

	Zombie->Attack();
	IsAttacking = true;
	Zombie->OnAttackEnd.AddLambda([this]() -> void {
		IsAttacking = false;
	});

	return EBTNodeResult::InProgress;
}

void UBTTask_Attack::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);
	if (!IsAttacking)
	{
		FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
	}
}

}

Zombie의 공격 몽타주가 종료되면 이를 알려주도록 델리게이트를 선언한다.

DECLARE_MULTICAST_DELEGATE(FOnAttackEndDelegate);

FOnAttackEndDelegate OnAttackEnd;
void AZombie::OnAttackMontageEnded(UAnimMontage * Montage, bool bInterrupted)
{
	IsAttacking = false;

	OnAttackEnd.Broadcast();
}

몽타주 재생이 끝나면 OnAttackEndBroadcast한다.

정리하면 태스크에서 공격 애니메이션을 호출하고 몽타주가 끝나면 OnAttackEnd에 의해 람다 함수가 호출되어 IsAttackingfalse가 되어 Tick함수에서 FinishLatentTask 함수가 호출되어 태스크가 종료된다.

회전

캐릭터가 공격방향 뒤로 가버려도 같은 방향만 공격하므로 공격을 하는 동시에 캐릭터 방향으로 회전하는 태스크를 추가한다.

EBTNodeResult::Type UBTTask_TurnToTarget::ExecuteTask(UBehaviorTreeComponent & OwnerComp, uint8 * NodeMemory)
{
	EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);

	auto Zombie = Cast<AZombie>(OwnerComp.GetAIOwner()->GetPawn());
	if (nullptr == Zombie)
		return EBTNodeResult::Failed;

	auto Target = Cast<AFPSPlayer>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(AZombieAIController::TargetKey));
	if (nullptr == Target)
		return EBTNodeResult::Failed;

	FVector LookVector = Target->GetActorLocation() - Zombie->GetActorLocation();
	LookVector.Z = 0.0f;
	FRotator TargetRot = FRotationMatrix::MakeFromX(LookVector).Rotator();
	Zombie->SetActorRotation(FMath::RInterpTo(Zombie->GetActorRotation(), TargetRot, GetWorld()->GetDeltaSeconds(), 2.0f));

	return EBTNodeResult::Succeeded;
}

타켓방향의 벡터를 저장하고 회전시키도록한다.

비헤이비어 트리 구성

8

틱마다 타겟을 감지하며 감지되지 않았을 때 랜덤한 지점으로 정찰을 하며 감지가되면 타겟쪽으로 이동한다.

타겟과 일정한 거리에 가까워지면 공격을 시도하며 Simple Parallel로 공격과 회전 태스크를 동시에 수행한다.

실행 결과

gif

참고문서

http://api.unrealengine.com/KOR/Engine/AI/BehaviorTrees/UserGuide/index.html

이득우, 이득우의 언리얼 C++ 게임개발의 정석,2018, 396쪽

댓글남기기