언리얼 런타임 SplineMeshComponent 동적 생성: 호출 순서·탄젠트 정규화·핫리로드 함정

기존 지형 위에 스플라인만 깔아 자율주행 경로로 쓰다 보니, 도로 메시가 없어 차량이 지형 굴곡을 타고 튀는 문제가 있었다.

스플라인을 따라 도로 메시를 코드로 생성하면서 마주친 세 가지 함정을 정리한다.

ASplineMeshActor 상속 대신 AActor 유지

처음에는 ASplineMeshActor를 상속하려 했다.

하지만 ASplineMeshActor는 USplineMeshComponent를 가진 액터라서, 이걸 상속하면 기존에 USplineComponent 기반으로 짜둔 자율주행 코드가 깨진다.

그 코드가 기대하는 함수와 구성이 사라지기 때문이다.

그래서 AActor 상속을 유지한 채 USplineComponent와 USplineMeshComponent들을 런타임에 동적 생성하는 방향을 택했다. 기존 코드의 전제를 깨지 않으려면 부모 클래스를 바꾸기보다 컴포넌트를 더하는 쪽이 안전하다고 판단했다.

함정 1: 호출 순서 — 설정을 끝낸 뒤에 부착·등록

메시가 스플라인과 어긋난 위치에 생성됐다.

원인은 SetupAttachment와 RegisterComponent를 너무 일찍 호출한 것이었다.

부착·등록을 먼저 하면 그 시점의 잘못된 상태로 메시가 등록된다.

USplineMeshComponent* SMC = NewObject<USplineMeshComponent>(...);
SMC->SetMobility(EComponentMobility::Movable);
SMC->AttachToComponent(SplineComponent, ...);
SMC->SetForwardAxis(ForwardAxis, false);
SMC->SetStaticMesh(RoadMesh);
SMC->SetStartAndEnd(StartPos, StartTan, EndPos, EndTan, true);
SMC->RegisterComponent();

 

이 프로젝트에서는 SetForwardAxis → SetStaticMesh → SetStartAndEnd 순으로 모든 설정을 끝낸 다음 마지막에 RegisterComponent를 호출하는 것으로 정리했다.

메시 적용 전에 ForwardAxis를 정하고, 등록은 가장 마지막이라는 순서가 핵심이었다.

함정 2: 탄젠트 길이를 세그먼트 실제 길이에 맞추기

위치는 맞췄는데 곡선 구간이 직선처럼 보였다.

스플라인에서 가져온 탄젠트의 길이가 세그먼트 실제 길이와 맞지 않으면 메시가 출렁이거나 직선이 된다.

StartTan = StartTan.GetSafeNormal() * ActualSegLen;
EndTan   = EndTan.GetSafeNormal()   * ActualSegLen;
SMC->SetStartAndEnd(StartPos, StartTan, EndPos, EndTan, true);

 

탄젠트를 정규화한 뒤 세그먼트 실제 길이를 곱해 길이를 맞추면 곡선이 자연스럽게 표현된다.

스플라인 메시의 곡률 표현은 탄젠트의 방향뿐 아니라 길이에도 의존한다는 점이 포인트다.

함정 3: 핫리로드 후에는 배열 추적이 믿을 수 없다

같은 위치에 메시가 여러 층으로 누적됐다. 기존 정리 코드는 추적용 배열을 순회하며 메시를 파괴했는데,

for (USplineMeshComponent* SMC : SplineMeshes)
{
    SMC->DestroyComponent();
}
SplineMeshes.Reset();

 

핫리로드 후에는 SplineMeshes 배열은 초기화되지만 액터에 실제로 붙어 있는 메시 컴포넌트는 그대로 남는다.

그래서 배열을 돌아도 아무것도 안 지워지고 새 메시만 계속 쌓였다.

TArray<USplineMeshComponent*> ExistingMeshes;
GetComponents<USplineMeshComponent>(ExistingMeshes);
for (USplineMeshComponent* SMC : ExistingMeshes)
{
    if (SMC) SMC->DestroyComponent();
}
SplineMeshes.Reset();

 

GetComponents는 액터에 실제로 붙어 있는 컴포넌트를 직접 가져오므로 핫리로드 후에도 안정적이었다.

자체 배열로 추적한 것만 정리하는 방식이 항상 안전하지는 않다는 걸 이 과정에서 확인했다.

+ Recent posts