Post

UE - 부트캠프 과제 5번 풀이

UnrealEngine

UE - 부트캠프 과제 5번 풀이

최근 C++만 한 것 같아서 부트캠프 과제할 겸 언리얼 엔진을 복습해봤다.

우선 과제 내용부터 보자.

필수 기능 가이드

새로운 엑터를 생성하고 엑터가 spawn 되는 시점에 동작되도록 아래 요구사항대로 동작하도록 코드를 구현 합니다

시작점(0,0) 있는 게임 캐릭터가 랜덤하게 10회 이동 합니다. 각 스텝에서 거쳐간 좌표를 모두 출력하는게 목적입니다.

세부 요구사항은 아래와 같습니다.

  • 시작점은 (0,0)이고 한번 이동시 x좌표와 y좌표 모두 2이상 이동할 수 없습니다. 예를 들면 아래와 같습니다.
    • (0,0) 에서 (1,2)은 이동할수 없습니다. y좌표가 2이상 이동했기 때문입니다.
    • (0,0)에서 (1,1)은 이동할 수 있습니다. x좌표 y좌표 모두 2미만 이동했기 때문 입니다.
  • 이동은 입력을 받는게 아니고 10회 모두 랜덤으로 움직입니다.
  • 매번 이동시 현재 좌표를 출력할 수 있어야 합니다.
  • 로그 출력은 UE_LOG를 활용합니다.
  • step 함수는 x좌표 y좌표 각각 이동할 거리 입니다.
    • 예를들어서 현재 좌표가(x1,y1)이라면 다음 좌표는 (x1+step 함수의 리턴값,y1 + step함수의 리턴 값) 입니다.
    • step함수는 0혹은 1을 랜덤으로 반환 합니다.
  • move함수는 (0,0)부터 10회 움직이면서 좌표를 출력합니다. 이동시 step 함수가 활용 됩니다.

도전 기능 가이드

필수 기능 구현을 완료한 후 아래 기능을 추가 힙니다.

  • 10회 이동시 각 스텝마다 이전 좌표기준 이동 거리를 계산해서 출력 합니다. 이동 거리는 아래와 같이 계산 합니다.
  • 10회 이동시 각 스텝마다, 50% 확률로 랜덤하게 이벤트가 발생합니다.(발생 시키는 부분도 구현하셔야 합니다.) 각 스텝마다 이벤트 발생여부를 출력합니다.
  • 10회 이동후에는 총 이동거리와 총 이벤트 발생횟수를 출력 합니다.

이런 내용이다. 난수 생성, 로그 출력에 대해 공부하라고 준 과제같다.

언리얼에 대해선 초보자다보니, 과제를 블루프린트로 구현해보고 C++로도 구현해봤다.

우선 전체적인 구조는 동일하다.

  1. Begin Play에서 목적지 10개를 만들고, TArray에 넣어준다.
  2. EventTick에서 현재 상태를 검사하고, 10번 움직이지 않았다면 Move 함수를 호출해준다.
    • Move 함수는 액터를 이동시키고, 목적지에 도착하면 1초 딜레이 후 랜덤 이벤트 발동 유무 확인하고 다음 목적지로 이동한다.

Begin Play

위에서 말했듯, Begin Play에선 목적지 10개를 만들고, 배열에 넣어준다.

BP

블루프린트에선 첫 요소에 액터 위치를 넣어준 다음, 그 위치를 기준으로 새로운 위치를 생성한다.

이렇게 배열의 현재 인덱스를 기준으로 새로운 목적지를 생성해준다.

이때, 과제에서 말한 Step 함수로 난수를 만들어 목적지에 더한다.

range는 이동 범위가 0~1 이기에 눈에 더 잘띄게 하기 위해 곱해준 값이다.

Step 함수 내부

Random Integer in Range 노드를 사용하면 쉽게 난수를 생성할 수 있다.

생성이 완료되었으면 startLocation 변수에 destinations의 0번 값을 넣어주고, destination에 1번 값을 넣어줬다.

CPP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void AMyActor::BeginPlay()
{
	Super::BeginPlay();

	destinations.Push(GetActorLocation());
	UE_LOG(LogTemp, Warning, TEXT("Actor Location: %.1f, %.1f, %.1f"), destinations[0].X, destinations[0].Y, destinations[0].Z);

	for (int i = 0; i < 10; i++)
	{
		FVector destination = destinations[i];
		
		destination.X += Step() * range;
		destination.Y += Step() * range;

		destinations.Push(destination);
	}

	curIndex = 0;
	eventCallCount = 0;
	stop = false;
}

