1295.39 ReactiveFallback의 의사 코드

1. 핵심 의사 코드

ReactiveFallback 노드의 Tick 동작을 구현하는 의사 코드는 다음과 같다.

function ReactiveFallback::tick():
    found_non_failure ← false
    result_status ← FAILURE
    result_index ← -1
    
    // Phase 1: 첫 번째 자식부터 순차적으로 Tick 전파
    for i ← 0 to childCount() - 1:
        child_status ← child(i).tick()
        
        if child_status ≠ FAILURE:
            // 비실패 자식 발견: Tick 전파 중단
            found_non_failure ← true
            result_status ← child_status
            result_index ← i
            break
    
    // Phase 2: 비실패 자식 이후의 RUNNING 자식에 Halt 전파
    if found_non_failure:
        for j ← result_index + 1 to childCount() - 1:
            if child(j).status() == RUNNING:
                child(j).halt()
    
    // Phase 3: 반환값 결정
    return result_status

2. 의사 코드의 상세 해설

2.1 Phase 1: 순차적 Tick 전파

for i ← 0 to childCount() - 1:
    child_status ← child(i).tick()

매 Tick에서 반드시 인덱스 i = 0부터 시작한다. 이것이 ReactiveFallback의 “반응형” 특성을 결정하는 핵심 부분이다. 일반 Fallback에서는 이전 Tick에서 RUNNING을 반환한 자식의 인덱스를 기억하고 해당 인덱스부터 재개하지만, ReactiveFallback에서는 그러한 인덱스 기억이 존재하지 않는다.

if child_status ≠ FAILURE:
    found_non_failure ← true
    result_status ← child_status
    result_index ← i
    break

자식이 SUCCESS 또는 RUNNING을 반환하면 Tick 전파를 즉시 중단(break)한다. 이 자식이 현재 Tick에서 선택된 행동이다. FAILURE를 반환한 경우에만 다음 자식으로 진행한다.

2.2 Phase 2: Halt 전파

if found_non_failure:
    for j ← result_index + 1 to childCount() - 1:
        if child(j).status() == RUNNING:
            child(j).halt()

비실패 자식이 발견된 경우, 그 이후 인덱스의 자식 중 RUNNING 상태인 자식에 Halt를 전파한다. 이 단계가 없으면 이전 Tick에서 실행 중이던 하위 우선순위 행동이 정리되지 않아 자원 누수가 발생한다.

Halt 대상 판별 시 child(j).status() == RUNNING 조건을 확인하는 것은, 이미 IDLE 또는 FAILURE 상태인 자식에는 Halt가 불필요하기 때문이다. Halt는 RUNNING 상태의 자식, 즉 진행 중인 비동기 행동을 보유한 자식에만 의미가 있다.

2.3 Phase 3: 반환값 결정

return result_status

result_status는 세 가지 경우 중 하나이다.

  • SUCCESS: 어떤 자식이 성공하였다.
  • RUNNING: 어떤 자식이 실행 중이다.
  • FAILURE: 모든 자식이 실패하였다 (초기값 FAILURE가 변경되지 않은 경우).

3. 일반 Fallback 의사 코드와의 비교

function Fallback::tick():
    // 이전 RUNNING 자식의 인덱스에서 시작
    for i ← current_index to childCount() - 1:
        child_status ← child(i).tick()
        
        if child_status == SUCCESS:
            current_index ← 0    // 다음 Tick에서 처음부터 시작
            return SUCCESS
        
        if child_status == RUNNING:
            current_index ← i    // 이 인덱스를 기억
            return RUNNING
    
    current_index ← 0
    return FAILURE

일반 Fallback에서는 current_index 변수가 이전 RUNNING 자식의 위치를 기억하며, 해당 인덱스부터 Tick을 재개한다. 또한 Halt 전파 로직이 없다. 이 두 가지 차이가 일반 Fallback과 ReactiveFallback의 근본적 동작 차이를 결정한다.

의사 코드 요소ReactiveFallback일반 Fallback
Tick 시작 인덱스항상 0current_index (기억된 값)
인덱스 기억 변수없음current_index
Halt 전파 로직Phase 2에서 수행없음 (자체 Halt 시에만)

4. C++ 스타일 구현 예시

BehaviorTree.CPP의 구현 스타일에 근접한 C++ 의사 코드는 다음과 같다.

NodeStatus ReactiveFallback::tick() {
    for (size_t i = 0; i < childrenCount(); ++i) {
        const NodeStatus child_status = children_[i]->executeTick();
        
        if (child_status != NodeStatus::FAILURE) {
            // 이 자식 이후의 RUNNING 자식에 Halt 전파
            for (size_t j = i + 1; j < childrenCount(); ++j) {
                if (children_[j]->status() == NodeStatus::RUNNING) {
                    haltChild(j);
                }
            }
            return child_status;
        }
    }
    return NodeStatus::FAILURE;
}

이 구현에서 haltChild(j) 메서드는 자식 노드 j에 Halt를 전파하고 해당 자식의 상태를 IDLE로 초기화하는 역할을 수행한다. executeTick() 메서드는 자식 노드의 tick() 함수를 호출하고 반환된 상태를 기록하는 프레임워크 수준의 래퍼(wrapper) 함수이다.

5. Halt 메서드의 의사 코드

ReactiveFallback 자체에 Halt가 호출되는 경우(상위 노드로부터의 Halt 전파), 현재 RUNNING 상태인 모든 자식에 Halt를 전파하여야 한다.

function ReactiveFallback::halt():
    for i ← 0 to childCount() - 1:
        if child(i).status() == RUNNING:
            child(i).halt()
    setStatus(IDLE)

이 Halt 메서드는 ReactiveFallback이 상위 Parallel 노드의 자식으로 배치된 경우 등, 외부에서 Halt가 호출되는 상황에서 모든 진행 중인 자식을 안전하게 정리하는 역할을 수행한다.

6. 알고리즘의 불변 조건

ReactiveFallback 알고리즘은 다음 불변 조건(invariant)을 항상 유지한다.

  1. 단일 활성 자식 불변: 임의의 시점에서 RUNNING 상태인 자식은 최대 하나이다. Phase 2의 Halt 전파에 의해 이전의 RUNNING 자식이 정리되므로, 새로운 RUNNING 자식과 이전 RUNNING 자식이 공존하지 않는다.

  2. 우선순위 순서 불변: RUNNING 상태인 자식은 항상 비실패 상태를 반환한 가장 높은 우선순위의 자식이다. 더 높은 우선순위의 자식이 모두 FAILURE를 반환한 경우에만 하위 자식이 RUNNING 상태가 된다.

  3. Halt 완전성 불변: Tick 완료 후, RUNNING 상태인 자식보다 하위 인덱스의 자식 중 RUNNING 상태인 자식은 존재하지 않는다. Phase 2에 의해 모두 Halt되었기 때문이다.