Post

State Machine: 상황별 규칙으로 상태와 경로를 결정하기

State Machine: 상황별 규칙으로 상태와 경로를 결정하기

UNICORN Racing 팀은 다양한 상황에 알맞게 대응하는 robust한 주행을 목표로 합니다. 주행 중 마주치는 각종 상황과 planner의 출력을 기반으로 예측 가능한 판단을 내려 주행의 목표와 경로, 속도를 결정하는 것이 State Machine의 역할입니다.

State Machine은 정교한 이론이나 최적화가 아니라, 레이싱에서의 직관과 상황별 규칙을 정리해 결론을 도출합니다. 본 포스트에서는 이 규칙들이 어떤 기준으로 판단하고, 어떻게 조합되어 하나의 결정을 내리는지, 그 작동 원리를 다룹니다.

State Machine이 필요한 이유

자율주행 레이싱 중에는 혼자 잘 달리는 것 외에도 다양한 상황에 대처하는 능력이 요구됩니다.

cost를 기반으로 판단과 계획을 함께 수행하는 모듈은 제어까지 한 번에 최적화할 수 있다는 장점이 있습니다. 다만 그 결과를 미리 예측하기 어렵다는 특성이 있습니다. UNICORN Racing은 예측 가능한 결과를 목표로 하며 계획과 제어를 분리해 사용하기 때문에, cost에 의존하지 않고 다양한 상황에 대처하는 planner들을 각각 구축해 운용합니다.

이 구조에서는 여러 planner의 결과와 현재 상황 정보를 함께 판단해, 주행의 목표와 실제로 주행할 경로 및 속도를 결정하는 결정자가 필요합니다. State Machine이 바로 그 결정자입니다.

State Machine 구조 여러 planner의 후보 경로와 상황 정보를 입력받아 하나의 경로를 선택하는 State Machine 구조

State Machine은 매 프레임 행동 상태와 행동에 사용할 경로를 한 쌍으로 결정합니다. 예를 들어 “행동은 앞차 뒤따라가기(TRAILING)지만 사용하는 경로는 글로벌 레이스라인”처럼, 상태와 경로를 분리한 뒤 짝지어 관리합니다.

STATE의 종류

각 상태는 특정 상황에 대응하도록 정의되어 있습니다. State Machine이 다루는 주요 상태와 그 상황·목적은 다음과 같습니다.

상태목적상황
🔵 GB_TRACK글로벌 레이스라인을 추종• 전방 경로에 간섭이 있는 대상이 없음
• 레이스라인에 가깝게 위치
🟡 TRAILING전방 안전거리 유지• 전방 경로에 장애물이 있으나 추월 불가
🔴 OVERTAKE회피 라인으로 상대를 추월• 안전한 회피 경로가 존재
🟢 RECOVERY글로벌 레이스라인으로 부드럽게 복귀• 글로벌 레이스라인에서 이탈
⚪ START직접 튜닝한 출발 궤적으로 초반 가속• 빠른 가속 출발 희망
• 출발 위치가 레이스라인과 멀리 떨어져 있음
⚪ FTGONLY경로 계획 없이 reactive하게 주행• 추월 불가 상태가 오래 지속됨

판단에 사용되는 경로

판단과 전이는 결국 여러 후보 경로 중 무엇을 쓸지 고르는 과정입니다.

State Machine은 경로를 직접 만들지 않습니다. 글로벌 레이스라인, 회피 경로, 복귀 경로, 출발 궤적은 각각 별도의 planner가 생성해 토픽으로 발행하고, State Machine은 이를 구독해 저장합니다. 각 경로는 담당 planner가 매 프레임 새로 내보내며, State Machine은 종류별로 캐시에 보관해 판단의 후보로 삼습니다.

받은 경로는 그대로 쓰지 않고 곡률로부터 속도 프로파일을 계산해 입힙니다. planner가 낸 경로는 지나갈 점들의 모양(기하)만 담고 있어 속도 정보가 없기 때문입니다. 이때 상태의 성격에 따라 속도 여유를 다르게 두며, 복귀 경로의 경우에는 모터·브레이크 한계를 절반(safety_factor 0.5)으로 줄인 보수적인 속도를 입혀 복귀 중 안정성을 우선합니다.

이렇게 속도까지 입혀 캐시에 보관된 후보 경로들이 판단 기준의 평가 대상이 됩니다.

판단 기준

관련 코드: state_machine/state_machine/state_machine_node.py_check_* 메서드

