본문 바로가기
프로그래밍/Unreal 부트캠프

TIL 2025.02.24 기록

by Rozentea 2025. 2. 24.

0. 개요


주말동안 드론 AI를 더 작업해서, Idle 상태 2개를 추가하고, Behavior Tree 구조로 변경해주었다.

또, 오늘은 이제 1주차 프로토타입을 다 같이 기능 연결해 테스트 해보는 시간을 가졌다.

 

아직 모든 기능이 연결된게 아니라.. Item, Player, Drone만 기능 연결이 끝났다.

 

연결하면서도 이런 저런 문제가 많이 발생하고, 또 생각하지 못한 부분에서도 계속해 문제가 발생해 쉬운 작업은 아니었던 것 같다.. ㅠ

 

또, 챌린지반 수업도 들어서 챌린지반 수업에 내용을 간단히 정리하고, 주말과 오늘 작업한 내용에 대해서 기록해둘 것이다.

 

1. 챌린지반 수업


< 버블 정렬 >

첫 번째 자료와 두 번째 자료를, 두 번째 자료와 세 번째 자료를 비교해가면서 자료를 정렬하는 방법이다.

구간에서 비교를 할 때, 둘의 순서를 잡아주는 것을 반복해 정렬하는 방법이다.

이때, 구간을 움직이면서 가장 큰 숫자를 우측으로 배치하는 등 (반대도 가능)을 해주는 정렬 방법이다.

이때 한 번 끝까지 갔으면 가장 뒷 원소는 이미 정렬이 된 상태이기 때문에 다음 반복할 때는 그 앞까지만 순회해도 된다.

이렇게 점점 줄이다보면 정렬이 된다.

 

#include <vector>

using namespace std;

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

	int Cnt = 0;
	for (int i = 0; i < arr.size() - 1; ++i)
	{
		Cnt++;
		for (int j = 0; j < arr.size() - Cnt; ++j)
		{
			if (arr[j] > arr[j + 1])
			{
				int Temp = arr[j + 1];
				arr[j + 1] = arr[j];
				arr[j] = Temp;
			}
		}	
	}

	return 0;
}

 

 

< 선택 정렬 >

선택 정렬은 index 하나마다 위치할 원소를 결정하고 그 다음 index로 넘어가는 기법이다.

내 기준으로 정리하자면.. idx 하나를 기준으로 잡고 그 뒤에 위치하는 모든 원소를 확인해 가장 작은 값을 가진 idx와 서로 swap한다.

이를 반복하면 오름차순으로 정렬되는 것.

 

#include <vector>

using namespace std;

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

	for (int i = 0; i < arr.size() - 1; ++i)
	{
		int MinIdx = 0;
		int MinVal = 10000000;
		for (int j = i; j < arr.size(); ++j)
		{
			if (MinVal > arr[j])
			{
				MinVal = arr[j];
				MinIdx = j;
			}
		}

		arr[MinIdx] = arr[i];
		arr[i] = MinVal;
	}

	return 0;
}

 

<삽입 정렬>

선택 정렬이 전체에서 최솟값을 "선택"하는 거 였다면 삽입 정렬은 전체에서 하나씩 올바른 위치에 "삽입"하는 방식이다.

선택 정렬에 비해 삽입 정렬은 필요한 때만 위치를 변경하므로 더 효율적인 방식이다.

 

#include <vector>

using namespace std;

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

	for (int i = 1; i < arr.size(); ++i)
	{
		for (int j = i; j > 0; --j)
		{
			if (arr[j - 1] > arr[j])
			{
				int temp = arr[j - 1];
				arr[j - 1] = arr[j];
				arr[j] = temp;
			}
			else
				break;
		}
	}

	return 0;
}

 

2. Drone AI 만들기


1. Idle 상태 구현하기

< 플레이어를 기준으로 회전하도록 만들기 >

우선 플레이어 주위로 회전하도록만 만들었는데, 단순히 공전만하는 것은 생동감이 떨어져 보였다.

때문에 위 아래의 위치를 조절해 주려했는데, 문제는 어떻게 자연스러운 랜덤을 주느냐였다.

단순 랜덤 값을 주면.. 무작위 스럽긴 하지만 상승, 하강의 폭이 연속적이지 않다면 부자연스럽게 나올 것이기 때문이다.

(단순 내 추측..)

 

때문에 언리얼에서 이미 제공해주는 PerlinNoise를 적용해 자연스러운 위 아래 움직임을 주었다.

