프로그래밍/Unreal 부트캠프

TIL 2025.02.25 기록

Rozentea 2025. 2. 25. 23:05

0. 개요


 

우선 오늘은.. 챌린지반 과제로 퀵 정렬을 구현해보고, 이후로는 똑같이 팀 프로젝트를 진행했다.

 

원래 어제 팀프로젝트 일정을 계획했을 때는 수요일까지 하려고 했던 분량을 오늘 안에 마무리 했다..

이유는 생각보다 남은 시간이 없어보였어서 최대한 빨리빨리 드론을 마치고 다른 업무에 붙어서 작업하는게 좋겠다는 판단이 들었기 때문이다.

물론.. 3D 게임 AI로직을 짜본적이 없어서.. 몬스터를 추적해 공격하는 기능 구현이 얼마나 걸릴지.. 감이 안잡힌다.. 그래도 최대한 하루 안에 끝내볼 생각이다.

 

시간이 얼마 없는 관계로 빨리 기록을 마치고 다시 작업하러 가야겠다.

 

1. 퀵 정렬 구현하기


#include <algorithm>
#include <vector>

using namespace std;

int Partition(vector<int>& arr, int left, int right)
{
	int pivot;
	int low, high;

	low = left;
	high = right +1;
	pivot = arr[left];

	do
	{
		do
		{
			low++;
		} while (low <= right && arr[low] < pivot);

		do
		{
			high--;
		} while (high >= left && arr[high] > pivot);

		if (low < high)
		{
			swap(arr[low], arr[high]);
		}

	} while (low < high);

	swap(arr[left], arr[high]);

	return high;
}

void QuickSort(vector<int>& arr, int left, int right)
{
	if (left < right)
	{
		int pivot = Partition(arr, left, right);

		QuickSort(arr, left, pivot - 1);
		QuickSort(arr, pivot + 1, right);
	}
}

int main()
{
	vector<int> arr = { 8, 4, 2, 6, 1, 9, 5 };

	QuickSort(arr, 0, arr.size() - 1);

	return 0;
}

pivot을 가운대로 잡고 시작하는게 아니라 각 부분배열들의 가장 왼쪽을 pivot으로 설정해 결국에는 해당 배열의 가운대부근과 swap한다.

low와 high가 이동해 서로 교차하는 순간 반복이 종료된다.

 

2. 드론 AI 만들기


1. IdleRandomTask 추가하기

#include "Drone/BehaviorTree/BTTask_SetIdleTypeRandomly.h"
#include "BehaviorTree/BlackboardComponent.h"

UBTTask_SetIdleTypeRandomly::UBTTask_SetIdleTypeRandomly()
{
	NodeName = "SetIdleTypeRandomly";
}

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

	UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
	if (!BlackboardComp) return EBTNodeResult::Failed;

	// 0 또는 1의 랜덤 값 선택
	const int32 RandomIdleType = FMath::RandRange(0, 1);
	BlackboardComp->SetValueAsInt("IdleType", RandomIdleType);

	return EBTNodeResult::Succeeded;
}

단순히 랜덤 값을 BlackBoard에 전달하도록 구현했다.

해당 랜덤 값을 좀 더 확률을 나누어 대부분 원형 회전이 나오게하고, 낮은 확률로 통통 튀는 행동을 하도록 수정하면 좋을 것 같다.

전환이 잘 되는지 확인하기 위해서 우선 각 IdleTask들의 끝 지점을 만들어줄 것이다.

 

2. HappyMovementTask 종료 후 IdleRandomTask를 다시 진행하도록 수정하기

void UBTTask_HappyMovement::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	if (OwnerComp.GetBlackboardComponent()->GetValueAsEnum("CurrentState") != 0)
		FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
	if (CurrentTime > 5.0f)
		FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
	
	Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);

	const TObjectPtr<APawn> ControlledPawn = OwnerComp.GetAIOwner()->GetPawn();
	const TObjectPtr<APawn> PlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);

	if (ControlledPawn && PlayerPawn)
	{
		UpdateHappyMovement(OwnerComp.GetAIOwner(), ControlledPawn, PlayerPawn, DeltaSeconds);
	}

	CurrentTime += DeltaSeconds;
}

통통 튀는 행동은 단순히 동작 시간을 부여해 5.0초가 지나면, 다시 랜덤 Task를 진행해 2가지 Idle 상태중 한가지로 전환되도록 해주었다.

 

3. CircleAroundPlayerTask 종료 IdleRandomTask를 다시 진행하도록 수정하기

어떻게 할까 생각을 하다가 단순히 float 변수를 추가해 누적 회전 값을 구해서 누적 회전 값이 2PI(360도)를 넘어서면 행동이 끝나다는 판정을 해주도록 해주었다.

 

하지만, 이렇게만 할 경우 지금 AddFroce로 드론을 조작중이기 때문에 아직 드론이 도착하지 않았는데, 행동이 끝나 점점 드론 위치가 밀리는 문제가 발생했었다.

 

때문에 DeadZone을 설정해 원래 드론이 있어야하는 BaseLocation에 어느정도 가까워져야만 행동이 끝날 수 있도록 추가 조건을 주어 해결했다.

 

