1294.97 자식 실행 순서 시각화를 통한 디버깅
1. 실행 순서 시각화의 목적
자식 실행 순서 시각화는 Sequence와 Fallback 제어 노드가 매 Tick에서 자식 노드를 어떤 순서로, 어디까지 실행하는지를 시각적으로 표현하는 디버깅 기법이다. 반환 상태 추적이 “무엇이 반환되었는가“에 초점을 맞춘다면, 실행 순서 시각화는 “어떤 자식이 실행되었고 어떤 자식이 실행되지 않았는가“에 초점을 맞춘다. 이 기법은 특히 SequenceWithMemory의 건너뛰기 동작, ReactiveSequence의 재평가 패턴, Fallback의 조기 종료 지점을 진단하는 데 효과적이다(Faconti, 2022).
2. 실행 순서 기록기의 구현
2.1 Tick별 실행 이벤트 기록
struct ExecutionEvent {
int tick_number;
int execution_order; // Tick 내 실행 순서
std::string node_name;
BT::NodeStatus result;
};
class ExecutionOrderTracker {
public:
void onNodeExecuted(int tick, const std::string& name,
BT::NodeStatus result) {
events_.push_back({tick, order_in_tick_++, name, result});
}
void onTickBegin() {
order_in_tick_ = 0;
}
const std::vector<ExecutionEvent>& events() const {
return events_;
}
private:
std::vector<ExecutionEvent> events_;
int order_in_tick_ = 0;
};
2.2 추적용 래퍼 노드
class TrackedAction : public BT::SyncActionNode {
public:
TrackedAction(const std::string& name,
const BT::NodeConfig& config,
ExecutionOrderTracker& tracker)
: SyncActionNode(name, config), tracker_(tracker) {}
BT::NodeStatus tick() override {
BT::NodeStatus result = inner_action_->tick();
tracker_.onNodeExecuted(current_tick_, name(), result);
return result;
}
private:
ExecutionOrderTracker& tracker_;
int current_tick_ = 0;
};
3. 텍스트 기반 시각화
3.1 타임라인 다이어그램
Tick | action1 | action2 | action3 | Sequence
-----|-----------|-----------|-----------|----------
1 | [1] S | [2] R | | R
2 | | [1] R | | R
3 | | [1] S | [2] R | R
4 | | | [1] S | S
각 셀의 [n]은 해당 Tick 내에서의 실행 순서를 나타낸다. 빈 셀은 해당 Tick에서 실행되지 않았음을 의미한다. 이 다이어그램은 SequenceWithMemory의 동작을 명확히 보여준다:
- Tick 2에서 action1이 실행되지 않음 → Memory에 의한 건너뛰기
- Tick 3에서 action1이 실행되지 않음 → 여전히 건너뜀
- Tick 4에서 action1, action2 모두 미실행 → Memory 인덱스가 2에서 시작
3.2 ReactiveSequence의 타임라인
Tick | condition | action1 | action2 | RSeq
-----|------------|-----------|-----------|-------
1 | [1] S | [2] R | | R
2 | [1] S | [2] R | | R
3 | [1] S | [2] S | [3] R | R
4 | [1] S | [2] S | [3] S | S
ReactiveSequence에서는 매 Tick condition이 [1]로 실행된다. Memory 변형과 달리 condition이 한 번도 건너뛰어지지 않는다.
3.3 비교 시각화
[SequenceWithMemory]
Tick 1: action1[S] → action2[R]
Tick 2: action2[R] ← action1 건너뜀
Tick 3: action2[S] → action3[R]
[ReactiveSequence]
Tick 1: cond[S] → action1[R]
Tick 2: cond[S] → action1[R] ← cond 재평가
Tick 3: cond[S] → action1[S] → action2[R]
두 변형의 실행 패턴을 병렬 배치하면 Memory와 Reactive의 차이가 직관적으로 드러난다.
4. Fallback의 실행 순서 시각화
4.1 기본 Fallback 타임라인
Tick | alt1 | alt2 | alt3 | Fallback
-----|-----------|-----------|-----------|----------
1 | [1] F | [2] R | | R
2 | | [1] R | | R
3 | | [1] F | [2] R | R
4 | | | [1] S | S
FallbackNode(WithMemory)에서 alt1이 FAILURE 후 건너뛰어지고, alt2가 RUNNING을 유지하다가 FAILURE를 반환하면 alt3으로 진행한다.
4.2 ReactiveFallback 타임라인
Tick | alt1 | alt2 | alt3 | RFb
-----|-----------|-----------|-----------|------
1 | [1] F | [2] R | | R
2 | [1] F | [2] R | | R
3 | [1] S | [2] Halt | | S
ReactiveFallback에서 Tick 3의 alt1이 SUCCESS로 변하면, alt2가 Halt되고 즉시 SUCCESS를 반환한다. Halt 이벤트가 시각화에 포함된다.
5. 시각화 도구의 구현
5.1 타임라인 텍스트 생성기
class TimelineVisualizer {
public:
void addEvent(int tick, const std::string& node,
BT::NodeStatus status, int order) {
timeline_[tick][node] = {status, order};
}
void addHaltEvent(int tick, const std::string& node) {
timeline_[tick][node] = {BT::NodeStatus::IDLE, -1};
}
void print(const std::vector<std::string>& node_names) const {
// 헤더 출력
std::cout << std::setw(6) << "Tick";
for (const auto& name : node_names) {
std::cout << " | " << std::setw(12) << name;
}
std::cout << std::endl;
// 구분선
std::cout << std::string(6 + (node_names.size() * 15), '-')
<< std::endl;
// 데이터 출력
for (const auto& [tick, events] : timeline_) {
std::cout << std::setw(6) << tick;
for (const auto& name : node_names) {
auto it = events.find(name);
if (it != events.end()) {
auto [status, order] = it->second;
if (order == -1) {
std::cout << " | " << std::setw(12) << "Halt";
} else {
std::string cell = "[" + std::to_string(order) + "] "
+ statusChar(status);
std::cout << " | " << std::setw(12) << cell;
}
} else {
std::cout << " | " << std::setw(12) << "";
}
}
std::cout << std::endl;
}
}
private:
static std::string statusChar(BT::NodeStatus s) {
switch (s) {
case BT::NodeStatus::SUCCESS: return "S";
case BT::NodeStatus::FAILURE: return "F";
case BT::NodeStatus::RUNNING: return "R";
case BT::NodeStatus::SKIPPED: return "K";
default: return "?";
}
}
std::map<int, std::map<std::string,
std::pair<BT::NodeStatus, int>>> timeline_;
};
6. Groot2를 활용한 그래픽 시각화
6.1 실시간 트리 시각화
Groot2는 행동 트리를 그래프 형태로 실시간 시각화한다. 각 노드는 현재 상태에 따라 색상이 변경된다:
상태별 색상 매핑:
IDLE → 회색 (미실행)
RUNNING → 황색 (실행 중)
SUCCESS → 녹색 (성공)
FAILURE → 적색 (실패)
Sequence 노드의 자식 실행 순서는 좌→우 배치에 의해 시각적으로 표현되며, 현재 실행 중인 자식이 황색으로 강조된다. 이를 통해 Memory 인덱스의 현재 위치를 직관적으로 파악할 수 있다.
6.2 로그 재생
// 기록된 로그의 Groot2 재생
BT::FileLogger2 logger(tree, "execution_trace.btlog");
// 실행
while (running) {
tree.tickOnce();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
기록된 .btlog 파일을 Groot2에서 재생하면, Tick 단위로 트리의 상태 변화를 역방향/순방향으로 탐색할 수 있다. 이 기능은 간헐적 오류의 재현과 분석에 유용하다.
7. 실행 순서 이상 패턴의 식별
7.1 패턴 1: 예상외 건너뛰기
기대:
Tick 2: action1[S] → action2[R]
실제:
Tick 2: action2[R] ← action1이 건너뛰어짐
SequenceNode를 의도했으나 ReactiveSequence가 아닌 상황에서 action1이 건너뛰어지면, 이는 정상적 Memory 동작이다. 그러나 ReactiveSequence를 의도했다면 XML 태그를 확인해야 한다.
7.2 패턴 2: 예상외 재평가
기대 (SequenceWithMemory):
Tick 2: action2[R]
실제:
Tick 2: action1[S] → action2[R] ← action1이 재평가됨
Memory 동작을 의도했으나 자식이 재평가되면, <ReactiveSequence> 태그가 사용되었을 가능성이 높다.
7.3 패턴 3: 예상외 Halt
Tick 3: cond[S] → action1[R]
Tick 4: cond[S] → action1[R]
Tick 5: cond[F] ← 조건 변화
action1[Halt] ← 예상외 Halt
ReactiveSequence에서 조건 변화에 의해 행동 노드가 Halt되는 것은 정상 동작이나, 빈번한 Halt가 관찰되면 조건의 안정성을 검토해야 한다.
7.4 패턴 4: 실행되지 않는 자식
Tick 1: action1[F]
Sequence[F]
action2 미실행, action3 미실행 ← 조기 종료
Sequence에서 첫 번째 자식의 FAILURE가 후속 자식의 실행을 차단한다. 이것이 의도된 동작인지, 아니면 첫 번째 자식의 조기 실패가 문제인지를 판단해야 한다.
8. 실행 횟수 기반 분석
8.1 자식별 실행 횟수 히스토그램
전체 100 Tick에서의 실행 횟수:
condition: 100회 (매 Tick 실행) ████████████████████
action1: 85회 █████████████████
action2: 42회 ████████
action3: 15회 ███
action4: 3회 █
ReactiveSequence의 자식 실행 횟수는 위치에 따라 감소하는 패턴을 보인다. 앞쪽 자식은 매 Tick 실행되지만, 뒤쪽 자식은 앞쪽 자식의 연속적 SUCCESS에 의존하므로 실행 빈도가 낮다. 이 분포가 예상과 크게 다르면 제어 흐름의 이상을 의심할 수 있다.
8.2 Fallback 대안별 도달 빈도
전체 50 사이클에서의 대안 도달 횟수:
alt1 (Primary): 50회 (항상 시도) ██████████████████████████████
alt2 (Recovery1): 12회 (24%) ███████
alt3 (Recovery2): 3회 (6%) ██
alt4 (SafeStop): 0회 (0%)
alt4에 한 번도 도달하지 않았다면 최후 수단이 실제로 작동하는지 검증이 필요하다. 반대로 alt4에 빈번하게 도달한다면 상위 대안들의 신뢰성을 개선해야 한다.
참고 문헌
- Colledanchise, M., & Ogren, P. (2018). Behavior Trees in Robotics and AI: An Introduction. CRC Press.
- Faconti, D. (2022). BehaviorTree.CPP documentation and API reference. https://www.behaviortree.dev/