UE - 부트캠프 과제 5번 풀이
UnrealEngine
최근 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++로도 구현해봤다.
우선 전체적인 구조는 동일하다.
Begin Play
에서 목적지 10개를 만들고, TArray에 넣어준다.EventTick
에서 현재 상태를 검사하고, 10번 움직이지 않았다면Move
함수를 호출해준다.Move
함수는 액터를 이동시키고, 목적지에 도착하면 1초 딜레이 후 랜덤 이벤트 발동 유무 확인하고 다음 목적지로 이동한다.
Begin Play
위에서 말했듯, Begin Play
에선 목적지 10개를 만들고, 배열에 넣어준다.
BP
블루프린트에선 첫 요소에 액터 위치를 넣어준 다음, 그 위치를 기준으로 새로운 위치를 생성한다.
이렇게 배열의 현재 인덱스를 기준으로 새로운 목적지를 생성해준다.
이때, 과제에서 말한 Step
함수로 난수를 만들어 목적지에 더한다.
range
는 이동 범위가 0~1 이기에 눈에 더 잘띄게 하기 위해 곱해준 값이다.
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
함수는
- 선형 보간으로 위치 업데이트
- 이동거리 계산
- 랜덤 이벤트 발생 유무
- 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();
}
CPP
의 Tick
함수다.
여기서도 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
함수는 FMath
의 Lerp
함수를 이용해 선형 보간을 할 수 있다.
그렇게 위치를 갱신하고, 타이머가 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
중 한 쪽만 지원하는 기능들도 꽤 있었고, 텍스트를 작성할 때와 노드를 연결할 때의 가독성/편의성 차이도 있기에 내용이 조금씩 달라지는 것 같다.