차량이 주행 중 겪는 위험 상황을 실시간으로 감지하고, 그 사건을 구조화된 데이터로 로깅 담당자에게 전달하는 컴포넌트를 구현
미끄러짐, 횡방향 G, 차체 회전 불안정, 경로 이탈, 전복, 급조작, 도로 밖 추락 일곱 가지를 감지함
-> 어느 지점에서, 어떤 종류의 위험이 발생했는지를 정량적으로 남겨야 "이 커브에서 반복적으로 미끄러진다", "이 구간에서 경로를 벗어난다" 같은 분석이 가능해짐
즉 주행 데이터를 분석 가능한 형태로 만들기 위한 입력 장치!
일단 로깅 담당자에게 알려야하니 가장 먼저 한 일은 코드를 짜는 게 아니라 위험 이벤트의 데이터 형태를 정함
USTRUCT(BlueprintType)
struct FHazardEvent
{
GENERATED_BODY()
double TimeStamp = 0.0; // 발생 시각 (시계열 키)
int32 ActiveFlags = 0; // 어떤 위험들이 동시에 활성인지 (비트플래그)
EHazardPhase Phase; // 진입 / 지속 / 해제
FVector WorldLocation; // 발생 위치 (공간 분석용)
float Speed = 0.f;
float SlipAngleDeg = 0.f; // 측정값 - 임계 초과 여부와 무관하게 항상 채움
float LateralG = 0.f;
float YawRate = 0.f;
float CrossTrackError = 0.f;
float RollDeg = 0.f;
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(
FOnHazardDetected, const FHazardEvent&, HazardEvent);
위험 종류를 단일 enum이 아니라 비트플래그(ActiveFlags)로 둔 게 핵심 결정
실제로 미끄러지면서 동시에 횡G가 한계에 닿는 상황이 자주 발생하기 때문에 한 사건에 여러 위험이 겹쳐 기록될 수 있어야 분석이 정확해짐
측정값을 임계 초과 여부와 무관하게 항상 채우는 것도 의도적
위험까지 가진 않았지만 위태로웠던 구간을 나중에 분석할 수 있어야 하기 때문!
왜 이 일곱 가지로 골랐나
AI에게 도움을 받아 차량 거동에서 추출할 수 있는 지표 중 서로 다른 실패 양상을 잡는 것들을 고름
핵심은 겹치지 않게, 다른 면을 보도록
- 미끄러짐(슬립각)은 차체가 향한 방향과 실제 진행 방향의 어긋남 -> 이미 미끄러지고 있는 결과를 잡음
- 횡방향 G는 옆으로 쏠리는 가속도로 미끄러지기 직전의 전조를 잡음
둘을 같이 쓰면 위험이 어떻게 전개됐는지 시간 순서가 데이터에 남는다
- 휘청거림(Yaw 회전 속도)은 스핀이나 오버스티어처럼 통제를 잃고 회전하는 양상을
- 경로 이탈은 의도한 경로를 얼마나 못 따라가는가를
- 전복은 차가 뒤집히는 상황을
- 급조작은 알고리즘이 얼마나 거칠게 조작하는지를 잡음
(추락은 처음 설계엔 없었지만 테스트 중 차가 종종 도로 밖으로 떨어지는 걸 관찰하고 추가함)
대표로 미끄러짐 측정 코드를 보면 이렇다
차체 방향과 속도 방향 두 벡터 사이 각도를 내적으로 구함

void UHazardDetectorComponent::CheckSlip()
{
AActor* Car = GetOwner();
if (!Car) return;
// 실제 진행 방향 = 속도 벡터 (Z는 버림: 경사가 각도 오염 방지)
FVector Velocity = Car->GetVelocity();
FVector VelFlat = FVector(Velocity.X, Velocity.Y, 0.f);
float Speed = VelFlat.Size();
// 저속이면 계산 안 함 (속도 벡터가 0에 가까워 각도가 튐)
if (Speed < MinSpeedForSlip)
{
if (bSlipActive)
{
bSlipActive = false;
BroadcastSlip(EHazardPhase::Exit, 0.f, Car, Speed);
}
return;
}
// 차체 방향 (코 방향, Z는 버림)
FVector Forward = Car->GetActorForwardVector();
FVector FwdFlat = FVector(Forward.X, Forward.Y, 0.f);
// 두 방향 사이 각도 (내적 → acos)
VelFlat.Normalize();
FwdFlat.Normalize();
float Dot = FMath::Clamp(FVector::DotProduct(FwdFlat, VelFlat), -1.f, 1.f);
float SlipAngleDeg = FMath::RadiansToDegrees(FMath::Acos(Dot));
// 진입/해제 판정 (히스테리시스)
if (!bSlipActive)
{
if (SlipAngleDeg >= SlipEnterDeg)
{
bSlipActive = true;
BroadcastSlip(EHazardPhase::Enter, SlipAngleDeg, Car, Speed);
}
}
else
{
if (SlipAngleDeg < SlipExitDeg)
{
bSlipActive = false;
BroadcastSlip(EHazardPhase::Exit, SlipAngleDeg, Car, Speed);
}
}
}
저속에서 계산을 건너뛰는 가드(MinSpeedForSlip)가 있는 이유는 속도 벡터가 0에 가까울 때 방향 각도가 의미 없이 튀기 때문
정상 동작을 위협이 아니라 노이즈로 인식하지 않으려면 이 가드가 필요했음
단일 기준선 대신 진입/해제 이중 임계값 사용
단일 기준선을 쓰면 측정값이 기준 근처에서 떨릴 때 위험과 안전이 1초에 수십 번 반복돼 로그가 무의미해짐
// 진입 기준: 이 각도를 넘으면 위험 시작
float SlipEnterDeg = 15.f;
// 해제 기준: 이 각도 아래로 떨어지면 위험 끝 (진입보다 낮게 — 깜빡임 방지)
float SlipExitDeg = 10.f;
진입은 15도, 해제는 10도로 벌려두면 그 사이 10~15도 구간에서는 상태가 흔들리지 않음
위험이 시작된 순간과 끝난 순간만 한 번씩 기록됨!
(임계값은 모두 UPROPERTY(EditAnywhere)로 노출해 코드 수정 없이 에디터에서 튜닝할 수 있게 함)
횡G 1.0g는 실제로 차를 미끄러뜨렸을 때 측정된 1.2g와 정상 주행 노이즈 0.9 사이의 경계로 잡음
화면 경고와 데이터 로깅은 목적이 다름
화면 경고는 사람이 보는 순간에만 의미가 있고, 데이터 로깅은 주행이 끝난 뒤 분석하기 위한 것!
void UHazardDetectorComponent::BroadcastSlip(EHazardPhase Phase,
float SlipAngleDeg, AActor* Car, float Speed)
{
FHazardEvent Ev;
Ev.TimeStamp = GetWorld()->GetTimeSeconds(); // 언제
Ev.ActiveFlags = (int32)EHazardFlags::Skid; // 무슨 위험
Ev.Phase = Phase; // 진입 / 해제
Ev.WorldLocation = Car->GetActorLocation(); // 어디서
Ev.Speed = Speed;
Ev.SlipAngleDeg = SlipAngleDeg; // 실제 측정값
OnHazardDetected.Broadcast(Ev); // 구독한 로거에 배달
}
시각을 남기는 것은 위험이 어떤 순서로 전개됐는지 시계열로 재구성하기 위해서
위치를 남기는 것은 특정 커브에서 반복적으로 같은 위험이 발생한다 같은 공간 분석을 위해서
실제 측정값(SlipAngleDeg)을 함께 남기기 때문에 위험의 정도까지 분석할 수 있음
감지와 기록을 델리게이트로 분리
위 코드 마지막 줄 OnHazardDetected.Broadcast(Ev)가 분리의 핵심
감지 컴포넌트는 위험을 감지하면 이벤트를 방송하기만 하고, 데이터 로거는 그 방송을 구독해 받음
둘은 델리게이트로만 연결돼 서로의 내부 구현을 모른다
감지 로직이 바뀌어도 로거 코드를 건드리지 않고, 로거의 기록 방식이 바뀌어도 감지 코드를 건드리지 않음
같은 프로젝트의 터널 시스템에서 이미 이 패턴으로 차량 폰과 컴포넌트를 분리했고, 그 경험이 여기서 같은 구조 선택의 근거가 됐음
같은 골격을 반복하되 공용 함수로 묶음
위험별 감지 함수는 모두 같은 골격을 공유
측정 -> 진입/해제를 판정 -> 이벤트를 채워 방송
처음 한 종류(미끄러짐)를 이 골격으로 완성하고 검증한 뒤, 나머지는 측정식과 식별자만 바꿔 끼움
다만 전용 데이터 칸이 필요 없는 단순 지표(경로이탈, 전복, 급조작, 추락)는 방송 함수를 매번 따로 만들지 않고 하나로 묶음
void UHazardDetectorComponent::BroadcastSimple(EHazardFlags Flag,
EHazardPhase Phase, AActor* Car, float Speed,
const FColor& Color, const FString& Label, float Value)
{
FHazardEvent Ev;
Ev.TimeStamp = GetWorld()->GetTimeSeconds();
Ev.ActiveFlags = (int32)Flag;
Ev.Phase = Phase;
Ev.WorldLocation = Car->GetActorLocation();
Ev.Speed = Speed;
// 지표 종류에 맞는 전용 칸만 채우기
if (Flag == EHazardFlags::LaneDeparture) Ev.CrossTrackError = Value;
else if (Flag == EHazardFlags::RolloverRisk) Ev.RollDeg = Value;
Ev.Severity = FMath::Clamp(Value / 100.f, 0.f, 1.f);
OnHazardDetected.Broadcast(Ev);
}
경로 이탈 값은 새로 계산하지 않음
도로 중심선에서 벗어난 거리는 자율주행 컴포넌트(SplineFollower)가 조향 보정을 위해 이미 계산하고 있었기 때문에, 그 값을 게터로 노출해 가져다 씀
float CTE = 0.f;
if (USplineFollowerComponent* Spline =
Car->FindComponentByClass<USplineFollowerComponent>())
{
CTE = Spline->GetCrossTrackError();
}
'프로젝트 > 자율주행 위험구간 분석 시뮬레이터' 카테고리의 다른 글
| 전방 감지 기반 속도 조절 시스템 (과속방지턱과 NPC 차량) (0) | 2026.05.22 |
|---|---|
| [트러블슈팅] 캐싱·결합·잘못 짚은 변수·가중 지수 (0) | 2026.05.19 |
| [트러블슈팅] 터널과 날씨 시스템의 충돌 (0) | 2026.05.19 |
| [트러블슈팅] 날씨 별 속도가 동일, 마찰력 줄어들면 코너에서 추락 (0) | 2026.05.18 |
| 날씨 시스템 구현하기 (0) | 2026.05.17 |
