1295.76 BehaviorTree.CPP에서의 ReactiveFallback 구현

1. ReactiveFallback 클래스

BehaviorTree.CPP에서 ReactiveFallback은 ControlNode를 상속하는 내장 제어 노드이다. 일반 Fallback(FallbackNode)이 메모리를 가지는 반면, ReactiveFallback은 매 Tick에서 항상 첫 번째 자식부터 재평가한다.

class ReactiveFallback : public ControlNode
{
public:
    ReactiveFallback(const std::string& name)
        : ControlNode(name, {}) {}
    
    ReactiveFallback(const std::string& name, const NodeConfig& config)
        : ControlNode(name, config) {}

private:
    NodeStatus tick() override;
    void halt() override;
};

ReactiveSequence와 마찬가지로 추가 포트를 정의하지 않는다.

2. tick() 메서드의 구현

NodeStatus ReactiveFallback::tick()
{
    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++)
                {
                    if (children_nodes_[j]->status() == NodeStatus::RUNNING)
                    {
                        haltChild(j);
                    }
                }
                return NodeStatus::RUNNING;
            }
            
            case NodeStatus::SUCCESS:
            {
                // 이후의 RUNNING 자식에 Halt 전파
                for (size_t j = index + 1; j < childrenCount(); j++)
                {
                    if (children_nodes_[j]->status() == NodeStatus::RUNNING)
                    {
                        haltChild(j);
                    }
                }
                return NodeStatus::SUCCESS;
            }
            
            case NodeStatus::FAILURE:
            {
                // 다음 자식으로 계속
                break;
            }
            
            default:
                break;
        }
    }
    
    // 모든 자식이 FAILURE
    return NodeStatus::FAILURE;
}

ReactiveSequence와 대칭적인 구조이다. 핵심 차이는 다음과 같다.

분기 조건ReactiveSequenceReactiveFallback
SUCCESS다음 자식으로 계속Halt 후 SUCCESS 반환
FAILUREHalt 후 FAILURE 반환다음 자식으로 계속
RUNNINGHalt 후 RUNNING 반환Halt 후 RUNNING 반환
모든 자식 완료모두 SUCCESS → SUCCESS모두 FAILURE → FAILURE

3. ReactiveSequence와의 코드 대칭

ReactiveSequence와 ReactiveFallback의 tick() 메서드는 SUCCESSFAILURE의 역할이 교환된 대칭적 구조이다.

ReactiveSequence:
  SUCCESS → continue
  FAILURE → halt remaining, return FAILURE
  all SUCCESS → return SUCCESS

ReactiveFallback:
  FAILURE → continue
  SUCCESS → halt remaining, return SUCCESS
  all FAILURE → return FAILURE

이 대칭성은 Sequence와 Fallback의 논리적 관계(AND vs OR)를 반영한다.

4. halt() 메서드의 구현

void ReactiveFallback::halt()
{
    haltChildren();
    ControlNode::halt();
}

ReactiveSequence의 halt()와 동일하다. 모든 RUNNING 상태의 자식에 Halt를 전파한다.

5. XML에서의 ReactiveFallback 정의

<BehaviorTree ID="ReactiveFallbackExample">
    <ReactiveFallback>
        <Sequence>
            <IsEmergency />
            <EmergencyStop />
        </Sequence>
        <Sequence>
            <IsBatteryLow />
            <ReturnToBase />
        </Sequence>
        <ExecuteMission waypoints="{mission_waypoints}" />
    </ReactiveFallback>
</BehaviorTree>

<ReactiveFallback> 태그 내에 우선순위 순서대로 자식 노드를 배치한다.

6. 일반 FallbackNode와의 코드 차이

일반 FallbackNoderunning_child_ 변수를 유지한다.

// FallbackNode의 tick() (개념적)
NodeStatus FallbackNode::tick()
{
    for (size_t index = running_child_; index < childrenCount(); index++)
    {
        auto child_status = children_nodes_[index]->executeTick();
        
        if (child_status == NodeStatus::RUNNING)
        {
            running_child_ = index;
            return NodeStatus::RUNNING;
        }
        
        if (child_status == NodeStatus::SUCCESS)
        {
            running_child_ = 0;
            return NodeStatus::SUCCESS;
        }
    }
    
    running_child_ = 0;
    return NodeStatus::FAILURE;
}

ReactiveFallback에는 running_child_가 없으며, 항상 index = 0부터 시작한다. 이것이 “reactive“의 의미이다.

7. ROS2 Nav2에서의 ReactiveFallback 사용

Nav2의 행동 트리에서 ReactiveFallback은 우선순위 기반 행동 선택에 사용된다.

<!-- Nav2의 전형적인 ReactiveFallback 사용 -->
<ReactiveFallback>
    <Sequence>
        <GoalUpdated />
        <ComputePathToPose goal="{goal}" path="{path}" 
                           planner_id="GridBased" />
    </Sequence>
    <FollowPath path="{path}" controller_id="FollowPath" />
</ReactiveFallback>

목표가 갱신되면 경로를 재계획하고, 갱신되지 않으면 기존 경로를 추종한다. 목표 갱신이 경로 추종보다 높은 우선순위를 가지므로, 새로운 목표가 설정되면 기존 경로 추종이 Halt되고 경로 재계획이 수행된다.

8. 우선순위 선점의 동작 추적

<ReactiveFallback>
    <Sequence name="Emergency">
        <IsEmergency />
        <EmergencyAction />
    </Sequence>
    <NormalAction />
</ReactiveFallback>
Tick 1: Emergency/IsEmergency → FAILURE
        → Emergency → FAILURE
        NormalAction → RUNNING
        → ReactiveFallback → RUNNING

Tick 2: Emergency/IsEmergency → SUCCESS
        Emergency/EmergencyAction → RUNNING
        → Emergency → RUNNING
        NormalAction은 Tick 미수신
        → NormalAction.halt() (이전 RUNNING)
        → ReactiveFallback → RUNNING

Tick 3: Emergency/IsEmergency → FAILURE
        → Emergency → FAILURE
        NormalAction → RUNNING (새로 시작)
        → ReactiveFallback → RUNNING

Tick 2에서 비상 상황이 감지되어 NormalAction이 선점되고, Tick 3에서 비상 해소 시 NormalAction이 자동으로 재시작된다.

9. 주의 사항

  1. 최하위 자식의 역할: ReactiveFallback의 마지막 자식은 모든 상위 조건이 FAILURE일 때 실행되는 기본(default) 행동이다. 이 행동은 RUNNING을 반환하여 지속적으로 실행될 수 있다.

  2. 조건-행동 쌍의 Sequence 구성: ReactiveFallback의 자식으로 “조건 → 행동“의 Sequence를 배치하는 것이 일반적이다. 조건이 FAILURE이면 해당 Sequence가 FAILURE를 반환하여 다음 분기로 진행한다.

  3. 상위 분기의 조건이 빈번히 전환되면 하위 분기의 행동이 반복 Halt된다: 조건에 히스테리시스를 적용하여 불필요한 선점을 방지하라.