1293.46 부모 노드에 의한 자식 Halt

1. 부모-자식 관계에서의 Halt 책임

행동 트리(Behavior Tree)에서 자식 노드의 Halt는 부모 노드의 책임이다. 자식 노드는 스스로 Halt를 결정하지 않으며, 부모 노드가 자신의 실행 논리에 따라 자식에 Halt를 호출한다. 이 설계 원칙은 Tick의 하향 전파와 대칭적으로, Halt 역시 부모에서 자식 방향으로 전파되는 하향식 제어 구조를 형성한다(Colledanchise & Ogren, 2018).

2. Halt 호출 메서드

BehaviorTree.CPP에서 부모 노드가 자식에 Halt를 호출하는 방법은 두 가지 패턴으로 구분된다(Faconti, 2022).

2.1 haltRunningChildren()

RUNNING 상태인 모든 자식에 Halt를 호출한다. 제어 노드가 최종 상태(SUCCESS 또는 FAILURE)를 결정한 후 호출하는 것이 일반적이다.

void ControlNode::haltRunningChildren() {
    for (size_t i = 0; i < children_count(); i++) {
        if (children_[i]->status() == NodeStatus::RUNNING) {
            children_[i]->halt();
        }
    }
}

2.2 haltChildrenAfter(index)

지정된 인덱스 이후의 자식 중 RUNNING 상태인 것에 Halt를 호출한다. Reactive 노드에서 현재 RUNNING인 자식 이후의 자식을 정리할 때 사용된다.

void ControlNode::haltChildrenAfter(size_t index) {
    for (size_t i = index; i < children_count(); i++) {
        if (children_[i]->status() == NodeStatus::RUNNING) {
            children_[i]->halt();
        }
    }
}

3. 제어 노드별 자식 Halt 패턴

3.1 Sequence에서의 자식 Halt

Sequence 노드에서 자식이 FAILURE를 반환하면, 다른 RUNNING 상태의 자식에 Halt를 호출한다.

Sequence.tick():
  ...
  if child_status == FAILURE:
      haltRunningChildren()    // 모든 RUNNING 자식 Halt
      return FAILURE

표준 WithMemory Sequence에서는 한 시점에 최대 하나의 자식만 RUNNING일 수 있으므로, 실질적으로 현재 RUNNING 자식에만 Halt가 호출된다.

3.2 Fallback에서의 자식 Halt

Fallback 노드에서 자식이 SUCCESS를 반환하면, 다른 RUNNING 상태의 자식에 Halt를 호출한다.

Fallback.tick():
  ...
  if child_status == SUCCESS:
      haltRunningChildren()    // 모든 RUNNING 자식 Halt
      return SUCCESS

3.3 ReactiveSequence에서의 자식 Halt

ReactiveSequence에서 부모가 자식에 Halt를 호출하는 시나리오는 두 가지이다.

3.3.1 시나리오 1: 앞선 자식의 FAILURE

ReactiveSequence.tick():
  Child_0.tick() → FAILURE
  haltRunningChildren()    // 뒤쪽 RUNNING 자식 Halt
  return FAILURE

3.3.2 시나리오 2: 자식의 RUNNING

ReactiveSequence.tick():
  Child_0.tick() → SUCCESS
  Child_1.tick() → RUNNING
  haltChildrenAfter(2)     // 인덱스 2 이후 RUNNING 자식 Halt
  return RUNNING

이전 Tick에서 인덱스 2 이후의 자식이 RUNNING이었을 수 있으므로, 해당 자식에 Halt를 호출하여 정리한다.

3.4 ReactiveFallback에서의 자식 Halt

ReactiveFallback에서 부모가 자식에 Halt를 호출하는 패턴은 ReactiveSequence와 대칭적이다.

3.4.1 시나리오 1: 앞선 자식의 SUCCESS

ReactiveFallback.tick():
  Child_0.tick() → SUCCESS
  haltRunningChildren()    // 뒤쪽 RUNNING 자식 Halt
  return SUCCESS

3.4.2 시나리오 2: 자식의 RUNNING

ReactiveFallback.tick():
  Child_0.tick() → FAILURE
  Child_1.tick() → RUNNING
  haltChildrenAfter(2)     // 인덱스 2 이후 RUNNING 자식 Halt
  return RUNNING

3.5 Parallel에서의 자식 Halt

Parallel 노드에서 임계값에 도달하면, 나머지 RUNNING 자식에 Halt를 호출한다.

Parallel.tick():
  // 모든 활성 자식 Tick 후
  if success_count >= threshold:
      haltRunningChildren()
      return SUCCESS
  if failure_count > N - threshold:
      haltRunningChildren()
      return FAILURE

3.6 데코레이터에서의 자식 Halt

데코레이터 노드는 단일 자식에 대해 halt_child() 메서드를 통해 Halt를 호출한다.

void DecoratorNode::halt_child() {
    if (child_node_->status() == NodeStatus::RUNNING) {
        child_node_->halt();
    }
}

4. 구체적 실행 흐름 예시

다음과 같은 중첩 트리 구조에서 부모에 의한 자식 Halt의 전파 과정을 추적한다.

ReactiveSequence
├── IsSafe (조건)
└── Sequence
    ├── MoveToA (비동기)
    ├── PickObject (비동기)
    └── MoveToB (비동기)

4.1 정상 실행 중 안전 조건 위반

Tick 5: ReactiveSequence
  IsSafe.tick() → SUCCESS
  Sequence.tick() → current_index=1
    PickObject.tick() → RUNNING
  Sequence 반환: RUNNING
  ReactiveSequence 반환: RUNNING

Tick 6: ReactiveSequence
  IsSafe.tick() → FAILURE          (안전 조건 위반!)
  haltRunningChildren() 호출:
    → Sequence.halt() 호출
      → Sequence.haltRunningChildren()
        → PickObject.halt()         (RUNNING → IDLE)
        → PickObject.onHalted()     (정리 작업)
      → Sequence.current_index = 0  (메모리 리셋)
      → Sequence 상태: IDLE
  ReactiveSequence 반환: FAILURE

이 예시에서 ReactiveSequence가 IsSafe의 FAILURE에 의해 Sequence에 Halt를 호출하고, Sequence는 다시 자신의 RUNNING 자식인 PickObject에 Halt를 전파한다. Halt는 재귀적으로 트리 구조를 따라 전파된다.

5. Halt 호출의 선택성

부모 노드는 모든 자식에 무차별적으로 Halt를 호출하지 않는다. RUNNING 상태인 자식에만 Halt를 호출하며, IDLE, SUCCESS, FAILURE 상태인 자식에는 Halt를 호출하지 않는다. 이는 RUNNING이 아닌 노드에 Halt를 호출하는 것이 무의미하기 때문이다.

// 상태 확인 후 선택적 Halt
if (child->status() == NodeStatus::RUNNING) {
    child->halt();  // RUNNING인 경우에만 Halt
}

6. 부모 Halt와 자식 상태 리셋의 관계

부모가 자식에 Halt를 호출하면, 자식은 자신의 상태를 IDLE로 리셋한다. 이 리셋은 자식의 halt() 메서드 내에서 resetStatus()를 호출하여 수행된다. 부모는 자식의 상태를 직접 변경하지 않으며, 자식이 자신의 Halt 처리 과정에서 스스로 상태를 리셋하는 것이 원칙이다.

부모: child.halt() 호출
  자식: onHalted() 실행 (정리 작업)
  자식: resetStatus() → IDLE
부모: child.status() 확인 → IDLE

참고 문헌

  • 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/