단순한 첫 시도

if (도로가 휘어져 있음) {
    Throttle = 0.3;  // 천천히
} else {
    Throttle = 1.0;  // 빠르게
}

 

도로가 휘어져 있음 -> 곡률(curvature) 측정 함수 사용

float EstimateCurvature(float AheadOffset) const
{
    const FVector D1 = GetDirectionAtDistance(CurrentDistance + AheadOffset);
    const FVector D2 = GetDirectionAtDistance(CurrentDistance + AheadOffset + 50.f);
    return FMath::Acos(FMath::Clamp(FVector::DotProduct(D1, D2), -1.f, 1.f));
}

 

이게 라디안 단위 각도를 반환함

  • 직선: 0에 가까움
  • 살짝 곡선: 0.1 정도
  • 급커브: 1.0 이상

곡률을 어떻게 속도로 바꾸지?

 

처음 떠오른 방식

// 단순 반비례
const float MaxSpeed = 3000.f;
float TargetSpeed = MaxSpeed - Curvature * 1000.f;

 

동작은 하는데...

  • 어떤 커브는 너무 천천히 감
  • 어떤 커브는 너무 빠르게 들어가서 미끄러짐
  • 값(1000.f)을 어떻게 정해야 할지 감이 안 옴

이거 아닌 것 같음;;

 

AI 도움

"한계 속도(critical speed)"

차량이 곡선 도로에서 미끄러지지 않고 지나갈 수 있는 최대 속도.

 

물리 공식 사용

 

원운동에서 차가 미끄러지지 않으려면:

원심력 ≤ 마찰력

 

공식 이용해서 최대 안전 속도 구하면

 

필요한 건:

  • 마찰 계수 (μ): 보통 도로 0.7~0.8, 빙판 0.1, 레이싱 타이어 1.2
  • 곡률 반경 (R): 도로가 얼마나 휘었는지
최대 안전 속도 = √(마찰계수 × 중력 × 커브반지름)
  • 도로가 끈끈할수록 → 빨리 가도 됨
  • 커브가 클수록(완만할수록) → 빨리 가도 됨
  • 급커브 + 빙판 → 매우 천천히

곡률 반경 = 그 지점에서 도로가 그리는 가상의 원의 반지름

직선 도로:        반지름 = 무한대
완만한 커브:      반지름 = 큼
급커브:          반지름 = 작음

 

곡률 측정할 때 도로 위에서 50cm 떨어진 두 지점을 고르고 각 지점에서 도로 방향을 가져옴

그 50cm가 CurvatureSampleSpan

 

중요한 거: 빨간 선(50cm)은 도로를 따라간 거리

도로가 휘어 있으면 빨간 선도 휘어 있음

 

 

빨간 도로는 회색 점선 원의 일부분

그 원의 반지름이 구하려는 R!

  • 원이 크면 → 도로가 거의 직선처럼 보임 (완만한 커브)
  • 원이 작으면 → 도로가 많이 휘어 있음 (급커브)

 

이제 두 개념 합치기

  1. 두 지점 사이 도로를 따라간 거리 = 50cm (이게 호의 길이)
  2. 두 지점에서 도로 방향의 각도 차이 = 곡률 (라디안)
호의 길이 = 반지름 × 각도
L = R × θ

 

대입

50cm = R × Curvature

 

그러니까

R = 50cm / Curvature

 

 

  • 빨간 호 = 두 지점 사이 도로 = 50cm (CurvatureSampleSpan)
  • 각도 θ = 두 반지름 사이 각도 = 측정한 곡률(라디안)
  • 파란 R = 가상의 원 반지름 = 구하려는 값

 

L = R × θ
50cm = R × Curvature

 

R을 구하면

R = 50cm / Curvature
R = CurvatureSampleSpan / Curvature

 

각도(작은 값)로 50cm를 나누면 반지름이 나옴

  • 각도가 작으면 (직선에 가까우면) → 50cm를 작은 수로 나눔 → R이 매우 큼 → 직선
  • 각도가 크면 (급커브) → 50cm를 큰 수로 나눔 → R이 작음 → 급커브

 