단순 목표 지점만 계산하고 실제 움직임은 이전에 구현했던 PID Controller를 이용해 이동하도록 해주었다.

 

하지만 여전히 불만족스럽다.

왜냐하면, 캐릭터의 앞쪽으로 드론이 이동할 경우 원근감 때문에 너무 드론이 멀리있는 것처럼 보인다..

TopView 방식이면.. 이렇게 불편하지 않았을 것 같은데.. 카메라가 뒤쪽에서 앞을 보는 형태라 눈에 너무 잘 띄는 것 같다.

 

< 원근감을 생각해 더 보기 좋게 만들기 >

때문에 좀 더 보기 편하게 하기 위해서 플레이어 기준으로 앞으로 드론이 갈 경우 보정을 해주도록 구현했다.

확실히 이전보다는 플레이어 앞쪽으로 드론이 이동할때 비교적 멀리가지 않는다.

음.. 하지만 보정 값을 너무 크게주면.. 원형이 너무 깨져서 오히려 부자연 스러워진다..

 

때문에 이후에 더 수정할 떄는 이점을 조금 고치고 싶다.

 

음.. 타원을 사용하기엔 한쪽이 너무 길어질것 같고..

사실 제일 괜찮아 보이는건.. 스플라인을 이용하는 것일 것 같다.

Z축 로케이션 변화나 이동 속도의 변화 등등을 다양한 랜덤값을 이용해 자연스럽게 조절하고 이동 경로만 스플라인으로 정해주는게 작업하기 훨씬 수월할 것 같다. 음음

 

2. BT 구조로 변환하기

< 상태 분류하기 >

  • Idle 상태:
    • 플레이어 주변을 원형으로 선회.
    • 랜덤하게 높이 변화.
  • Follow 상태:
    • 플레이어를 따라다님.
    • 특정 거리 안에서는 속도를 조절하면서 접근.

- Blackboard 변수관리 -

Key Type
PlayerActor Object
CurrentState Enum/String

 

 

- BT Node 설계 -

Root
├── Selector

│ ├── Sequence (플레이어 따라가기)
│ │ ├── Blackboard 조건 (IsPlayerMove == true)
│ │ └── Task: Follow Player

│ ├── Sequence (Idle 상태)
│ │ └── Task: Circle Around Player

 

< BTNode 생성 및 함수 이동하기 >

기존 DroneAIController에서 완성했던 플레이어를 추적하는 기능과 플레이어 주위를 회전하는 기능을 각 행동 노드로 만들어 BT를 완성해주었다.

 

BTTask들은 모두 C++로 작성해 만들었다.

 

Build.cs에 GamePlayTasks 추가 ㄴ> BTTaskNode 상속받아 C++ 클래스를 만들면서 추가함.

ExecuteTask : Task가 실행되기 시작할 때 호출되는 함수 AbortTask : 실행을 중단해야 한다고 판단할 때 호출하는 조건함수

TickTask : Task가 실행될 때마다 발생하지만, 첫 Task에서는 아니다. 첫 번째는 ExecuteTask가 호출된다.

OnMessage : 메세지를 보내려할 때

 

또, InProgress로 설정해 TickTask를 호출하도록 구현한다면, FinishLatentTask()을 사용해 EBTNodeResult를 반환해주어야 한다.

⇒ 이걸 몰랐어서 분명 Controller에서 상태를 변경했는데, 왜 실제로는 변경이 안되지? 싶었다… ㅠㅠ

BlackBoard 판정은 false가 뜨더라도 EBTNodeResult를 반환해주지 않는다면, 계속해 TickTask()를 호출하게 된다.. ㅠ

 

아무튼 각 함수들을 BTTask로 만들어 실험해본 결과 동일하게 잘 동작하는 것을 확인할 수 있었다.

 

< AIController에서 상태 변환하기 >

