언리얼 C++ Actor 랜덤 이동·회전 구현
FMath::VInterpTo·RInterpTo로 순간이동 없애기
Actor를 랜덤 거리·각도로 10회 움직이는 과제. 좌표를 바로 대입하면 순간이동이 되기 때문에 매 프레임 조금씩 다가가는 보간으로 갔다. Tick이 비대해지지 않게 나눈 메서드 구조까지.
Actor를 랜덤 거리·각도로 10회 이동·회전시키는 과제를 구현했다. 좌표를 바로 대입하면 순간이동처럼 보이기 때문에, 목표 지점까지 매 프레임 조금씩 다가가는 보간 방식으로 갔다. 이 글에서는 그 구현을 이야기하려 한다 — 쓰는 언리얼 메서드와 FMath 함수들, 일반 C++과 갈라지는 지점, 그리고 Tick이 비대해지지 않게 메서드를 나눈 구조다. 필수 요구는 매 스텝 현재 좌표를 AddOnScreenDebugMessage로 화면에 찍는 것까지였고, 도전 과제로 세 가지를 더 붙였다.
FMath::VInterpTo/FMath::RInterpTo를 이용한 Lerp(선형 보간 — 목표 지점까지 매 프레임 조금씩 이동시켜 부드럽게 잇는 기법) 이동FMath::FRand()로 50% 확률 이벤트 시스템 (TriggerEvent함수 분리)- 10회 완료 후 총 이동 거리 + 이벤트 발생 횟수 최종 리포트 출력
핵심 언리얼 메서드 정리
| 메서드 | 역할 | 비고 |
|---|---|---|
BeginPlay() | 게임 시작 시 1회 실행 | 초기 위치 설정, 루틴 시작 |
Tick(float DeltaTime) | 매 프레임 실행 | Lerp 보간 처리 위치 |
SetActorLocation(FVector) | Actor 위치를 즉시 설정 | 월드 좌표 기준 |
SetActorRotation(FRotator) | Actor 회전을 즉시 설정 | Yaw(Z축) 기준 |
GetActorLocation() | 현재 위치 반환 | FVector 타입 |
GetActorRotation() | 현재 회전 반환 | FRotator 타입 |
GetActorForwardVector() | 액터 전방 방향 벡터 반환 | 이동 방향 계산에 사용 |
GEngine->AddOnScreenDebugMessage(...) | 화면에 디버그 텍스트 출력 | Key, 시간, 색, 메시지 인자 |
핵심 FMath 라이브러리 정리
| 함수 | 역할 | 예시 |
|---|---|---|
FMath::RandRange(A, B) | A~B 사이 랜덤 값 반환 | RandRange(50.f, 200.f) |
FMath::FRand() | 0.0 ~ 1.0 사이 랜덤 float | FRand() < 0.5f → 50% 확률 |
FMath::VInterpTo(Current, Target, DeltaTime, Speed) | 벡터(위치)를 부드럽게 보간 | Lerp 이동 |
FMath::RInterpTo(Current, Target, DeltaTime, Speed) | 회전값을 부드럽게 보간 | Lerp 회전 |
FVector::Dist(A, B) | 두 벡터 사이 거리 계산 | 총 이동 거리 누적 |
FRotator::NormalizeAxis(Yaw) | 각도를 -180~180 범위로 정규화 | Yaw 누적 시 오버플로 방지 |
언리얼 C++ — 일반 C++과 다른 점
과제를 하면서 일반 C++과 다르게 써야 하는 부분들이 계속 손에 걸렸다.
먼저 타입 시스템이 다르다. 위치는 FVector(X, Y, Z), 회전은 FRotator(Pitch, Yaw, Roll)을 쓰고, 문자열은 std::string이 아니라 FString, 배열은 std::vector가 아니라 TArray<T>를 쓴다. 문자열 리터럴은 유니코드 호환을 위해 반드시 TEXT("...") 매크로로 감싸야 한다 — FString::Printf(TEXT("[Step %d] ..."), Step) 식이다.
디버그 출력도 std::cout이 아니다. 화면에 띄우려면 GEngine->AddOnScreenDebugMessage, Output Log 창에 남기려면 UE_LOG(LogTemp, Log, TEXT("..."))를 쓴다.
클래스 이름 앞에는 접두사가 붙는다. A는 Actor 기반(예: Aassignment_5), U는 UObject 기반, F는 일반 구조체(FVector, FRotator)다. 그리고 UFUNCTION, UPROPERTY 매크로를 선언 위에 붙여야 블루프린트 노출·리플렉션·직렬화가 동작한다. UFUNCTION(BlueprintCallable)이면 블루프린트에서 호출할 수 있고, UPROPERTY(EditAnywhere)면 에디터 Details 패널에서 값을 고칠 수 있다.
구현 구조 (메서드 분리)
RunRandomMoveAndTurn()— 웨이포인트 사전 계산, 보간 시작Move(float Distance)— 전방 방향으로 즉시 이동Turn(float Angle)— Yaw 즉시 회전LogCurrentLocation(int32 Step)— 현재 좌표 화면 출력TriggerEvent(int32 Step)— 50% 확률 이벤트 처리PrintFinalReport()— 총 이동 거리 + 이벤트 횟수 출력Tick()— 매 프레임 Lerp 보간 처리, 웨이포인트 도달 판정
유의점
가장 크게 걸린 건 즉시 이동과 Lerp 이동의 구조 차이였다. 즉시 이동은 for 루프 안에서 SetActorLocation을 바로 호출하면 되지만, 한 프레임 안에 다 끝나 버려서 순간 텔레포트로 보인다. 부드럽게 보이려면 웨이포인트를 미리 계산해 배열에 저장해 두고, Tick에서 매 프레임 보간으로 옮겨야 한다. 반복 이동 로직이 시각적으로 보이려면 게임 루프의 중심인 Tick 안에서 처리해야 한다는 걸 몸으로 확인했다.
Lerp에는 DeltaTime(직전 프레임에서 이번 프레임까지 걸린 시간)을 반드시 넣어야 한다. VInterpTo(..., DeltaTime, Speed)에서 이걸 빠뜨리면 프레임레이트에 따라 이동 속도가 달라진다. 프레임레이트 독립 이동을 위한 필수 인자다.
웨이포인트 도달 판정은 FVector::Dist(NewLoc, TargetLoc) < 1.f처럼 임계값으로 한다. 부동소수점 좌표가 정확히 일치하는 일은 없어서, 값이 너무 작으면 영원히 도착 판정이 안 나고 너무 크면 웨이포인트를 부정확하게 건너뛴다.
Yaw를 단순 누적하면 360도를 넘어가면서 예기치 않은 동작이 생긴다. FRotator::NormalizeAxis(Yaw)로 -180~180 범위를 유지해야 한다. 마지막으로 GEngine은 빌드 설정이나 서버 모드에서 null일 수 있으니, AddOnScreenDebugMessage 호출 전에 널 체크를 해 주는 게 안전하다.
최종 코드
핵심 요약 — 여러 프레임에 걸친 이동은 for 루프가 아니라 Tick에서 보간으로 처리해야 눈에 보인다는 것, 그리고 보간에 DeltaTime을 넣어야 프레임레이트와 무관하게 같은 속도가 나온다는 것을 배웠다.