CPP도 로직은 동일하다.

배열에다 현재 목적지 + Step() * range 값을 넣어줬다.

다른점이 있다면, 블루프린트에선 현재 위치와 다음 목적지를 저장하는 변수를 따로 생성했다.

만약 변수 없이 인덱스로 값을 가져와야 한다면, 이런식으로 가져와야하기에 너무 난잡해진다.

하지만 CPP에선 destinations[curIndex]와 ‘destinations[curIndex + 1]’로 간단하게 접근이 가능하기에 두 변수를 생성하지 않았다.

Event Tick/Move

Event Tick에선 현재 상태를 검사하고, Move 함수를 호출할지 결정한다.

BP

stop 불리언 변수를 통해 현재 동작 중인지 확인하고, Move 함수를 호출한다.

이동을 자연스럽게 표현하기 위해 Move에서 선형보간을 이용해 위치를 업데이트한다.

Timer는 그 때 사용하기 위해 Delat Seconds를 매 프레임마다 더해준다.

Move 함수는

  1. 선형 보간으로 위치 업데이트
  2. 이동거리 계산
  3. 랜덤 이벤트 발생 유무
  4. 10번 이동 유무에 따라 상태 처리

이렇게 4가지 단계로 이루어진다.

선형 보간을 위해 Move 가 시작되기 전 첫 위치와 목적지를 선형보간 해줬다.

Timer를 사용해 비율을 조정했기에 두 위치의 거리가 얼마나 차이나든 1초에 목적지에 도착한다.

1초가 지났으면, 정확한 목적지에 위치하기 위해 액터의 위치를 목적지와 동일하게 만들어준다.

이 오차범위 수정을 진행할 때는 이미 목적지에 근사한 위치에 있기에 눈으로 봤을 때 티가 나지 않는다.

목적지에 도착했으니, 거리를 계산해준다.

두 위치간의 거리는 단순히 두 벡터의 차의 크기를 구하면 된다.

이건 함수가 아닌 매크로로 구현해봤다.

그렇게 구한 거리를 totalDistance 에 합해준다.

이제 랜덤 이벤트를 발생시킬지 결정한다.

Step 함수가 0과 1을 반환하니 이걸 true false로 이용했다.

이벤트는 Add Custom Event를 사용해 만들어줬다.

이벤트 발생 유무까지 정하고 딜레이를 줘 잠깐 동작을 멈춘다.

Delay 노드로 잠깐의 딜레이를 줄 수 있다.

마지막으로 10번 이동했는지 검사하고 처리하는 부분이다.

10번 검사는 단순히 현재 인덱스가 9번 이상인지를 기준으로 정한다.

만약 10번 이하면 ‘Update Start and Destination’ (Begin Play에서 사용한 함수)로 시작점과 목적지를 업데이트 해준다.

10번 이동이 다 완료되었다면 End Event 를 실행한다.

여기선 총 이동거리와 총 이벤트 발생 횟수를 출력하고, 정지상태가 된다.

BP에서 로그 메시지를 출력할 때는 Print String 노드를 이용하면 되며, 뷰표트 좌측 상단에 표시되어 잘 보인다.

CPP

1
2
3
4
5
6
7
8
void AMyActor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	if (stop) return;
	timer += DeltaTime;
	Move();
}

CPPTick 함수다.

여기서도 stop으로 상태를 검사하고, 선형보간을 위한 timer 계산 후 Move함수를 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void AMyActor::Move()
{
	FVector curLocation = FMath::Lerp(destinations[curIndex], destinations[curIndex+1], timer);
	SetActorLocation(curLocation);

	if (timer >= 1.f)
	{
		CalculateDistanceAndLocation();
		CreateRandomEvent();

		if (curIndex < destinations.Num() - 2)
		{
			curIndex++;
			timer = 0.0f;
			
			stop = true;

			GetWorld()->GetTimerManager().SetTimer(TimerHandle, 
				FTimerDelegate::CreateLambda([this]()
				{
						stop = false;
				}), 1.0f, false);
		}
		else
		{
			EndEvent();
		}
	}
}

Move 함수는 FMathLerp 함수를 이용해 선형 보간을 할 수 있다.

그렇게 위치를 갱신하고, 타이머가 1초 이상인 순간 거리와 위치를 계산한다.

