1294.84 BehaviorTree.CPP의 ReactiveSequence 클래스
1. ReactiveSequence 클래스의 개요
BehaviorTree.CPP v4에서 ReactiveSequence 클래스는 매 Tick마다 첫 번째 자식부터 재평가하는 비상태적(stateless) Sequence를 구현한다. SequenceNode와 달리 current_child_idx_를 사용하지 않으며, 항상 인덱스 0부터 순회를 시작한다. 이 클래스는 주로 안전 조건의 지속적 감시 패턴에서 사용된다(Faconti, 2022).
2. 클래스 선언
namespace BT {
class ReactiveSequence : public ControlNode {
public:
ReactiveSequence(const std::string& name,
const NodeConfig& config);
virtual ~ReactiveSequence() override = default;
static PortsList providedPorts() { return {}; }
private:
virtual BT::NodeStatus tick() override;
};
} // namespace BT
SequenceNode와 비교하여, current_child_idx_ 멤버 변수가 없고, halt() 메서드를 별도로 오버라이드하지 않는다는 점이 주요 차이이다. 부모 클래스 ControlNode의 halt()를 그대로 사용한다.
3. tick() 메서드의 상세 분석
3.1 전체 구현
NodeStatus ReactiveSequence::tick() {
bool all_skipped = true;
setStatus(NodeStatus::RUNNING);
for (size_t index = 0; index < childrenCount(); index++) {
TreeNode* current_child = children_nodes_[index];
const NodeStatus child_status = current_child->executeTick();
switch (child_status) {
case NodeStatus::RUNNING: {
// RUNNING 자식 이후의 모든 자식을 Halt
for (size_t j = index + 1; j < childrenCount(); j++) {
haltChild(j);
}
return NodeStatus::RUNNING;
}
case NodeStatus::FAILURE: {
// 모든 자식 초기화 후 FAILURE 반환
resetChildren();
return NodeStatus::FAILURE;
}
case NodeStatus::SUCCESS: {
all_skipped = false;
} break;
case NodeStatus::SKIPPED: {
// SKIPPED는 다음 자식으로 진행
} break;
}
}
resetChildren();
return all_skipped ? NodeStatus::SKIPPED : NodeStatus::SUCCESS;
}
3.2 동작 분석
-
항상 인덱스 0부터 시작: for 루프가 항상
index = 0에서 시작하므로, 매 Tick마다 모든 자식이 순서대로 재평가된다. -
RUNNING 자식 이후의 Halt: 자식이 RUNNING을 반환하면, 해당 인덱스 이후의 모든 자식을 Halt한다. 이는 이전 Tick에서 더 뒤쪽 자식이 RUNNING이었을 가능성을 처리한다.
-
FAILURE의 즉시 전파: 어떤 자식이든 FAILURE를 반환하면,
resetChildren()으로 모든 자식을 초기화하고 FAILURE를 반환한다. -
전체 SUCCESS: 모든 자식이 SUCCESS를 반환하면, 모든 자식을 초기화하고 SUCCESS를 반환한다.
4. SequenceNode와의 핵심 차이점
4.1 구현 수준의 차이
| 특성 | SequenceNode | ReactiveSequence |
|---|---|---|
| current_child_idx_ | 있음 (메모리) | 없음 |
| 순회 시작점 | current_child_idx_ | 항상 0 |
| 이전 SUCCESS 자식 | 건너뜀 | 매 Tick 재평가 |
| RUNNING 이후 자식 | Halt 불필요 | 명시적 Halt |
| halt() 오버라이드 | 예 (idx 초기화) | 아니오 (기본 사용) |
4.2 RUNNING 이후 자식 Halt의 필요성
ReactiveSequence에서 RUNNING 자식 이후의 Halt가 필요한 이유는 다음 시나리오에서 발생한다:
Tick 1: Cond→S, Action1→S, Action2→R (Action2가 RUNNING)
Tick 2: Cond→S, Action1→R (Action1이 RUNNING으로 변경)
Action2→Halt (더 이상 활성이 아닌 Action2 정리)
Tick 2에서 Action1이 RUNNING을 반환하면, 이전 Tick에서 RUNNING이었던 Action2를 Halt해야 한다. 이 Halt가 없으면 Action2가 정리되지 않은 상태로 남는다.
5. XML에서의 사용
<BehaviorTree ID="SafetyMonitoredMission">
<ReactiveSequence>
<Condition ID="IsBatteryOK"/>
<Condition ID="IsCommsActive"/>
<SubTree ID="Mission"/>
</ReactiveSequence>
</BehaviorTree>
<ReactiveSequence> 태그는 BehaviorTree.CPP에 기본 등록되어 있으며, 별도의 등록 없이 XML에서 사용할 수 있다.
6. 성능 고려 사항
ReactiveSequence는 매 Tick마다 모든 앞쪽 자식을 재평가하므로, 자식 수가 많을수록 Tick당 계산 비용이 증가한다. 앞쪽에 배치되는 조건 노드는 경량으로 구현하여 재평가 비용을 최소화해야 한다. 특히 센서 읽기, 네트워크 통신 등 비용이 높은 연산을 조건 노드 내부에서 직접 수행하는 것은 피해야 하며, 블랙보드를 통해 비동기적으로 갱신된 값을 읽는 방식이 권장된다(Faconti, 2022).
참고 문헌
- 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/