비, 눈, 안개 속에서도 안전하게 주행해야 하고 그런 환경의 데이터는 실제로 모으기 어렵고 위험하기 때문에 시뮬레이터 안에서 악천후를 합성해보는 게 좋을 것 같아서 구현하기로 함

 

팀 작업이기 때문에 기본적인 명세서가 필요할 것 같아 작성함

날씨 구현.pdf
0.14MB

 

 

spline 알고리즘 짜야하는 내 목표: 

비나 눈이 오면 도로가 미끄러워지니까 그에 맞춰서 자율주행 차가 알아서 천천히 달리게 하자

 

주요로 본 사항 -> 도로의 마찰력이 바뀌었을 때 차가 원래대로 악셀을 밟았다가 미끄러지면서 속도가 순간적으로 빨라지거나 아니면 급브레이크를 밟아서 속도가 갑자기 줄어들지 않도록 터널을 지났을 때 초반은 속도를 일정하게 유지하도록 설정하고, 점차 속도를 천천히 줄여나가도록 설정하자

 

이미 있던 터널 시스템 이용하기

터널 시스템 구조

ATunnelTrigger (공간 트리거)
        ↓
Pawn의 OnTunnelToggleDelegate.Broadcast(bool)
        ↓
   각 컴포넌트의 ApplyTunnelProfile(bool)

 

델리게이트 하나로 여러 컴포넌트에 동시에 신호를 뿌리는 팬아웃(fan-out) 구조

 

날씨 시스템도 같은 방식으로 구현

트리거 주체 ATunnelTrigger (공간 기반) UWeatherSubsystem (월드 전역)
신호 타입 bool EWeather enum
차량 내부 전달 Pawn 델리게이트 Pawn 델리게이트 
컴포넌트 함수 ApplyTunnelProfile(bool) ApplyWeatherProfile(EWeather)
상태 복원 베이스라인 캐싱 베이스라인 캐싱 

 

 

매니저 액터 생성

날씨를 관리하는 무언가가 필요함

가장 먼저 떠올린 건 터널과 똑같은 방식 차용하기

날씨도 터널과 비슷하게 AWeatherManager라는 액터를 맵에 하나 두면 되지 않을까?

근데 터널은 Overlap으로 차를 알지만, 날씨는 맵 전체에 적용되니까 매니저가 직접 월드를 뒤져서 차를 찾아야 함

언리얼에는 TActorIterator라는 월드의 특정 종류 액터를 전부 훑는 도구가 있음 -> 이걸로 차를 찾을 계획

 

 

튜터님의 조언

튜터님이 WorldSubsystem을 사용해보라고 하셨다

 

WorldSubsystem 찾아보기!

쉽게 말하면 맵이 시작될 때 엔진이 자동으로 만들어 주는 보이지 않는 매니저

 

접속 코드

UWeatherSubsystem* Weather = GetWorld()->GetSubsystem<UWeatherSubsystem>();

 

 

Register 패턴이 뭔가

매니저가 차를 찾아다니는 게 아니라 차가 태어날 때 스스로 손을 드는 방식

등록된 명단만 들고 있다가, 공지(날씨 변경)가 생기면 명단 전체에 한 번에 뿌린다

차량이 BeginPlay에서 → WeatherSubsystem->RegisterVehicle(this)
도로가 BeginPlay에서 → WeatherSubsystem->RegisterRoad(this)

 

이 방식이 1차 계획의 문제를 전부 해결함

배치 깜빡할 일 없고(엔진이 자동 생성), 차가 여러 대여도 다 등록되며, 타이밍 문제 없고(각자 태어날 때 등록), 도중에 생긴 차도 등록하면 합류됨

 

원래 계획에서는 내가 이 코어(뼈대)를 다 만들 예정이었는데 팀원이 해주심

원래 내 작업:  코어 전체 + 자율주행 반응 + 통합 테스트
바뀐 내 작업:  자율주행 반응 + 통합 테스트
              (코어는 팀원이 만든 인터페이스 위에 얹기)

 

