OSC 시나리오가 Behavior Tree가 되기까지
OpenSCENARIO DSL로 적은 시나리오는 사람이 읽는 텍스트입니다. 그런데 컴퓨터는 텍스트를 직접 실행하지 못합니다. 그 사이를 잇는 게 Behavior Tree(BT)로의 컴파일입니다 — 선언적 시나리오가 실행 가능한 트리로 변신하는 과정입니다.
문법이 트리 노드가 된다
핵심은 OSC의 구조 문법이 BT의 노드 타입에 1:1로 대응한다는 것입니다.
도식 렌더링 중…
- serial → Sequence 노드: 자식을 순서대로 실행, 하나가 끝나야 다음.
- parallel → Parallel 노드: 자식을 동시에 실행.
- 각 액션(spawn·move·emit·assert 등) → 잎(leaf) 노드: 실제 한 가지 동작.
이렇게 컴파일하면, 사람이 읽던 시나리오가 그대로 실행 트리가 됩니다 — 구조가 보존된 채로요.
잎의 언어 — SUCCESS / RUNNING / FAILURE
BT의 우아함은 단순한 신호 세 개에 있습니다. 모든 잎(액션)은 매 tick마다 셋 중 하나를 반환합니다.
도식 렌더링 중…
- SUCCESS — 동작 완료. 부모 Sequence는 다음 자식으로.
- RUNNING — 아직 진행 중(예: "목표까지 이동" 중). 다음 tick에 다시 평가.
- FAILURE — 실패. 부모가 복구 분기로 보내거나 시나리오를 종료.
이 세 신호의 조합만으로, 순서·동시성·반복·복구 같은 복잡한 시나리오 흐름이 표현됩니다. Nav2가 내비게이션을 BT로 조율하던 것과 정확히 같은 원리입니다 — 도메인만 다를 뿐.
Sequence 노드의 tick은 이렇게 단순합니다.
# serial -> Sequence: 자식을 순서대로, 하나 끝나야 다음
def tick_sequence(children):
for child in children:
status = child.tick()
if status != SUCCESS:
return status # RUNNING이면 대기, FAILURE면 중단
return SUCCESS # 전부 성공해야 성공
왜 BT로 컴파일하나
💡 선언과 실행의 분리 — OSC는 "무엇을"(선언), BT는 "어떻게 tick하며 실행할지"(실행)를 맡습니다. 이 분리 덕분에 시나리오 작성자는 트리 실행을 몰라도 되고, 실행 엔진은 시나리오 의미를 몰라도 됩니다. self-containment가 가능한 것도 이 컴파일 계층이 깔끔하기 때문입니다.
⚠️ RUNNING을 SUCCESS로 착각하지 마라 — 비동기 동작(이동·대기)을 동기처럼 다뤄 RUNNING을 즉시 SUCCESS로 처리하면, 동작이 끝나기 전에 다음 단계로 넘어가 버립니다. "이동이 끝날 때까지 RUNNING"을 정확히 반환해야 시나리오가 의도대로 흐릅니다.
한 줄 정리
📌 OSC 텍스트는 Behavior Tree로 컴파일돼 실행된다 —
serial→Sequence,parallel→Parallel, 각 액션은 매 tick마다 SUCCESS/RUNNING/FAILURE를 내는 잎이 된다. 이 세 신호의 조합이 순서·동시성·복구를 표현하며, 선언(OSC)과 실행(BT)의 분리가 DSL을 깔끔하게 만든다.