각 STATE는 대응하는 상황과 목적이 다르므로, 언제 어떤 상태로 넘어갈지를 정하려면 현재 상황을 몇 가지 항목으로 나눠 검사해야 합니다. State Machine은 이를 판단 기준으로 정의해 두고, 모든 전이 규칙을 이 기준들의 조합으로 구성합니다.

  • 전방 장애물 존재 여부 — 전방 일정 거리 안에 상대 차량이 있는지, 그리고 그 상대에게 가까워지고 있는지를 검사합니다.
  • 경로의 충돌 안전성 — 후보 경로를 따라갈 때 충돌이 없는지 판정합니다. 현재 장애물 위치만 보는 것이 아니라, 상대의 예측 궤적을 바탕으로 상대와 자차의 예측 위치와 충돌까지 걸리는 시간(TTC)을 함께 계산합니다. 반응에는 지연이 있고 그 사이 상대도 이동하므로, 지금 비어 있다고 해서 안전한 것은 아니기 때문입니다.
  • 레이스라인 근접 여부 — 차량이 위치와 진행 방향(heading) 모두 글로벌 레이스라인에 충분히 붙어 있는지 검사합니다. heading까지 함께 보는 이유는, 라인 위에 있어도 비스듬히 향하고 있으면 아직 안정적으로 붙은 상태가 아니기 때문입니다.
  • 경로 추종 가능성 — 차량이 실제로 그 경로를 추종할 수 있는지 검사합니다. 경로가 차량에 충분히 가까워야 하고, 경로의 끝점이 아직 차량 앞에 남아 있어야 합니다. 경로가 존재하더라도 너무 멀거나 이미 그 끝점을 지나쳤으면 따라갈 수 없습니다.
  • 경로의 최신성 — planner가 낸 경로가 오래된 것은 아닌지 검사합니다. planner가 매 프레임 새로 내놓아야 유효한 경로이며, 일정 시간 갱신이 끊긴 경로는 더 이상 신뢰하지 않습니다.
  • 저속 정체 지속 여부TRAILING속도가 지나치게 낮은 상태가 일정 시간 이상 지속되는지 검사합니다.

전이: 판단 기준을 조합해 상태 결정

관련 코드: state_machine/state_machine/state_transitions.py

전이 함수는 판단 기준을 조합해 다음 상태와 다음에 사용할 경로를 한 쌍으로 반환합니다. 이때 상태와 경로가 항상 같은 것은 아닙니다. 상태는 지금 어떤 행동을 할지를, 경로는 그 행동에 어떤 후보 경로를 쓸지를 나타내며, 하나의 판정 안에서 짝지어 결정됩니다. 예를 들어 앞이 막혀 뒤따라가는 TRAILING 상태라도 실제로 추종하는 경로는 복귀 경로일 수 있습니다.

STATE별 함수: 유지 여부만 판정

각 상태는 자신에게 맞는 전이 함수를 하나씩 가지며, 현재 활성화된 상태에 해당하는 함수가 그 프레임의 전이를 담당합니다. 이 함수들은 대부분 현재 상태를 계속 유지할지 여부만 판정합니다. 유지 조건이 성립하면 그 상태에 머무르고, 조건이 깨지면 다음 상태를 스스로 결정하는 대신 공통 판정 함수로 넘깁니다.

전이 함수담당 상태유지 조건
GlobalTrackingTransitionGB_TRACK공통 판정 함수로 위임
TrailingTransitionTRAILING공통 판정 함수로 위임
OvertakingTransitionOVERTAKE회피 경로가 유효하고 앞차가 남아 있으면 유지
• 조건이 깨지면 공통 판정 함수로 위임
RecoveryTransitionRECOVERY복귀 경로가 유효하고 아직 레이스라인에서 떨어져 있으면 유지
• 조건이 깨지면 공통 판정 함수로 위임
StartTransitionSTART시작 경로가 안전하고 그 위에 올라타 있으면 유지
• 조건이 깨지면 공통 판정 함수로 위임
FTGOnlyTransitionFTGONLY공통 판정 함수로 위임

STATE 전이

상태와 경로의 실제 결정은 상황을 두 갈래로 나눈 공통 판정 함수 두 개에 모여 있습니다. 전방에 장애물이 없으면 NonObstacleTransition, 있으면 ObstacleTransition이 처리하며, 위 상태별 함수들은 이 둘 중 하나로 흘러듭니다.

  • NonObstacleTransition (장애물 없음): 앞을 막는 상대가 없으므로 충돌은 따지지 않고, 레이스라인에 붙어 있으면 GB_TRACK, 아직 떨어져 있고 복귀 경로가 최신성과 추종 가능성을 통과하면 RECOVERY로 복귀를 이어갑니다.
  • ObstacleTransition (장애물 있음): 레이스라인, 복귀 경로, 회피 경로를 차례로 두고 각각 최신성과 충돌 안전성 같은 판단 기준으로 평가해, 통과하는 후보에 따라 상태와 경로를 함께 정합니다.

ObstacleTransition은 후보 경로를 위에서부터 평가해, 앞 단계가 성립하지 않을 때에만 다음으로 내려갑니다.

레이스라인에 붙어 있고 그 경로가 충돌 안전성을 통과하면 → GB_TRACK