void UBTTask_CircleAroundPlayer::IsIdleCircleMovementDone(UBehaviorTreeComponent& OwnerComp)
{
	const TObjectPtr<APawn> Drone = OwnerComp.GetAIOwner()->GetPawn();
	const TObjectPtr<APawn> Player = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
	const TObjectPtr<ADroneAIController> DroneAIController = Cast<ADroneAIController>(OwnerComp.GetAIOwner());
	if (AccumulateAngle >= 2 * PI
		&& FVector::Dist(Drone->GetActorLocation(),
			Player->GetActorLocation() + Player->GetActorRotation().RotateVector(DroneAIController->GetBaseDroneOffset())) <= 15.f)
	{
		AccumulateAngle = 0.0f;
		FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
	}
}

또, 추가적으로 드론이 원형으로 도는 것과 통통 튀는 행동들의 시작 지점이 원하는 지점이 아니었기 때문에 해당 부분을 조금 수정해주었다.

 

현재 통통 튀는 모션이 너무 자주 나오고, 한번 실행되고 나면 다음번에는 무조건 Idle 상태가 나오도록 수정도 해야할 것 같고..

지금 드론을 AddForce()로 조작하고 있어서 원형으로 돌다가 행동이 전환될 때, 통통 튀는 행동의 시작 지점과 거리가 멀면 위로 튀는게 아니라 앞뒤로도 조금 튀는 행동이 발생한다.

ㄴ> 해당 부분은 오늘이나 내일 작업할 원하는 지점에 드론 정지시키기 행동을 만들면, 해당 기능을 이용해 원하는 지점에 멈추었다가 통통 튀도록 시퀀스 노드를 이용해 수정해볼 생각이다.

 

4. IdleRandomTask 수정하기

우선.. 상황 몇가지를 생각해보자.

  1. UBTTask_CircleAroundPlayer → UBTTask_CircleAroundPlayer
    이런 전환은 무한하게 가능해도 상관 없다.
  2. UBTTask_HappyMovement → UBTTask_HappyMovement
    이런 전환은 무조건 한번만 호출되는게 좋다.

때문에.. 이전에 UBTTask_HappyMovement가 호출되었다면, 다음 행동에서 UBTTask_HappyMovement를 제외해주도록 해야할 것 같다.

 

또한, UBTTask_HappyMovement이 너무 자주 발생한다면, 확률을 조금 낮춰서 해당 행동이 자주 발생하지 않도록 수정해야 할 것 같다.

 

if (PrevIdleType == 1)
	{
		BlackboardComp->SetValueAsInt("IdleType", 0);
		PrevIdleType = 0;
	}
	else
	{
		const int32 RandomIdleType = FMath::RandRange(0, 1);
		PrevIdleType = RandomIdleType;
		BlackboardComp->SetValueAsInt("IdleType", RandomIdleType);
	}

int 변수 PrevIdleType을 추가해 간단하게 수정해주었다.

다만.. 추후에 Idle이 추가되거나 할 경우를 생각하면.. 이 방식이 안좋을 것 같지만..

( 차라리 Enum이 관리 차원에서 나을 것 같다. )

아직 Idle을 추가할 예정이 없기도 하고, 아직 구현할 것들이 너무 많이 남아있어서 이 정도 간단한 처리로 마무리하려 한다..

( 영상을 제대로 못찍은거 같긴한데.. ㅠ )

위에서 수정한 대로 UBTTask_HappyMovement를 행동하고 나면, 다음은 반드시 UBTTask_CircleAroundPlayer을 선택해준다.

 

5. 대기 기능 만들기 ( UBTTask_WaitForStabilization )

우선 Idle 상태 전환 시점을 다시 잡기 위한 것에 초점을 맞춰 대기 상태를 만들 것이다.

음.. 조금 이따가 이동 명령 수행 기능을 만들 때.. 해당 테스크를 이용해주고 싶은데.. 로직이 조금씩 달라서.. 한번에 깔끔하게 처리해줄 방법이 잘 떠오르지 않는다..

오랫동안 고민하고 있을 시간이 없으니.. 우선 해당 테스크에서 모드를 나누어 현재 State가 Idle이냐 이동 명령이냐에 따라 다르게 처리해주는 방식으로 설계할 것이다.

 

드론이 원하는 TargetLocation에 멈추어져 있는지 확인을 하고, 만약 멈추지 않았다면, 조금씩 이동을 해주도록 구현했다.

ㄴ> 음.. 이미 이동 로직들이 있는데, 이곳에서 속도만 낮춰서 다시 하는게… 조금 이상하지만.. 지금은 어쩔 수 없는 것 같다.. ㅠ

 

또, 원하는 위치에 드론이 위치하게 되더라도 1초라는 시간을 두어 확실히 도착한 것인지 확인하도록 구현했다.

 

6. 대기 기능을 활용해 Idle 상태 전환 시점 다시 잡기

이제 드론이 신나기 전에 포지션을 한번 잡고, 이후에 신나는 상태로 전환되도록 수정했다.

 

아무리봐도 UBTTask_HappyMovement가 부자연스럽다.. ㅠㅠ

시간이 날 때마다 조금씩 손을 봐야 할 것 같다..

 

7. UBTTask_HappyMovement 수정

높이를 조금 줄이고, 기존 PID 컨트롤에서 제한하던 DeadZone을 해제해 주었다가 행동이 끝나면 ,다시 DeadZone을 활성화 하도록 수정해 주었다.

 

음.. 이젠 조금 자연스러운 것 같은데, 회전만 이동에 맞춰서 해줄 수 있으면 좋을 것 같다.. ㅠ