float ComputeCurveSpeedLimit(float Curvature) const
{
    if (Curvature <= KINDA_SMALL_NUMBER)
        return MaxSpeed;  // 직선이면 최대 속도

    // 곡률 → 곡률 반경
    const float Radius = CurvatureSampleSpan / Curvature;

    // V = √(μ × g × R)
    return FMath::Sqrt(LateralFriction * 980.f * Radius);
}

 

주의: 언리얼은 cm 단위라 g = 9.8 m/s² 가 아니라 980 cm/s²

이거 모르고 m 단위로 계산하면 결과가 100배 이상 작아짐

 

KINDA_SMALL_NUMBER

언리얼 매크로 문서

 

KINDA_SMALL_NUMBER = 0.0001f

부동소수점에서 "사실상 0인지" 확인할 때 쓰는 값

Curvature == 0으로 비교하면 부동소수점 오차 때문에 위험함

대신 Curvature <= 0.0001f로 비교

 

1. 직선 처리

if (Curvature <= KINDA_SMALL_NUMBER)
    return MaxSpeed;

 

Curvature가 거의 0이면 (직선이면) 0으로 나누기 사고가 남

그래서 직선이면 그냥 최대 속도 반환

 

2. 곡률 → 반지름

const float Radius = CurvatureSampleSpan / Curvature;

 

방금 본 호의 길이 공식

CurvatureSampleSpan(50cm)를 곡률(라디안)로 나눠서 반지름(cm) 얻기

 

3. 최대 속도 계산

return FMath::Sqrt(LateralFriction * 980.f * Radius);

 

물리 공식 그대로

V = √(μ × g × R)
  = √(LateralFriction × 980 × Radius)

 

원심력 = 마찰력 조건에서 질량이 약분돼서 V = √(μ·g·R) 만 남고, 호의 길이 공식 L = R·θ로 곡률(각도)을 반지름으로 바꾼다. 단위(cm)만 조심하면 끝.

 

 

최소/최대 속도 제한

return FMath::Clamp(
    FMath::Sqrt(LateralFriction * 980.f * Radius),
    MinSpeed,  // 너무 천천히 가도 답답하니 최저 속도 보장
    MaxSpeed   // 직선이라도 너무 빠르면 위험
);

 

MinSpeed = 400 (4m/s = 14km/h 정도)로 두면 안 막힘.

 

 

새 문제 발견

1. 너무 늦게 감속 -> 전방 곡률 미리 측정

// 현재 위치 곡률
const float CurvHere = EstimateCurvature(0.f);

// 전방 50m 앞의 곡률
const float CurvAhead = EstimateCurvature(5000.f);  // 50m 앞

// 둘 중 더 위험한 쪽 채택
const float SpeedLimit = FMath::Min(
    ComputeCurveSpeedLimit(CurvHere),
    ComputeCurveSpeedLimit(CurvAhead)
);

 

FMath::Min으로 더 작은(=더 제한적인) 속도를 선택

이러면 50m 앞에 급커브가 있으면 지금부터 감속 시작

 

지금 50m 앞 결과
직선 (빠름 OK) 직선 (빠름 OK) 빠르게 달림
직선 (빠름 OK) 급커브 (느려야) 미리 감속
급커브 (느려야) 직선 (빠름 OK) 일단 느림
급커브 (느려야) 급커브 (느려야) 느림

 

(참고) FMath::Min - 그냥 작은 거 고르는 함수

FMath::Min(3, 7)  // → 3
FMath::Min(50, 80)  // → 50

 

 

변수화해서 튜닝 가능하게

UPROPERTY(EditAnywhere)
float BrakePreviewDist = 5000.f;  // 50m

const float CurvAhead = EstimateCurvature(BrakePreviewDist);

 

 

5000(50m)이 항상 좋은 값은 아님

  • 빠른 차는 더 멀리 봐야 함
  • 느린 차는 가까이 봐도 됨 
  • 트랙별로 다를 수도 있음

그래서 에디터에서 조정 가능하게 변수화한 것

 

 

2. 속도가 갑자기 변함 -> 부드러운 가/감속 함수 적용

FMath::FInterpTo 함수

float FInterpTo(float Current, float Target, float DeltaTime, float InterpSpeed);

 

현재 값에서 목표 값으로 매 프레임 조금씩 다가가게 만들어주는 함수

지수 함수처럼 처음엔 빠르게 가까워지면 천천히

 