void ADroneAIController::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
	
	if (const TObjectPtr<APawn> Player = UGameplayStatics::GetPlayerPawn(GetWorld(), 0))
	{
		const FVector PlayerVelocity = Player->GetVelocity();
		const FVector DroneVelocity = GetPawn()->GetVelocity();
		const float DistanceToPlayer = FVector::Dist(Player->GetActorLocation(), GetPawn()->GetActorLocation());
        	
		static float IdleTransitionTime = 0.0f;
		static float FollowTransitionTime = 0.0f;
		static float LastDistanceToPlayer = DistanceToPlayer;

		const bool bPlayerStopped = PlayerVelocity.Length() < 10.0f;
		const bool bDroneAlmostStopped = DroneVelocity.Length() <= 10.0f;
		const bool bPlayerClose = DistanceToPlayer < 300.0f;
		const bool bPlayerFar = DistanceToPlayer > 1000.0f;

		float DistanceChange = FMath::Abs(DistanceToPlayer - LastDistanceToPlayer);

		if (bPlayerStopped && bDroneAlmostStopped && bPlayerClose && DistanceChange < 5.0f)
		{
			IdleTransitionTime += DeltaTime;
			FollowTransitionTime = 0.0f;

			if (IdleTransitionTime >= 1.0f)
			{
				BlackboardComp->SetValueAsEnum("CurrentState", 0); // Idle State
			}
		}
		else if (bPlayerFar)
		{
			BlackboardComp->SetValueAsEnum("CurrentState", 1); // Follow State
			IdleTransitionTime = 0.0f;
			FollowTransitionTime = 0.0f;
		}
		else
		{
			FollowTransitionTime += DeltaTime;
			IdleTransitionTime = 0.0f;

			if (FollowTransitionTime >= 0.5f && DistanceChange > 5.0f)
			{
				BlackboardComp->SetValueAsEnum("CurrentState", 1); // Follow State
			}
		}
		LastDistanceToPlayer = DistanceToPlayer;

		DroneRotation(Player);
	}
}

이제 DroneAIController는 BT의 상태를 변경해주고, PID Controller와 Drone의 회전만 관리 해주도록 만들었다.

Drone에 AI의 움직임들을 계산하도록 하는 구조로 바꿀까 생각했지만..

음.. Controller에서 최종적인 MoveInput을 구해 이걸로 조종하는 기존 방식을 그대로 사용하는게 지금 상황에서는 더 맞는거 같아서 구조를 크게 바꾸지 않았다.

 

처음에는 상태 변환 조건을 단순히 플레이어의 속도와 드론의 속도만으로 잡으려했는데, 문제가 너무 많이 발생해서..

상태를 전환할 수 있는 시간과 드론과 플레이어간의 거리도 추가적으로 검토하도록 추가해 상태를 변경해주었다.

그 결과 각 상태가 서로 잘 전환되었다… 으아아!!! 됐다아아아!!! ㅠㅠ

 

3. 상태에 따른 회전 적용하기

Idle일 때는 플레이어가 바라보는 방향을 보면서 회전하도록 만들고, 플레이어를 따라 추적해올때는 목표지점을 바라보면서 오도록 만들고 싶었다.

이전 테스트를 진행했을 때, Idle 상태일때는 문제 없이 구현할 수 있었지만, 플레이어를 따라 추적해 올때는 아쉬운 부분이 있었다.

 

PID와 AddForce로 이동을 구현했기 때문에 원하는 목표지점에 딱 멈추는게 아니라 목표지점에서 좀더 나아갔다가 오차를 점차 줄이는 방식으로 원하는 목표지점에 도달한다.

이 과정에서 목표지점을 지나쳤다가 돌아왔다를 반복하면서, 회전이 너무 부자연스럽게 적용된다는 아쉬운 부분이 있었다.

때문에 DeadZone을 만들어, 목표지점과 드론의 거리가 작을 경우 회전을 적용하지 않도록 구현했다.

 

그 결과 원하는 동작을 얻을 수 있었다.

 

다만.. 아직도 회전이 부자연스러운 부분들이 있는 것 같아서 추후에 조금 손을 봐야할 것 같다.

(지금 당장은.. 프로토타입 완성이 가장 먼저이기 때문이다.)

 

또.. 아쉬운 부분은 Idle 상태의 시작이 이전 TargetLocation이 아니어서 부자연스럽다.. 이것도 수정해야겠다.

 

4. Idle 상태 회전 시작 지점 잡기

회전 시작 지점을 ExecuteTask()에서 계산을 해주도록 추가해주었다.

지금 보니.. ExecuteTask()가 BeginPlay()와 비슷한 느낌의 시점 함수인 것 같다.

 

5. 다양한 Idle 상태 추가하기

< 드론이 신나서 좌우로 흔드는 모습 >

음 통통 튀는게 괜찮은것 같다..

 

확실히 회전까지 시켜주니까 더 보기 좋아진 것 같다.

