게임 개발 (언리얼 엔진)

UE4 생존게임 제작 - 6 (PreyAnimal AI Controller 및 Data Table 제작)

언린이 2020. 11. 4. 18:21

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

 

PredatorAnimal AI Controller를 제작할 때와 마찬가지로 블랙보드와 비헤이비어 트리를 생성하였습니다.

먼저 블랙보드를 생성하여 타겟을 찾을 수 있도록 오브젝트 타입으로 타겟 변수를 생성하였고 비헤이비어 트리에 생성한 블랙보드를 적용하였습니다.

 

 

APreyAIController::APreyAIController()
{
	static ConstructorHelpers::FObjectFinder<UBehaviorTree> BTAsset(TEXT("BehaviorTree'/Game/Animal/BTPrey.BTPrey'"));

	if (BTAsset.Succeeded())
		m_pBTAsset = BTAsset.Object;

	static ConstructorHelpers::FObjectFinder<UBlackboardData> BBAsset(TEXT("BlackboardData'/Game/Animal/BBPrey.BBPrey'"));

	if (BBAsset.Succeeded())
		m_pBBAsset = BBAsset.Object;
}

void APreyAIController::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);

	// 사용하려는 블랙보드를 지정한다
	if (UseBlackboard(m_pBBAsset, Blackboard))
	{
		// 행동트리를 지정하고 동작하게 한다
		if (!RunBehaviorTree(m_pBTAsset))
		{
		}
	}
}

 

생성자에서 생성한 블랙보드와 비헤이비어 트리 에셋을 적용하고 이를 동작시켰습니다.

 

 

 

PredatorAnimal AI Controller와 마찬가지로 플레이어를 감지하는 서비스가 부착된 노드를 루트 노드에 붙여주었습니다.

 

 

2. 플레이어 감지 서비스 제작

 

이 또한 PredatorAnimal AI Controller에서 제작한 것과 동일하여 넘어가도록 하겠습니다.

 

 

3. 몬스터 Task 제작

 

 

타겟이 설정되었을 때, 실행되는 Task로 Run이라는 Task를 만들어주었습니다.

해당 Task는 플레이어를 감지하면 플레이어의 위치와 반대 방향으로 몬스터를 이동시킵니다.

PredatorAnimal AI Controller는 타겟을 발견하면 공격하기 위해 플레이어 쪽으로 오지만 PreyAnimal AI Controller는 반대로 도망치도록 한 것입니다. 이것이 제가 AI Controller를 두 부류로 나눈 이유입니다.

 

 

// 매초마다 실행되는 함수
void UBTTask_Run::TickTask(UBehaviorTreeComponent& OwnerComp,
	uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);

	APreyAIController* pController = Cast<APreyAIController>(OwnerComp.GetAIOwner());

	APreyAnimal* pPreyAnimal = Cast<APreyAnimal>(pController->GetPawn());

	ACharacter* pTarget = pController->GetTarget();

	// 타겟이 존재하지 않는다면 멈추고 Task를 Failed 처리한다
	if (!pTarget)
	{
		pPreyAnimal->SetAnimType(EAnimalAnim::Idle);
		pController->StopMovement();
		FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
		return;
	}


	// 타겟과 반대 방향을 바라보도록 회전시킨다
	FVector vDir = pTarget->GetActorLocation() - pPreyAnimal->GetActorLocation();
	vDir.Normalize(); // 방향 정보만 필요하기 때문에 Normalize를 적용한다
	vDir = -vDir; // 반대 방향 벡터를 구한다

	// 계산한 방향으로 몬스터를 강제 Yaw 회전시킨다
	pPreyAnimal->SetActorRotation(FRotator(0.f, vDir.Rotation().Yaw, 0.f));

	// 몬스터의 위치와 타겟의 위치를 얻어온다
	FVector vPreyAnimalLoc = pPreyAnimal->GetActorLocation();
	FVector vTargetLoc = pTarget->GetActorLocation();

	// 타겟 위치에서 몬스터의 위치와 대칭하는 점을 찾는다
	FVector vGoalArea;
	vGoalArea.X = vPreyAnimalLoc.X - (vTargetLoc.X - vPreyAnimalLoc.X);
	vGoalArea.Y = vPreyAnimalLoc.Y - (vTargetLoc.Y - vPreyAnimalLoc.Y);
	vGoalArea.Z = vPreyAnimalLoc.Z;

	// 대칭하는 점으로 이동시킨다
	pController->MoveToLocation(vGoalArea);
	pPreyAnimal->SetAnimType(EAnimalAnim::Run);
}

 

매초마다 실행되는 TickTask 함수를 사용하여 타겟이 존재하는지 계속 확인하였습니다.