멤버 변수로 "스무딩된 목표 속도" 추가

private:
    float SmoothedTargetSpeed = 0.f;

 

Tick에서 부드럽게 보간

// 진짜 목표 속도 (곡률 기반)
const float SpeedLimit = FMath::Min(
    ComputeCurveSpeedLimit(CurvHere),
    ComputeCurveSpeedLimit(CurvAhead)
);

// 부드럽게 다가감 (1초에 절반씩 가까워지는 정도)
SmoothedTargetSpeed = FMath::FInterpTo(
    SmoothedTargetSpeed,
    SpeedLimit,
    DeltaTime,
    0.5f  // InterpSpeed
);

 

  • 감속(브레이크): 안전을 위해 천천히, 부드럽게
  • 가속(스로틀): 빠르게 회복

→ 두 경우에 다른 보간 속도 적용:

const float Rate = (SpeedLimit < SmoothedTargetSpeed) ? DecelRate : AccelRate;
SmoothedTargetSpeed = FMath::FInterpTo(SmoothedTargetSpeed, SpeedLimit, DeltaTime, Rate);
UPROPERTY(EditAnywhere)
float DecelRate = 0.5f;  // 감속은 천천히

UPROPERTY(EditAnywhere)
float AccelRate = 1.0f;  // 가속은 빠르게

 

 

이제 이걸 실제 페달 조작으로 바꾸기

  • 현재 속도 < 목표 속도 → 가속해야 함 (스로틀)
  • 현재 속도 > 목표 속도 → 감속해야 함 (브레이크)
  • 거의 같음 → 페달 모두 떼기 (관성)

 

P 컨트롤러

P 컨트롤러란?

P는 Proportional(비례). 이름 그대로: 차이가 크면 많이 밟고, 차이가 작으면 살짝 밟아라.

명령 = (목표 - 현재) × Gain

 

  • 목표 - 현재 = 차이 (얼마나 부족한지)
  • Gain = 곱하는 비율 (얼마나 강하게 반응할지)

 

const float Cmd = (SmoothedTargetSpeed - CurrentSpeed) * ThrottleGain;
  • SmoothedTargetSpeed - CurrentSpeed = 차이 (양수면 더 빨라야, 음수면 너무 빠름)
  • × ThrottleGain = 그 차이를 페달 명령으로 환산

Cmd 값:

  • 양수 → 가속해야 함 (액셀)
  • 음수 → 감속해야 함 (브레이크)
  • 0 근처 → 그대로 유지
 

ThrottleGain은 적당히 작게 (예: 0.002)

너무 크면: 차이가 살짝만 나도 페달을 확 밟아버림. 목표를 지나치고, 다시 반대로 확 밟고 -> 차가 휘청거림

너무 작으면: 차이가 커도 페달을 살살만 밟음. 목표에 도달하는 데 한참 걸림. (반응 느림)

 

즉 너무 크면 진동, 너무 작으면 반응 느림

 

-1 ~ +1 범위로 클램프

const float Cmd = FMath::Clamp(
    (SmoothedTargetSpeed - CurrentSpeed) * ThrottleGain,
    -1.f, 1.f
);
  • +1 = 액셀 100% 풀로 밟기
  • 0 = 페달에서 발 떼기
  • -1 = 브레이크 100% 풀로 밟기

차이가 엄청 클 때 (예: 목표 200km/h, 현재 0km/h) 계산값이 +5 같은 말도 안 되는 값이 나올 수 있음

페달은 100%가 최대인데 500% 밟을 수는 없으니 clamp 사용

 

 

데드존 추가 (0 근처는 무시)

 

문제: Cmd가 0.001 같은 작은 값이어도 페달이 살짝 밟힘 → 데드존(Deadzone) 도입

if (Cmd > 0.05f) {
    // 충분히 큰 양수 → 가속
    Throttle = Cmd;
}
else if (Cmd < -0.05f) {
    // 충분히 큰 음수 → 감속
    Brake = -Cmd;
}
else {
    // 거의 0 → 페달 다 떼기
    Throttle = 0;
    Brake = 0;
}

 

"명령이 너무 작으면 그냥 무시하자."

  • Cmd > 0.05 → 충분히 크니까 액셀 밟기
  • Cmd < -0.05 → 충분히 (반대로) 크니까 브레이크 밟기
  • -0.05 < Cmd < 0.05 → 의미 없는 값. 둘 다 떼기 (관성 주행)

 