하지만.. 원하는 정도의 퀄리티가 나온것 같지 않아 아쉬움이 많이 남는다.. ㅠ

 

6. 플레이어에서 드론으로 전환하기

원래는 컨트롤러를 드론 컨트롤러와 플레이어 컨트롤러를 나누어 만들었기 때문에 서로 전환하는게 쉽지 않았다..

때문에 이전 과제를 진행할 때 했던 방식으로 PlayerController에서 2가지 키 맵핑을 가지고, 전환해주는 방식으로 진행했다.

 

처음부터 이렇게 했어도 됐지만.. 컨트롤러 자체를 전환해본적은 없어서 여러가지 시도했는데.. 결국 실패했다.. ㅠ

계속 컨트롤러에 Pawn이 배정이 안되는 문제가 발생해 여기저기서 난리가 났었다..

 

7. 드론에서 플레이어로 전환하기

반대로 드론에서 플레이어로 전환도 잘 이루어지도록 만들었다.

 

이때는 드론에 AI를 다시 넣어주어야 했는데, 이 AI 컨트롤러를 어떻게 관리할지에 대해서도 고민을 많이 했다.

사실 드론으로 플레이하는 시간이 길지 않을 것 같아서.

잠시 빙의만 해제하고 나중에는 이미 만들어져있는 해당 AI컨트롤러에 다시 드론을 빙의시켜주려 했다.

 

하지만, 계속 tick()이 돌고 있는 문제도 있었고.. 위에서 말한 것과 마찬가지로 Pawn이 제대로 전달 되지 않아.. 문제가 많았다.

 

때문에 2번째 방법인 컨트롤러를 삭제했다가 다시 생성하는 방식을 사용해 구현했다.

 

8. 플레이어에서 드론으로 전환시 플레이어 처리하기

으악.. 다 끝낸줄 알았는데, 드론 시점으로 전환하면, 빙의가 풀린 플레이어는 체공상태로 멈추어져있다..

추측하기로는 아마.. 빙의를 풀면 캐릭터 무브먼트나 관련 기능들이 모두 정지하는 것으로 보인다..

때문에 강제로 피직스를 키거나 캐릭터 무브먼트를 등 다양한 시도를 했지만.. 원하는 자연스러움이 안나오기도 하고.. 이게 맞나에 대해 고민을 계속했다.

 

그러다가 생각난게.. 그냥 깡통AI컨트롤러에다가 폰을 빙의시키면.. 동작을 멈췄던 애들이 다시 동작해서 자연스럽게 되지 않을까? 하는 생각이 떠올라서 바로 실행했더니.. 이게 정답이었던것 같다..

여러 어려움이 있었지만 그래도 빙의가 자연스럽게 전환이 잘 된다.

 

이제 빙의 전환시 카메라만 자연스럽게 변경되면 될 것 같다.

ㄴ> 요건 나중에 최후 순위로!

ㄴ> 지금도 완전 부자연스럽지는 않다.

 

3. 마무리


다 마치고, 코드 정리까지 마무리하고, 깃 머지 및 LFS 에셋 충돌 오류를 수정하고 TIL도 다 작성이 끝났따...

또 오늘 2주차 ~ 3주차 작업을 어떤것을 할지, 일정을 좀 더 확실히 잡고 작업에 들어가서 음.. 원하는 모든 것들을 만들 수 있을지는 모르겠지만, 최소한 매끄러운 진행이 되도록은 나올 수 있을 것 같다.

 

아무래도.. 맵을 디테일하게 꾸미거나하는 것은 이번에는 조금 무리일 수 있겠다는 생각도 드는데.. 팀원분들께서 너무 열심히 잘 해주셔서 기능적이나, 어떤 플레이하는데 레벨의 흐름 같은 것은 완성도 있게 나올 수 있을 것 같았다. ㅎㅎ

 

나도 어서 드론 작업을 마치고 다른 작업들을 해야겠다.. ㅎㅎ

 

내일도 화이팅!!

'프로그래밍 > Unreal 부트캠프' 카테고리의 다른 글

TIL 2025.02.26 기록  (0) 2025.02.26
TIL 2025.02.25 기록  (0) 2025.02.25
TIL 2025.02.21 기록  (0) 2025.02.21
TIL 2025.02.20 기록  (0) 2025.02.20
TIL 2025.02.19 기록  (2) 2025.02.19