팀원이 보낸 코드를 받음

명세서와 다른 부분 발견! -> DataAsset 도입

 

처음 계획

// 이렇게 하려고 했음
switch (Weather)
{
case EWeather::Rain:  FrictionScale = 0.6f; break;
case EWeather::Snow:  FrictionScale = 0.3f; break;
}

 

그런데 팀원이 만든 구조는 WeatherPresetDataAsset이라는 데이터 에셋을 두고, 실제 숫자는 콘텐츠 브라우저의 에셋 파일(DA_Weather_Rain 등)에서 입력하는 방식

 

팀원이 바꾼대로 코드와 데이터를 분리하는 게 좋다고 생각함

  • 원래: 비의 마찰을 0.6에서 0.5로 바꾸려면 → 코드 수정 → 빌드(컴파일 대기) → 확인
  • DataAsset: 비의 마찰을 바꾸려면 → 에디터에서 에셋 파일 열고 숫자만 고치면 됨

빌드를 안 기다려도 되고, 코드를 모르는 사람도 수치를 조정할 수 있음.

 

그럼 나는

1. DataAsset에 변수를 선언하기

2. 그 변수를 읽어서 적용하는 코드 짜기

 

터널과 날씨가 충돌하는 문제

코드를 짜려다보니 문제 발생

터널 시스템과 날씨 시스템이 같은 자율주행 파라미터를 건드림

 

터널에 들어가면 속도/시야 파라미터가 줄어듦

날씨가 나빠져도 비슷한 파라미터를 건드리고 싶음...

근데 그러면 문제 발생

1. 터널 진입 → 시야값 = 원본 × 0.6  (터널이 깎음)
2. 터널 안에서도 비 오는 시스템 적용됨 → 날씨도 같은 변수 건드림
3. 어느 게 진짜 원본인지 꼬임 → 터널 나가도 복원 안 됨

 

  • 원본 하나만 두고, 최종값 = 원본 × 터널계수 × 날씨계수로 곱해서 누적
  • 날씨는 터널과 다른 파라미터(LateralFriction)만 건드린다. 백업 변수도 따로 둔다.
  • 터널과 날씨를 하나의 함수로 합쳐서 통합 관리

셋 중 하나 선택해야 하는데... 시간도 별로 없고 터널 코드 건드는 건 너무 위험하다고 생각해서 

그냥 날씨는 터널과 다른 파라미터만 건드리기로 함

 

터널은 속도/시야를, 날씨는 마찰만 건드림

백업 변수도 bBaselineCached(터널용)와 bWeatherBaselineCached(날씨용)로 분리

서로 다른 변수를 건드리니 충돌이 없슴

 

마찰만 바꿨는데 속도가 알아서 줄어들게 설계 (원래 짠 코드가 그럼)

그래서 날씨 신호를 받으면 마찰 계수 하나만 바꾸면 됨

void USplineFollowerComponent::ApplyWeatherProfile(EWeather Weather)
{
    // 처음 호출될 때 원본 마찰값을 딱 한 번 백업
    if (!bWeatherBaselineCached)
    {
        BaselineLateralFriction = LateralFriction;
        bWeatherBaselineCached = true;
    }

    // 현재 날씨의 DataAsset에서 배율을 읽어옴
    float FrictionScale = 1.f;  // 못 찾으면 원본 그대로 (안전장치)
    if (UWorld* World = GetWorld())
    {
        if (UWeatherSubsystem* WeatherSub = World->GetSubsystem<UWeatherSubsystem>())
        {
            if (UWeatherPresetDataAsset* Preset = WeatherSub->GetCurrentWeatherPreset())
            {
                FrictionScale = Preset->LateralFrictionScale;
            }
        }
    }

    // 원본 × 배율 (현재값이 아니라 원본 기준으로 계산!)
    LateralFriction = BaselineLateralFriction * FrictionScale;
}

 

BaselineLateralFriction * FrictionScale. 현재값이 아니라 백업한 원본에 곱함