0.05 임계값을 CoastDeadzone 변수로 빼서 튜닝 가능하게

 

도로별 제한 속도 추가

스쿨존이나 어린이보호구역 같은 시나리오를 위해 도로별 SpeedLimit 도 적용

곡률 계산만 하면 스쿨존 같은 직선 도로를 100km/h로 달릴 수도 있으니까

if (TargetRoad->SpeedLimit > 0.f)
{
    SpeedLimit = FMath::Min(SpeedLimit, TargetRoad->SpeedLimit);
}

 

여러 제한이 있으면 가장 빡센 거(=가장 작은 거) 따라가기

ex. 

80 (완만한 커브) 60 (일반 도로) 60
80 (완만한 커브) 30 (스쿨존) 30
40 (급커브) 100 (고속도로) 40
40 (급커브) 30 (스쿨존) 30

 

if (TargetRoad->SpeedLimit > 0.f)

 

모든 도로에 제한 속도가 정해져 있는 건 아니니까 제한 없는 도로일 떄

언리얼에서는 보통 제한 없음을 0이나 음수로 표시

  • SpeedLimit = 0 → 제한 없음 → 곡률 한계만 적용
  • SpeedLimit = 60 → 60km/h 제한 있음 → 둘 중 작은 거 적용

if (... > 0.f)로 "유효한 값일 때만" 적용하는 것

 
 
유지 패턴
SpeedLimit = ComputeCurveSpeedLimit(CurvHere);
SpeedLimit = FMath::Min(SpeedLimit, ComputeCurveSpeedLimit(CurvAhead));  // 전방 곡률
SpeedLimit = FMath::Min(SpeedLimit, TargetRoad->SpeedLimit);             // 도로 제한
// 더 추가한다면
// SpeedLimit = FMath::Min(SpeedLimit, RainSpeedLimit);                   // 비 올 때
// SpeedLimit = FMath::Min(SpeedLimit, TrafficAheadLimit);                // 앞차 있을 때

 

Min을 계속 누적하는 패턴

"지금까지 본 모든 제한 중 가장 빡센 거"를 들고 가는 것

 

// SpeedLimit = 곡률 기반 한계
float SpeedLimit = FMath::Min(
    ComputeCurveSpeedLimit(CurvHere),
    ComputeCurveSpeedLimit(CurvAhead)
);

// 도로 자체 제한 속도가 있으면 추가 적용
if (TargetRoad->SpeedLimit > 0.f)
{
    SpeedLimit = FMath::Min(SpeedLimit, TargetRoad->SpeedLimit);
}

결과 정리

// 매 프레임 실행
1. 현재 곡률 측정 → 안전 속도 계산
2. 전방 곡률 측정 → 미리 감속용 안전 속도 계산
3. 둘 중 더 제한적인 속도 선택
4. (있으면) 도로별 제한 속도 적용
5. 부드럽게 보간 (FInterpTo)
6. 목표 속도 vs 현재 속도 → 스로틀/브레이크 명령
7. 데드존 처리

 

 

물리 개념

  • 원심력 vs 마찰력 : 차가 미끄러지는 원리
  • 곡률 반경 :  1/곡률
  • 마찰 계수 : 노면별로 다른 그립력

제어 개념

  • P 컨트롤러 : 가장 단순한 비례 제어
  • 데드존 : 작은 명령은 무시 (떨림 방지)
  • 선행 제어 (Look-ahead) : 미래를 예측해서 미리 대응

 

언리얼 함수

FMath::Sqrt 제곱근
FMath::FInterpTo 부드러운 보간
FMath::Min 둘 중 작은 값
FMath::Clamp 값 범위 제한
KINDA_SMALL_NUMBER 0.0001 (부동소수점 비교용)

'프로젝트 > 자율주행 위험구간 분석 시뮬레이터' 카테고리의 다른 글

파라미터  (0) 2026.05.08
코드 정리와 확장 준비  (0) 2026.05.08
조향 알고리즘  (0) 2026.05.08
매 프레임 도로 따라가기  (0) 2026.05.07
첫 설계와 도로 클래스  (0) 2026.05.07

+ Recent posts