2026-05-08 TIL (53일차)
팀원 SplineFollowerComponent 분석
- ARoadActor : SplineComponent(선)를 가지고 있는 Actor
ARoadActor가 도로에 깔리는데 그 선을 따라 이동하기 때문에 그 선의 데이터를 제공해주는 역할
주요 변수 (Variables)
TargetRoad
FindBestRoadActor 함수 결과에 나온 ARoadActor를 가리키는 Weak포인터
OwnerPawn
조종되는 자동차(Pawn)를 저장할 Weak포인터
CurrentDistance
TargetRoad가 가지고있는 SplineComponent의 시작점 부터 OwnerPawn까지의 거리
- 트랙 중 어느 지점에 있는지 위치를 파악하여, 그 지점에서부터 자율주행을 이어가기 위한 기준점(거리 값) 역할을 합니다.
함수
BeginPlay
OwnerPawn, TargetRoad, CurrentDistance, SmoothedTargetSpeed 값을 할당해
FindBestRoadActor
- 차량 위치 파악: 현재 자동차(
OwnerPawn)의 월드 좌표(VehicleLoc)를 가져옵니다. - 최단 거리 변수 초기화: 검색 허용 범위인
SearchRadius를 초기값으로 설정하고, 가장 가까운 도로를 담을Best변수를 선언합니다. - 월드 도로 전수 조사 (Loop):
TActorIterator를 사용하여 월드에 배치된 모든ARoadActor를 하나씩 꺼내며 아래 과정을 반복합니다.- 3-1. 자율주행 전용 확인: 도로 액터의
bUseForAutopilot변수가true인지 확인합니다. (자율주행용으로 설계된 도로만 골라내는 안전장치) - 3-2. 스플라인 컴포넌트 유효성 검사: 해당 도로가 실제 곡선 데이터(
SplineComponent)를 가지고 있는지 확인합니다. - 3-3. 최단 지점 추적: 도로 선(Spline) 위에서 내 차와 가장 가까운 지점의 좌표(
ClosestOnSpline)를 찾습니다. - 3-4. 직선 거리 계산: 내 차의 위치와 도로 위 최단 지점 사이의 거리(
Dist, 단위: cm)를 계산합니다. - 3-5. 최단 거리 갱신: 계산된 거리(
Dist)가 현재까지 찾은 최소 거리(BestDist)보다 짧다면, 해당 거리를 새로운 1등으로 기록하고 해당 도로 액터를Best에 저장합니다.
- 3-1. 자율주행 전용 확인: 도로 액터의
- 결과 반환: 모든 도로를 검사한 후, 최종적으로 선택된 가장 가까운 도로 액터를 반환합니다. 주변에 도로가 없다면
nullptr를 반환합니다.
FindLocationClosestToWorldLocation
스플라인 상에서 특정 월드 좌표와 가장 가까운 지점의 좌표를 계산해주는 언리얼 엔진의 내장 함수입니다.
매개변수 (Parameters)
WorldLocation(FVector): 기준이 되는 월드 좌표입니다. (코드에서는 내 차량의 위치VehicleLoc가 들어갑니다.)CoordinateSpace(ESplineCoordinateSpace): 반환받을 좌표를 무엇으로 받을지 정합니다.
World: 게임 월드의 절대 좌표로 반환합니다.Local: 해당 스플라인 액터의 로컬 좌표 기준으로 반환합니다.내부 실행 원리 (알고리즘) 이 함수는 수학적으로 다음과 같은 과정을 거쳐 결과를 도출합니다.
- 세그먼트 분할: 스플라인 곡선을 수많은 작은 구간(Segment)으로 나눕니다.
- 투영(Projection): 입력받은 월드 좌표에서 각 구간에 수직으로 선을 내려 가장 가까운 지점(Perpendicular Point)을 찾습니다.
- 최적화 계산: 뉴턴 방법(Newton’s Method) 등의 수치 해석 기법을 사용하여, 곡선 방정식 위에서 입력 좌표와의 거리가 최소가 되는 파라미터($t$) 값을 찾아내어 최종 좌표를 반환합니다.
GetSpline
FindBestRoadActor로 찾은 ARoadActor에 있는 SplineComponent를 가져오는 함수.
ComputeStartDistance
- 도로 데이터(Spline) 확보 및 검사: TargetRoad의 SplineComponent를 가져옵니다. 만약 도로 데이터가 없다면(유효하지 않다면) 에러가 나지 않도록 즉시
0.f를 반환하여 함수를 종료합니다. - 최단 지점의 인덱스(InputKey) 탐색:
FindInputKeyClosestToWorldLocation함수를 사용하여, 입력받은 차량의 현재 위치(VehicleLocation)와 가장 가까운 도로 위의 지점을 찾습니다.- 이때 반환되는 값은 3D 좌표(X, Y, Z)가 아니라, 해당 지점의 고유 번호표인
InputKey입니다.
- 이때 반환되는 값은 3D 좌표(X, Y, Z)가 아니라, 해당 지점의 고유 번호표인
- 실제 누적 거리(Distance)로 변환: 방금 찾은 번호표(
InputKey)를GetDistanceAlongSplineAtSplineInputKey함수에 넣습니다.- 이 함수는 도로의 맨 처음 시작점에서부터 해당 번호표 지점까지 도로 선을 따라 쭉 걸어왔을 때의 실제 곡선 이동 거리(누적 길이, 단위: cm)로 변환해 줍니다.
- 결과 반환: 최종적으로 계산된 누적 시작 거리를 반환합니다. 자율주행 시스템은 이 거리를 기준으로 “아, 우리 차가 전체 트랙 중 300m 지점에 서 있구나! 여기서부터 주행을 시작하자”라고 판단하게 됩니다.
FindInputKeyClosestToWorldLocation
특정 월드 좌표에서 스플라인 곡선 상의 가장 가까운 지점을 찾고, 그 지점의 좌표가 아닌 InputKey(고유 번호표)를 반환하는 함수입니다.
매개변수 (Parameters)
WorldLocation(FVector): 기준이 되는 월드 좌표입니다.내부 실행 원리 (알고리즘)
- 최단 거리 탐색:
FindLocationClosestToWorldLocation과 동일한 수학적 방식(세그먼트 분할 및 투영)을 사용하여 가장 가까운 지점을 찾습니다.- 파라미터 변환 (InputKey 추출): 찾아낸 지점의 실제 좌표(X, Y, Z)를 계산하는 대신, 해당 지점이 스플라인 구조 내에서 몇 번째 제어점(Control Point) 사이에 있는지를 나타내는 실수형 매개변수 값(InputKey)을 도출합니다.
- 예시: 0번 점과 1번 점의 정확히 중간이라면
0.5, 2번 점과 3번 점 사이의 1/4 지점이라면2.25를 반환합니다.
GetDistanceAlongSplineAtSplineInputKey
스플라인의 상대적 위치 값인
InputKey를 입력받아, 도로의 맨 처음 시작점에서부터 해당 지점까지의 실제 물리적 누적 거리(Distance, cm)를 계산하여 반환하는 함수입니다.매개변수 (Parameters)
InKey(float): 실제 거리로 변환하고자 하는 스플라인의 InputKey 값입니다. (주로 위FindInputKeyClosestToWorldLocation에서 구한 값을 넣습니다.)내부 실행 원리 (알고리즘)
- 구간 식별 및 참조: 입력받은 InputKey의 정수 부분을 통해 스플라인의 몇 번째 세그먼트(구간)인지 파악하고, 소수점 부분을 통해 구간 내의 어느 지점인지 비율을 확인합니다.
- 곡선 길이 적분 (Arc Length Integration): 직선거리가 아닌 구불구불한 곡선의 실제 길이를 구하기 위해 수학적 계산을 수행합니다.
- 언리얼 엔진은 성능 최적화를 위해 스플라인을 생성할 때 미리 거리 데이터 테이블(Distance Table)을 캐싱(저장)해 둡니다.
- 테이블의 데이터와 베지어(Bezier) 또는 캣멀-롬(Catmull-Rom) 곡선 방정식을 결합 및 보간(Interpolation)하여, 시작점부터 해당 지점까지 줄자로 잰 듯한 실제 호의 길이(Arc Length)를 정밀하게 산출해 반환합니다.
TickComponent
매 프레임마다 안전 검사 및 차량의 현재 상태(위치, 속도, 방향)를 받아온 후, 자율주행 파이프라인을 순서대로 실행합니다.
UpdateProgressDistance (매개변수 VehicleSpeed는 없어도 되는거 같은데?)
매 프레임마다 도로 위에서 차량이 얼마나 전진했는지 누적 거리를 갱신하고, 최종 목적지에 도달했는지를 판단하여 반환(bool)하는 함수입니다.
- 도로 데이터 유효성 검사: 현재 자율주행 중인 도로 데이터(
Spline)를 정상적으로 참조하고 있는지 확인합니다. 만약 도로가 없다면 에러 방지를 위해 즉시true를 반환하여 주행을 종료(도착으로 간주)시킵니다. - 현재 누적 거리(CurrentDistance) 갱신: 차량의 현재 진행 상태를 최신화합니다.
- 2-1. 최단 지점 번호표(InputKey) 추적: 차량의 현재 위치(
VehicleLocation)에서 도로 선상에 가장 가까운 지점의InputKey를 찾아냅니다. (FindInputKeyClosestToWorldLocation) - 2-2. 누적 이동 거리 변환 및 덮어쓰기: 찾아낸 번호표를 실제 물리적 누적 길이(cm)로 변환하여
CurrentDistance에 저장합니다.(GetDistanceAlongSplineAtSplineInputKey) - -> 매 프레임마다 이를 갱신하기 때문에, 차가 충돌로 인해 경로를 이탈하더라도 가장 가까운 도로 위치를 다시 잡아내어 주행을 복구할 수 있습니다.
- 2-1. 최단 지점 번호표(InputKey) 추적: 차량의 현재 위치(
- 순환 도로(루프 트랙) 예외 처리: 해당 도로가 트랙처럼 무한히 연결된 순환 도로(
IsClosedLoop())인지 확인합니다. 순환 도로라면 ‘끝(도착)’이라는 개념이 없으므로 항상false를 반환하여 차량이 영구적으로 달리게 만듭니다. - 최종 도착 여부 판단 및 반환 (일반 도로): 끝이 있는 일반 도로일 경우, 도착 지점에 다다랐는지 계산합니다.
- 4-1. 남은 거리 계산: 도로의 총길이(
TotalLength)를 가져온 뒤, 현재까지 달려온 누적 거리(CurrentDistance)를 빼서 ‘목적지까지 남은 거리’를 구합니다. - 4-2. 도착 판정: 그 남은 거리가 미리 설정해 둔 도착 허용 오차 거리(
EndOfPathThreshold)보다 작아졌다면 목적지에 도착했다고 판단하여true를 반환합니다. 반대로 아직 거리가 많이 남았다면false를 반환하여 주행 파이프라인이 계속 돌게 합니다.
- 4-1. 남은 거리 계산: 도로의 총길이(
HandlePathCompleted
UpdateProgressDistance값이 true를 반환하게 되면 실행되는 함수, 주행하는 자동차 폰을 멈추게 하고 ComponentTick도 꺼주는 함수
SampleCurvatures
현재 내 자동차가 있는 위치의 도로가 얼마나 꺾였는지(각도)를 EstimateCurvature 사용해 계산해 저장해주고 내 자동차보다 지정한 값의 거리만큼 앞선 도로가 얼마나 꺾였는지를 EstimateCurvature을 계산해 저장합니다.
[ SampleCurvatures 내부에서 호출되는 하위 로직들 ]
EstimateCurvature
특정 지점을 기준으로, 도로가 앞으로 얼마나 심하게 꺾여 있는지(곡률, Curvature)를 실제 각도(라디안)로 계산하여 반환하는 함수입니다.
1. 두 지점의 방향 벡터 구하기 (GetDirectionAtDistance 함수 사용)
D1(시작 방향): 차량의 현재 위치(또는 앞선 기준점)에서 도로가 뻗어 나가는 정면 방향(단위 벡터)을 구합니다.D2(조금 앞선 방향):D1지점에서CurvatureSampleSpan(샘플링 간격)만큼 조금 더 앞으로 이동한 지점의 정면 방향(단위 벡터)을 구합니다.- 비유하자면, 코너 진입 직전의 자동차가 바라보는 방향(D1)과 코너 중간에서 바라보는 방향(D2)을 가져오는 것입니다.
2. 두 화살표의 내적 (Dot Product)
FVector::DotProduct(D1, D2)를 사용하여 두 방향 화살표를 내적합니다.
3. 부동소수점 오차 방지 (Clamp)
FMath::Clamp(..., -1.f, 1.f)를 사용하여 내적 결과값이 무조건 -1.0과 1.0 사이를 유지하도록 묶어둡니다. (안전장치)
4. 각도 복원 (Acos)
FMath::Acos(...)함수를 씌워 최종적으로 두 화살표 사이의 실제 ‘각도(라디안)’를 추출해 반환합니다. 이 각도가 클수록 도로가 급격하게 휘어졌다는 뜻입니다.
FMath::Acos를 사용할까? 이 원리를 이해하려면 벡터의 내적 공식을 떠올려야 합니다. 수학적으로 두 벡터 A와 B의 내적 공식은 다음과 같습니다.
**A · B = A × B × cos(θ)** (θ는 두 벡터 사이의 각도)
여기서 우리가 가져온 D1과 D2는 언리얼 엔진의 스플라인에서 추출한 단위 벡터(Unit Vector)입니다. 단위 벡터는 길이가 무조건 ‘1’입니다. 즉, |D1| = 1 이고, |D2| = 1 입니다. 이것을 공식에 대입하면 놀라운 결과가 나옵니다.
D1 · D2 = 1 × 1 × cos(θ) = cos(θ)
결론적으로, 길이가 1인 두 방향 화살표를 내적하면 그 결과값은 순수한 cos(θ) 값이 됩니다!
Acos(역코사인) 우리가 최종적으로 알고 싶은 것은 cos(θ) 값이 아니라, 실제로 도로가 꺾인 순수한 각도 θ (세타)입니다. 수학에서 코사인 함수의 반대 역할을 하는 것이 바로 역코사인(Arccosine, C++에서는 Acos)입니다.
Acos( cos(θ) ) = θ
따라서 내적한 결과값에 FMath::Acos를 씌워주면 코사인의 껍질이 벗겨지고, 두 방향 화살표가 이루는 실제 각도(라디안)만 튀어나오게 됩니다.
Clamp를 함께 쓰는 이유 수학적으로 cos(θ) 값은 절대로 -1과 1을 벗어날 수 없습니다. 하지만 컴퓨터의 미세한 소수점 계산 오차 때문에 아주 가끔 1.0000001 같은 값이 나올 수 있습니다. 만약 Acos 함수에 1.0을 초과하는 값을 넣으면 연산이 불가능해져 게임이 터지거나 NaN(Not a Number) 에러를 뿜어냅니다. 이를 방지하기 위해 Acos에 값을 넣기 직전, Clamp를 통해 값을 -1.0 ~ 1.0 사이로 안전하게 잘라주는 것입니다.
GetDirectionAtDistance (EstimateCurvature 내부 사용)
도로(스플라인)를 따라 특정 거리만큼 이동했을 때, “그 지점에서 도로가 정확히 어느 방향으로 뻗어 있는지(정면 방향)”를 계산해서 화살표(방향 벡터=단위 벡터)로 알려주는 함수입니다. EstimateCurvature(곡률 계산) 함수에서 도로가 꺾인 각도를 재기 위해 두 지점의 방향 화살표를 가져올 때 바로 이 함수가 사용됩니다.
1. 도로 데이터 검사 (안전장치)
- 실행 원리: 현재 소유한 도로(스플라인) 데이터가 정상적으로 존재하는지 확인합니다.
2. 거리 보정 (WrapDistance 실행)
- 실행 원리:
WrapDistance함수를 호출합니다. 입력받은 목표 거리(Distance)가 도로 범위를 벗어났을 경우, 이 거리를 순환(루프)시키거나 끝점에 가두어(클램프) ‘안전하고 유효한 거리 값(WrappedDist)’으로 보정합니다.
3. 방향 벡터(접선) 추출 및 반환
- 실행 원리: 언리얼 엔진의 내장 함수를 사용하여, 보정된 안전한 거리(
WrappedDist) 지점에서 도로가 향하는 방향을 추출합니다. - 이때
ESplineCoordinateSpace::World를 사용하여, 로컬 기준이 아닌 실제 게임 월드 상의 절대적인 3D 방향(화살표)을 반환하게 됩니다.
WrapDistance (GetDirectionAtDistance 내부 보정용)
계산된 목표 거리(Distance)가 도로의 범위를 벗어나지 않도록, 도로의 성격(순환형/직선형)에 맞춰 안전한 유효 거리 값으로 변환(보정)해 주는 함수입니다.
1. 도로 데이터 및 총길이 확보
GetSpline()으로 도로 데이터를 가져옵니다.Spline->GetSplineLength()를 통해 현재 도로의 맨 처음부터 끝까지의 전체 길이(Length)를 구합니다.
2. 도로 형태 확인 (순환로 vs 일반 도로)
IsClosedLoop()를 호출하여 이 도로가 서킷처럼 시작과 끝이 이어진 순환 도로인지, 아니면 끝이 있는 일반 도로인지 판별하여 로직을 나눕니다.
3. 순환 도로 (Closed Loop) 일 경우
- 실행 원리: 트랙을 한 바퀴(예: 1000m) 다 돌고 나서 1050m 지점을 조회하면, 다시 50m 지점으로 인식하도록 만들어야 합니다.
- FMath::Fmod(FMath::Fmod(Distance, Length) + Length, Length) 공식 원리
- 1단계: 첫번째 매개변수 FMath::Fmod(Distance, Length) + Length 예를 들어 차가 한 바퀴를 넘어 1200m를 달렸다면, 1200 fmod 1000 = 200입니다. 하지만 200m 후진했다면(-200)? C++의 나머지 연산은 -200 fmod 1000 = -200이라는 음수를 내보냅니다. 음수 문제를 해결하기 위해 방금 구한 값에 트랙 한 바퀴 길이(1000)를 더해줍니다 음수였던 경우: -200 + 1000 = 800. 드디어 우리가 원하던 800m 좌표가 나옵니다. 정상 작동했던 양수는 200 + 1000 = 1200. 양수였던 애들은 트랙 길이를 한 번 더 더해버렸기 때문에 다시 1000을 넘습니다.
- 2단계: FMath::Fmod(FMath::Fmod(Distance, Length) + Length, Length) 그래서 마지막으로 전체 값에 다시 한번 나머지 연산을 해줍니다. 넘쳤던 값이 다시 예쁘게 200으로 돌아옵니다.
4. 일반 도로 (Open Path) 일 경우
- 실행 원리: 일반 도로는 끝을 넘어가면 낭떠러지이거나 길이 없으므로, 값이 범위를 벗어나지 못하게 가둬야(
Clamp) 합니다. Clamp(값, 최소, 최대)란?- 이름 그대로 “값이 최소~최대 범위를 벗어나지 못하게 묶어두는 함수”입니다.
- 사용 이유: 일반 도로는 끝을 지나치면 길이 없습니다. 허공을 참조하려고 하면 게임이 에러를 발생하거나 차가 맵 밖으로 추락합니다.
- 따라서 “스플라인의 맨 끝점(혹은 맨 시작점)에 차를 안전하게 잡아두기 위한” 필수 방어 용도입니다.
GetDirectionAtDistanceAlongSpline (GetDirectionAtDistance에서 사용)
스플라인 곡선을 따라 특정 거리만큼 이동한 지점에서, 곡선이 향하는 정면 방향(접선 단위 벡터= 방향 벡터)을 계산하여 반환하는 언리얼 엔진의 내장 함수입니다.
매개변수 (Parameters)
Distance(float): 도로(스플라인)의 시작점에서부터 곡선을 따라 이동한 실제 물리적 누적 거리(cm)입니다. (작성하신 코드에서는 범위를 안전하게 보정한WrappedDist가 들어갑니다.)CoordinateSpace(ESplineCoordinateSpace): 반환받을 방향 벡터의 기준 공간을 정합니다.
World: 게임 월드의 절대적인 3D 방향(화살표)으로 반환합니다. (차량의 현재 회전 방향과 직접 비교하기 위해 주로 사용합니다.)Local: 스플라인 컴포넌트 자체가 가지고 있는 로컬 축을 기준으로 한 방향을 반환합니다.내부 실행 원리 (알고리즘)
- 거리 → 파라미터 변환: 먼저 입력받은 실제 누적 거리(
Distance)를 스플라인 내부의 거리 데이터 테이블(Distance Table)과 대조합니다. 이를 통해 해당 거리가 곡선 방정식 상의 어느 지점(InputKey 또는 파라미터 t)에 위치하는지 역으로 찾아냅니다.- 곡선 미분 및 접선 계산 (Derivative): 찾아낸 곡선 지점에서 베지어(Bezier) 또는 캣멀-롬(Catmull-Rom) 곡선 방정식을 수학적으로 미분합니다. 곡선의 특정 지점을 미분하면, 그 지점에서의 순간적인 기울기인 접선(Tangent) 벡터가 도출됩니다.
- 정규화 및 공간 변환 (Normalization & Transform):
- 도출된 접선 벡터의 길이를 1로 압축(정규화)하여, 힘이나 속도가 배제된 순수한 단위 벡터(방향 화살표)로 만듭니다.
- 마지막으로 매개변수에서
World를 선택했기 때문에, 스플라인 액터가 월드에 배치된 회전값을 곱하여 완벽한 게임 월드 기준의 방향을 반환합니다.
ComputeSteeringErrors
차량의 현재 위치, 속도, 방향(Yaw)을 도로 데이터와 실시간으로 비교하여, 자율주행 핸들 조작의 근거가 되는 위치, 헤딩, 횡방향이라는 세 가지 핵심 조향 오차를 계산하는 함수입니다.
1. 전방 주시 거리(Look-Ahead Distance) 결정 운전자가 커브길에서 먼 곳을 미리 보고 핸들을 조절하듯, 차량이 참고할 전방의 목표 거리를 계산합니다.
- 곡률 정규화 (
CurvNorm): 현재 도로의 꺾임 정도(CurvHere)를 민감도에 따라 0 ~ 1 사이 값으로 변환합니다. - 거리 스케일 결정 (
CurvScale):Lerp함수를 사용하여 급커브일수록 전방 주시 거리를 유연하게 조절합니다. - 최종 거리 계산 (
LADist): 아래 공식을 사용하여 속도가 빠를수록 더 먼 곳을 주시하도록 설정합니다.LADist = (기본거리 + 차량속도 * 속도계수) * 곡률스케일
2. 전방 목표 지점(Look-Ahead Point) 데이터 확보 위에서 계산된 거리(LADist)를 바탕으로 도로 위의 목표 정보를 가져옵니다.
- 목표 위치(
LAPos):GetLocationAtDistance를 사용하여 전방 지점의 월드 좌표를 구합니다. - 목표 방향(
LADir):GetDirectionAtDistance를 사용하여 해당 지점에서 도로가 나아가는 방향(단위 벡터)을 구합니다.
3. 단계별 세 가지 핵심 오차 계산
- ① 위치 오차 (Position Error) - 공부 예정
- 내용: 내 차에서 목표 지점(
LAPos)을 바라봤을 때의 각도와 현재 차량 정면 각도의 차이입니다. - 역할: 차의 전면이 목표 지점을 정확히 조준하여 달려가게 만듭니다.
- 흐름: GetSafeNormal로 목표물까지의 ‘순수한 방향’만 뽑아내고, Atan2로 그 방향이 몇 도인지 알아낸 뒤, FindDeltaAngleDegrees로 내 차가 지금 당장 왼쪽/오른쪽 중 어디로 꺾는 게 빠른지 결정하는 과정입니다.
- 내용: 내 차에서 목표 지점(
- ② 헤딩 오차 (Heading Error) -> 공부 예정
- 내용: 목표 지점 도로의 진행 방향(
LADir)과 현재 차량 정면 각도의 차이입니다. - 역할: 도로의 휜 모양과 차량의 방향을 평행하게 맞추어 코너링을 부드럽게 합니다.
- 흐름: 전방 도로가 어느 쪽으로 굽어 있는지 ‘진행 방향’(GetDirectionAtDistance)을 가져오고, Atan2로 그 도로의 방향이 월드 기준으로 몇 도인지 알아낸 뒤, FindDeltaAngleDegrees로 내 차가 도로의 흐름과 평행하게 정렬되려면 어느 쪽으로 꺾는 게 빠른지 결정하는 과정입니다
- 내용: 목표 지점 도로의 진행 방향(
- ③ 횡방향 오차 (Cross Track Error) -공부 예정
- 내용: 현재 도로의 중앙선으로부터 차량이 좌/우로 얼마나 떨어져 있는지 물리적 거리를 계산합니다.
- 계산법: 차량 오프셋에서 도로 진행 방향 성분을 제거하여 순수 옆방향 벡터를 추출하고, 이를 도로 기준 오른쪽 단위 벡터와 내적하여 부호가 있는 횡방향 거리를 산출합니다.
- 결과: 양수(+)면 오른쪽 이탈, 음수(-)면 왼쪽 이탈을 의미하며, 차를 다시 도로 중앙으로 복귀시키는 기준이 됩니다.
4. 결과 반환
- 계산된 세 가지 오차를
FSteeringErrors구조체에 담아 반환합니다. 이 데이터는 다음 단계인 조향 명령(Steering Command) 적용 시 핸들을 꺾는 각도를 결정하는 데 사용됩니다.
주요 수학 함수 및 로직 정의
| 항목 | 설명 |
|---|---|
FMath::Atan2 | 위치/방향 벡터를 각도(라디안)로 변환할 때 사용하며, 360도 전 방향을 정확하게 판별합니다. |
FMath::FindDeltaAngleDegrees | 두 각도 사이의 최단 회전 경로를 구합니다. (예: 350도에서 10도로 갈 때 -340도가 아닌 +20도로 계산) |
FVector::CrossProduct | [외적] 도로의 정면 방향과 하늘 방향(UpVector)을 외적하여, 횡방향 오차 계산에 필요한 ‘도로의 오른쪽 방향’을 생성합니다. |
FVector::DotProduct | [내적] 두 벡터가 얼마나 일치하는지 계산합니다. 횡방향 오차의 거리값과 방향(좌/우)을 판별하는 데 사용됩니다. |
GetLocationAtDistance
스플라인의 시작점에서부터 특정 거리만큼 이동했을 때, 그 지점의 실제 월드 좌표(X, Y, Z)가 어디인지 계산해서 알려주는 함수입니다.
- 도로 데이터 유효성 검사: 현재 참조하고 있는 도로 데이터(
GetSpline())가 정상적으로 존재하는지 확인합니다. 만약 도로 데이터가 없다면 에러 방지를 위해 원점 좌표(FVector::ZeroVector)를 반환하고 함수를 즉시 종료합니다. - 거리 보정 (WrapDistance 실행): 입력받은 목표 거리(
Distance)를WrapDistance함수에 전달합니다. 이 과정을 통해 거리가 도로의 총길이를 벗어날 경우 도로 형태(순환형/직선형)에 맞춰 안전한 범위 내의 값(WrappedDist)으로 보정합니다. - 월드 좌표 추출: 언리얼 엔진의 내장 함수인
GetLocationAtDistanceAlongSpline을 호출합니다. 보정된 거리(WrappedDist)를 기준으로 실제 게임 월드 상의 절대 좌표(ESplineCoordinateSpace::World)를 계산합니다. - 결과 반환: 계산된 도로 위의 3D 좌표값(
FVector)을 반환합니다. 자율주행 시스템은 이 좌표를 ‘목표 지점(Way Point)’으로 삼아 차량이 이동해야 할 방향을 결정하게 됩니다.
GetLocationAtDistanceAlongSpline
스플라인 곡선을 따라 특정 거리만큼 이동한 지점의 공간 좌표(Position)를 계산하여 반환하는 함수입니다.
매개변수 (Parameters)
Distance(float): 도로의 시작점에서부터 측정할 누적 거리(cm)입니다.CoordinateSpace(ESplineCoordinateSpace): 좌표의 기준 공간을 정합니다.World: 게임 월드의 절대 좌표로 반환합니다.Local: 스플라인 컴포넌트 자체의 기준 좌표로 반환합니다.
내부 실행 원리 (알고리즘)
- 테이블 룩업: 엔진 내부의 거리 데이터 테이블을 참조하여 입력된 거리가 어느 곡선 구간(Segment)에 위치하는지 확인합니다.
- 곡선 보간 (Interpolation): 해당 구간의 제어점 정보를 바탕으로 곡선 방정식(Bezier 등)을 연산하여, 직선이 아닌 실제 휘어진 곡선 경로 상의 정확한 지점을 산출합니다.
BlendSteeringInput
계산된 세 가지 오차(위치, 헤딩, 횡방향)를 현재 도로 상황에 맞춰 적절한 비율로 섞어, 최종적인 자동차 핸들 조작 값(-1.0 ~ 1.0)을 결정하는 함수입니다.
- 도로 곡률에 따른 가중치 조절: 현재 도로가 얼마나 굽었는지(
CurvHere)를 바탕으로 어떤 오차를 더 중요하게 볼지 결정합니다.CurvNorm: 현재 곡률을 0(직선)에서 1(급커브) 사이의 값으로 정규화합니다.HdgW(헤딩 가중치):Lerp함수를 사용해 직선 도로에서는 헤딩 오차(도로와 평행하게 가기)에 가중치를 높이고, 급커브에서는 위치 오차(목표 지점 쫓아가기)에 비중을 두도록 유연하게 조절합니다.
- 기본 조향 명령(YawCmd) 계산: 앞서 구한 가중치(
HdgW)를 사용하여 위치 오차와 헤딩 오차의 가중 평균을 구합니다.- 공식:
위치 오차 * (1.0 - 가중치) + 헤딩 오차 * 가중치 - 이 과정을 통해 차가 목표 지점을 향해 달려가는 힘과 도로의 흐름에 몸을 맞추는 힘이 적절히 조화된 기본 회전 각도가 만들어집니다.
- 공식:
- 횡방향 이탈 보정(CrossTrack) 결합: 차량이 도로 중앙선에서 좌우로 벗어난 정도를 최종 명령에 반영합니다.
- 복원력 적용:
Errors.CrossTrackError * CrosstrackGain을 계산하여, 차가 중앙에서 멀어질수록 반대 방향으로 핸들을 더 강하게 당겨주는 힘을 추가합니다.
- 복원력 적용:
- 최종 정규화 및 결과 반환: 계산된 값을 실제 자동차 핸들이 인식할 수 있는 입력 범위로 변환합니다.
- 단위 변환: 각도 단위인
YawCmd를MaxYawDelta로 나누어 핸들 입력 단위(0~1)로 스케일을 맞춥니다. - 최종 제한(Clamp):
FMath::Clamp를 통해 결과값이 -1.0(최대 왼쪽)에서 1.0(최대 오른쪽) 범위를 절대로 벗어나지 않도록 고정하여 반환합니다.
- 단위 변환: 각도 단위인
주요 로직 포인트
- 가중치를 섞는 이유 (Blending): 직선도로에서는 도로와 나란히 가는 것이 중요하지만, 급커브에서는 도로의 결을 따지기보다 일단 앞에 있는 목표 지점을 향해 차를 빠르게 들이미는 것이 주행 안정성에 유리하기 때문입니다.
- 횡방향 오차의 역할: 위치와 헤딩 오차만으로는 차가 한쪽으로 쏠려서 달리는 현상을 완전히 잡기 어렵습니다. 이때
CrossTrackError가 중앙으로 차를 계속 밀어넣는 라인 유지(Line Keeping) 역할을 수행합니다.
요약
- 곡률을 보고 핸들을 얼마나 부드럽게 혹은 예민하게 꺾을지 가중치를 정하고,
- 위치/헤딩 오차를 섞어 기본 회전 각도를 만든 뒤,
- 횡방향 오차를 더해 차를 도로 정중앙으로 끌어당기며,
- 마지막으로 Clamp를 통해 실제 자동차 핸들이 돌아가는 물리적 한계 안으로 값을 가두는 과정입니다!
ApplySteeringCommand
BlendSteeringInput을 통해 계산된 값을 차량의 Steering값에 적용시킴
UpdateTargetSpeed
현재 위치와 전방 도로의 곡률(굽은 정도)을 분석하여 안전한 제한 속도를 계산하고, 급격한 속도 변화 없이 부드럽게 가속 또는 감속하도록 목표 속도를 갱신하는 함수입니다.
- 곡률 기반 속도 제한 결정: 현재 내 차가 있는 지점(
CurvHere)과 일정 거리 앞선 지점(CurvAhead)의 곡률을 각각 계산하여, 둘 중 더 낮은 속도 제한값을 채택합니다.- 핵심 전략:
FMath::Min을 사용해 전방에 급커브가 감지되면 현재 위치에서부터 미리 속도를 줄이기 시작하는 ‘선제적 감속’ 로직이 핵심입니다.
- 핵심 전략:
- 도로 자체 제한 속도 적용: 도로 액터(
TargetRoad)에 별도로 설정된 제한 속도(SpeedLimit)가 있는지 확인합니다.- 만약 법적 제한 속도가 설정되어 있다면, 앞서 계산한 곡률 기반 속도와 비교하여 더 낮은 값을 최종 목표 속도(
SpeedLimit)로 확정합니다.
- 만약 법적 제한 속도가 설정되어 있다면, 앞서 계산한 곡률 기반 속도와 비교하여 더 낮은 값을 최종 목표 속도(
- 가속/감속 상황별 반응 속도(Rate) 설정: 현재 속도와 목표 속도를 비교하여 주행 상황에 맞는 보간 속도를 결정합니다.
- 감속 상황: 속도를 줄여야 할 때는
DecelRate를 적용하여 민감하고 빠르게 감속합니다. - 가속 상황: 속도를 높일 때는
AccelRate를 적용하여 점진적이고 부드럽게 속도를 올립니다.
- 감속 상황: 속도를 줄여야 할 때는
- 부드러운 속도 보간 및 결과 반환: 언리얼 엔진의 유틸리티 함수인
FInterpTo를 사용하여 최종 속도를 도출합니다.- 목표 속도까지 수치가 툭툭 끊기지 않고 프레임마다 매끄럽게 변화하도록 처리하여(Smoothing), 실제 자동차가 움직이는 듯한 자연스러운 주행감을 구현하고 최종 갱신된
SmoothedTargetSpeed를 반환합니다.
- 목표 속도까지 수치가 툭툭 끊기지 않고 프레임마다 매끄럽게 변화하도록 처리하여(Smoothing), 실제 자동차가 움직이는 듯한 자연스러운 주행감을 구현하고 최종 갱신된
주요 로직 포인트
- 전방 곡률(
CurvAhead) 참조의 이유: 현재 위치만 보고 속도를 정하면 이미 커브에 진입한 뒤에야 급브레이크를 밟게 됩니다. 전방 지점을 미리 살펴봄으로써 실제 운전자처럼 코너 진입 전에 미리 속도를 줄이는 지능적인 주행이 가능해집니다. - FInterpTo (보간 함수): 현재 값에서 목표 값까지 지정된 속도(
Rate)로 부드럽게 이어주는 함수입니다. 자율주행에서 갑작스러운 속도 변화로 인한 물리 엔진의 오작동이나 어색한 움직임을 방지하는 중요한 역할을 합니다.
요약
- 현재와 전방의 커브를 비교해 “얼마나 느리게 가야 안전할지” 최소 속도를 정하고,
- 도로 제한 속도를 한 번 더 체크하여 법규를 준수한 뒤,
- 가속인지 감속인지에 따라 반응 속도를 다르게 설정하여,
- 최종적으로 FInterpTo를 통해 속도가 끊기지 않고 부드럽게 변하도록 만드는 과정입니다!
ApplySpeedCommand
계산된 목표 속도(TargetSpeed)와 현재 차량의 속도(CurrentSpeed)를 비교하여, 차량에 실제 가속(Throttle) 또는 감속(Brake) 명령을 전달하는 함수입니다.
- 속도 오차 계산 및 정규화: 목표 속도와 현재 속도의 차이에 민감도(
ThrottleGain)를 곱하여 명령값(Cmd)을 생성합니다.- 공식:
(목표 속도 - 현재 속도) * 가속 민감도 - 정규화:
FMath::Clamp를 통해 결과값을 -1.0(최대 브레이크)에서 1.0(최대 가속) 사이로 맞춥니다.
- 공식:
- 주행 상태 결정 (Decision Making): 계산된
Cmd값의 크기에 따라 차량의 행동을 세 가지 중 하나로 결정합니다.- 가속 (Throttle):
Cmd가 설정된 불감대(CoastDeadzone)보다 클 경우, 차를 더 빠르게 밀어붙입니다. - 감속 (Brake):
Cmd가 마이너스 값이고 그 절댓값이 불감대보다 클 경우, 브레이크를 밟아 속도를 줄입니다. - 관성 주행 (Coast): 속도 차이가 아주 미미하여
Cmd가 불감대 범위 안에 있을 경우, 엑셀과 브레이크를 모두 떼고 관성에 의해 달리게 합니다.
- 가속 (Throttle):
- 실제 차량 입력 호출 (연동 예정): 결정된 명령값(
Cmd)을 실제 자동차 폰(Pawn)의 가속/브레이크 함수에 전달합니다.
주요 로직 포인트
- ThrottleGain (가속 민감도): 속도 차이를 얼마나 민감하게 받아들여 엑셀을 밟을지 결정하는 계수입니다. 이 값이 너무 높으면 차가 덜컥거리며 가속하고, 너무 낮으면 목표 속도에 도달하는 데 한참 걸립니다.
- CoastDeadzone (관성 주행 불감대): 목표 속도와 현재 속도가 거의 비슷할 때, 엑셀과 브레이크를 번갈아 가며 밟는 ‘채터링(Chattering)’ 현상을 방지하기 위한 여유 공간입니다. 이 구간 덕분에 주행이 훨씬 부드러워집니다.
요약
- 속도 차이를 계산해 “더 밟아야 할지, 멈춰야 할지”를 -1 ~ +1 사이의 숫자로 만들고,
- Deadzone을 체크해 속도 차이가 작으면 관성 주행(Coast)을 선택하며,
- 값이 크면 가속(Positive), 값이 작으면 브레이크(Negative) 명령을 내리고,
- 마지막으로 이 명령을 실제 자동차 폰의 페달 시스템에 전달하는 과정입니다!