현재값에 곱하면 날씨를 여러 번 바꿀 때 값이 오염됨

시작 0.8 0.8
비 (×0.6) 0.48 0.48
눈 (×0.3) 0.48×0.3 = 0.144 ❌ 0.8×0.3 = 0.24 ✅
맑음 (×1.0) 0.144 ❌ 0.8 ✅

 

현재값에 곱하면 맑음으로 돌아와도 원본 복원이 안 됨

항상 원본 기준으로 계산해야 깨끗함

이건 터널 시스템의 BaselineMaxSpeed * TunnelSpeedScale에서 이미 쓰던 패턴

 

 

잘 작동되는 거 확인

속도는 왜 안 건드렸나

비가 오면 차가 느려져야 하는데, 코드에서 속도를 직접 건드리지 않았음 그래도 차가 느려지기 때문

이유는 기존 자율주행 코드에

곡선 안전속도는 속도 = √(마찰 × 중력 × 곡선반경) 공식으로 계산됨.

 

마찰을 낮추면 안전속도가 자동으로 낮아짐

-> 그 낮아진 목표를 향해 차가 부드럽게(FInterpTo라는 보간 함수로) 감속

 

그래서 비 오는 순간 급정거가 아니라 자연스럽게 느려짐

마찰 하나만 바꿨는데 시스템이 알아서 연쇄 반응 

 

알게된 것

의문이 들었음

도로의 PM이 변경되어서 마찰력이 실제로 변화되는 게 내 코드랑 상관이 없는데? 

 

내가 짠 ApplyWeatherProfile은 비가 오면 LateralFriction이라는 값을 낮춤

근데 실제 도로의 물리 재질을 바꾸는 코드는 따로 있음

 

여기서 갑자기 의문

비가 오면 미끄러워지는 건데, 왜 내 마찰 코드랑 실제 도로 마찰이 따로 놀지?

이거 설계가 잘못된 거 아닌가?

 

ai 도움 받아서 파악해보니

판단 층 "비 오니 천천히" 마음먹음 ApplyWeatherProfile → LateralFriction 낮춤 → 자율주행이 곡선을 천천히 돌도록 판단 나 (자율주행)
물리 층 젖은 노면이 실제로 미끄러움 도로 PhysicsMaterial 교체 → 물리 엔진이 타이어를 실제로 미끄러지게 계산 팀원 (물리/도로)

 

내가 만진 LateralFriction은 판단 층

이건 자율주행 알고리즘이 "곡선을 얼마나 빨리 돌아도 되나"를 계산할 때 쓰는 가정값임 

즉, 자율주행의 머릿속에서만 쓰는 계산용 숫자

 

도로 PhysicsMaterial은 물리 층

언리얼 물리 엔진이 타이어와 노면의 실제 마찰을 계산할 때 쓰는 값

자율주행이 뭘 생각하든 상관없이, 물리적으로 차를 미끄러뜨리는 진짜 값

 

두 층이 합쳐져야 시뮬레이터의 진짜 가치

팀원이 물리 층을 마저 구현하면, 비 오는 날 이렇게 된다.

유저가 비 켬 → 날씨 변경
   │
   ├─[판단 층, 나]  LateralFriction ↓
   │     → 자율주행: "곡선 안전속도 낮춰야지" → 천천히 몰려고 시도
   │
   └─[물리 층, 팀원]  도로 물리 재질 = 비 버전 (마찰 ↓)
         → 물리 엔진: 타이어가 실제로 더 잘 미끄러짐

 

그러면 두 층이 서로를 검증함

  • 내 판단이 정확하면: 자율주행이 적절히 감속 → 미끄러운 도로에서도 안 미끄러지고 잘 돈다. 성공.
  • 내 판단이 너무 안일하면: 충분히 감속 안 함 → 미끄러운 도로에서 실제로 미끄러져 코스 이탈. 알고리즘의 약점이 드러남

 

트러블슈팅 모음

아직 없음

+ Recent posts