거리 계산이 끝나면 이벤트 처리를 실행하고, 그 이후 10번 이동했는지 검사하고 처리한다.

1
2
3
4
5
6
7
8
9
10
11
12
void AMyActor::CalculateDistanceAndLocation()
{
	float distance = (destinations[curIndex + 1] - destinations[curIndex]).Length() / range;

	UE_LOG(LogTemp, Log, TEXT("Distance: %.2f"), distance);

	totalDistance += distance;

	FVector curLocation = GetActorLocation() / range;

	UE_LOG(LogTemp, Log, TEXT("Actor Location: %.1f, %.1f, %.1f"), curLocation.X, curLocation.Y, curLocation.Z);
}

아까 말했듯, 거리는 두 위치 벡터의 차의 크기와 같다.

여기도 이동을 잘 보이게 하기 위해 rage를 곱해줬으므로, / range를 수행한다.

curLocation은 현재 위치를 저장하고 이걸 로그로로 출력한다.

사실 블루프린트에서도 현재 위치를 출력했어야 했는데, 깜빡하고 구현하지 못했다.

거리까지 구했으니 랜덤 이벤트 차례다.

1
2
3
4
5
6
7
8
9
10
11
12
void AMyActor::CreateRandomEvent()
{
	if (Step())
	{
		UE_LOG(LogTemp, Log, TEXT("Event triggered successfully."));
		eventCallCount++;
	}
	else
	{
		UE_LOG(LogTemp, Log, TEXT("Event not triggered."));
	}
}

여기서도 Step 을 이용해 이벤트 발생 여부를 로그로 출력한다.

처음에는 함수 포인터로 이벤트를 진짜로 만들까 했지만 이 이벤트를 다른 곳에서 실행하지 않아서 그만뒀다.

또한 언리얼에선 함수 포인터를 사용하지 않고 Deligate 를 사용하여 이벤트를 구현하는 것이 일반적이라고 하다.

다시 Move 함수로 돌아와서

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (curIndex < destinations.Num() - 2)
		{
			curIndex++;
			timer = 0.0f;
			
			stop = true;

			GetWorld()->GetTimerManager().SetTimer(TimerHandle, 
				FTimerDelegate::CreateLambda([this]()
				{
						stop = false;
				}), 1.0f, false);
		}
		else
		{
			EndEvent();
		}

여기가 10번 이동 유무 검사 부분이다.

블루프린트와 동일하게 curIndex로 검사했고, 10번 미만이면 딜레이를 걸어준다.

CPP에선 블루프린트 노드인 Delay를 사용할 수 없기에, SetTimer 함수를 사용했다.

SetTimer는 사용자가 타이머를 설정하고, 타이머의 시간이 다 지나면 특정 함수를 실행하는 것이 가능하다.

원래대로면

1
2
3
4
void AMyActor::Play()
{
  stop = false;
}

이런 함수를 생성해 매개변수로 넣어줬겠지만,

마침 어제 람다 함수를 공부했으니 람다로 구현해봤다.

어제도 말했듯 [this] 일 때, this는 const라 수정이 불가능하지만, this의 멤버 변수인 pause는 수정이 가능하다.

마지막으로 10번 동작이 끝났을 때는 EndEvent 함수를 호출한다.

1
2
3
4
5
6
7
8
void AMyActor::EndEvent()
{
	UE_LOG(LogTemp, Warning, TEXT("Total Distance: %.2f"), totalDistance);

	UE_LOG(LogTemp, Warning, TEXT("Event Call Count: %d"), eventCallCount);

	stop = true;
}

이 함수에서 최종 거리와 이벤트 실행 횟수를 로그에 표시한다.

CPP에선 Print String 노드를 사용하지 못하기도 하고, 과제에서도 UE_LOG를 이용해 로그를 출력하라고 했기에 사용했다.

UE_LOG로 출력된 메시지들은 Print String처럼 뷰포트 좌측 상단에 표시되지 않고, 출력 로그창에 표시된다.

최종 결과물

BP 결과

CPP 결과

둘 다 사용해보니, 흐름은 비슷해도 구현 방법이 조금씩 달라진다는 걸 느꼈다.

블루프린트나 CPP 중 한 쪽만 지원하는 기능들도 꽤 있었고, 텍스트를 작성할 때와 노드를 연결할 때의 가독성/편의성 차이도 있기에 내용이 조금씩 달라지는 것 같다.

This post is licensed under CC BY 4.0 by the author.