② 아니라면, 복귀 경로가 최신성과 충돌 안전성을 통과하면 → RECOVERY

③ 그것도 아니라면, 회피 경로가 최신성과 충돌 안전성을 통과하면 → OVERTAKE

④ 어느 것도 통과하지 못하면 → TRAILING

마지막 TRAILING에서도 실제 추종하는 경로는 복귀 경로나 레이스라인이 될 수 있어, 상태와 경로가 서로 다른 쌍으로 반환됩니다.

Local Waypoints

관련 코드: state_machine/state_machine/states.py

판단과 전이로 경로 출처가 정해지면, 그 출처의 캐시에서 실제로 컨트롤러에 넘길 로컬 경로를 만듭니다. 회피·복귀 경로 캐시에서 현재 차량과 가장 가까운 점을 찾아, 그 지점부터 정해진 개수(n_loc_wpnts, 기본값 80)만큼 잘라냅니다.

잘라낸 점이 n_loc_wpnts에 못 미치면, 그 뒤를 글로벌 레이스라인에서 이어붙여 길이를 맞춰 회피가 끝나는 지점부터 자연스럽게 레이스라인으로 연결됩니다. 만약 캐시가 무효화되어 잘라낼 경로가 없으면 빈 경로를 반환하고, 상위 루프에서 경로 출처를 글로벌 레이스라인으로 되돌려 대체함으로써 컨트롤러가 멈추지 않게 합니다.

규칙을 견고하게 만드는 장치들

State 규칙은 직관에서 출발하지만, 실제 차량에서 사고 없이 주행을 마치려면 그 직관이 순간의 노이즈나 지연에 흔들리지 않아야 합니다. UNICORN Racing 스택은 아래 장치들로 이를 보완합니다.

  • 실행 가능한 경로만 선택합니다. 충돌 검사(예측 포함), 경로 위 여부, 최신성을 통과하지 못한 경로는 애초에 컨트롤러로 나가지 않습니다.
  • 상태와 경로 출처를 분리합니다. 앞서 다룬 대로, 상태가 바뀌어도 추종하는 경로는 연속적으로 유지되어 목표가 튀지 않습니다.
  • 경계에서의 진동을 억제합니다(hysteresis). 조건이 경계에서 깜빡여도 상태가 진동하지 않도록, TTL·타이머로 잠시 유지합니다. 예를 들어 상대를 지나친 직후에도 잠깐은 OVERTAKE를 유지합니다.
  • 지연을 예측으로 보완합니다. 반응하는 동안 상대와 자차가 움직이는 것을 감안해, 충돌 검사에 예측 위치와 충돌까지의 시간(TTC)을 함께 사용합니다.
  • 빈 경로는 절대 내보내지 않습니다. 어떠한 이유로 선택한 경로가 비면, 상태 판단은 유지한 채 경로만 글로벌 레이스라인으로 채워 컨트롤러가 멈추지 않게 합니다.

한계

이 구조가 장점만 있는 것은 아닙니다. 예측 가능한 거동으로 robust함을 얻는 대신, 상대를 고려한 전략적 기동을 만들기 어렵다는 한계를 함께 가집니다. 상대와 자차의 상태, 트랙 정보를 함께 놓고 타임스텝 단위로 행동을 최적화하는 방식과 달리, UNICORN Racing 스택은 기하 기반 planner로 경로를 생성하고 곡률 기반으로 속도를 입힌 뒤 별도의 기하 기반 제어기로 추종합니다. 경로 생성기, 평가자, 제어기가 각각 분리되어 있어, 상대의 움직임에 맞춰 경로와 속도를 하나의 의도로 묶어 계획하기가 어렵습니다.

특히 곡률에서 얻은 속도에는 주행 의도가 담기지 않습니다. 감속하려는 궤적이라도 곡률이 작으면 그대로 가속하므로, 감속이나 가속의 타이밍 판단은 TRAILING 같은 종방향 제어에 위임됩니다. 이 과정에서 동적 장애물을 회피할 때 필요한 가속 시점을 놓치는 경우가 생깁니다. 상대와의 격차가 크지 않은 상위 리그에서는 이런 미세한 지연이 그대로 손해로 이어져, 추월에 실패하는 원인이 되기도 합니다.

마무리

이 글에서는 여러 planner가 내놓은 후보 경로와 상황 정보를 판단 기준(_check_*)으로 평가해, 매 프레임 상태와 경로를 한 쌍으로 결정하는 State Machine의 작동 원리를 다뤘습니다. STATE별 함수는 유지 여부만 판정하고 실제 결정은 두 공통 판정 함수에 모여 있으며, hysteresis·TTC 예측·빈 경로 방지 같은 장치들이 규칙 기반 판단을 실전에서 견고하게 만듭니다.

This post is licensed under CC BY 4.0 by the author.