타겟이 있다면, 몬스터에서 타겟으로 향하는 벡터를 구한 다음 그 벡터의 -값을 적용하여 정반대를 향하는 벡터를 구하였습니다. 그리고 해당 벡터를 이용해 몬스터를 강제 Yaw 회전시켜서 몬스터가 플레이어의 반대 방향을 향하도록 하였습니다. 또한, 타겟의 위치를 몬스터의 위치에 대칭을 통해 대칭점을 구한 다음 해당 지점으로 몬스터를 이동시켰습니다. 해당 지점은 플레이어의 위치와 정반대 쪽에 위치한 곳이기 때문에 몬스터가 플레이어로부터 멀어질 수 있습니다.

 

 

 

타겟이 설정되지 않았을 때는 PredatorAnimal AI Controller와 마찬가지로 몬스터들이 자신이 가지고 있는 Patrol Point로 이동하고 해당 지점에 도착하면 2초간 머무르도록 하였습니다. 이 부분 또한 PredatorAnimal AI Controller와 동일하므로 넘어가도록 하겠습니다.

 

 

 

전체 비헤이비어 트리의 모습입니다.

 

 

4. Data Table 제작

 

 

Data Table에 제작한 몬스터에 대한 정보들을 설정하였습니다.

 

 

USTRUCT(Atomic, BlueprintType)
struct FAnimalState
{
	GENERATED_USTRUCT_BODY()

public:
	FAnimalState()
	{
	}

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (AllowPrivateAccess = "true"))
	float	fAttack;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (AllowPrivateAccess = "true"))
	float	fArmor;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (AllowPrivateAccess = "true"))
	int32	iHP;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (AllowPrivateAccess = "true"))
	int32	iHPMax;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (AllowPrivateAccess = "true"))
	int32	iMP;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Meta = (AllowPrivateAccess = "true"))
	int32	iMPMax;
};

 

그리고 나서 Data Table의 정보들을 저장할 변수들을 struct 타입으로 선언하였습니다.

 

 

UMainGameInstance::UMainGameInstance()
{
	static ConstructorHelpers::FObjectFinder<UDataTable> AnimalInfoAsset(TEXT("DataTable'/Game/Animal/AnimalInfoTable.AnimalInfoTable'"));

	if (AnimalInfoAsset.Succeeded())
		AnimalInfoTable = AnimalInfoAsset.Object;
}

 

const FAnimalInfo* UMainGameInstance::FindAnimalInfo(const FName& key) const
{
	if (!IsValid(AnimalInfoTable))
		return nullptr;

	return AnimalInfoTable->FindRow<FAnimalInfo>(key, TEXT(""));
}

 

GameInstance를 상속받은 C++ 프로젝트를 하나 만든 후에 ConstructorHelpers를 이용해 제작한 Data Table을 불러왔습니다. 그리고 해당 프로젝트에서 키 값을 이용해 Data Table의 Row 정보를 찾는 함수를 정의하였습니다. 함수 안의 FindRow 함수는 언리얼에서 제공해주는 함수입니다.

 

 

void AAnimal::BeginPlay()
{
	Super::BeginPlay();

	AnimalAnim = Cast<UAnimalAnim>(GetMesh()->GetAnimInstance());

	UMainGameInstance* GameInst = GetGameInstance<UMainGameInstance>();

	const FAnimalInfo* pAnimalInfo = GameInst->FindAnimalInfo(AnimalName);

	if (pAnimalInfo)
	{
		AnimalState.fAttack = pAnimalInfo->Attack;
		AnimalState.fArmor = pAnimalInfo->Armor;
		AnimalState.iHP = pAnimalInfo->HP;
		AnimalState.iHPMax = pAnimalInfo->HPMax;
		AnimalState.iMP = pAnimalInfo->MP;
		AnimalState.iMPMax = pAnimalInfo->MPMax;
		fTraceRange = pAnimalInfo->TraceRange;
		fAttackRange = pAnimalInfo->AttackRange;
	}
}

 

마지막으로 프로그램이 실행되면 호출되는 BeginPlay 함수에서 Data Table의 정보들로 아까 선언해두었던 struct의 값들을 초기화하였습니다. 생성자에서 하지 않고 BeginPlay 함수에서 하는 이유는 생성자는 에디터의 뷰포트에 객체들을 배치하기 위해 프로그램을 실행하지 않아도 호출됩니다. 하지만 Data Table을 읽는 행위는 프로그램이 실행된 이후에 벌어지는 일이므로 BeginPlay 함수에서 해줘야 합니다.

 

 

 

프로그램을 실행하면 몬스터의 state 값들이 초기화된 모습을 확인할 수 있습니다.

 

 

다음에 해야할 일

 

몬스터 공격 충돌 시스템 및 UI 제작