1297.22 함수 포인터 기반 조건 노드 생성
1. 함수 포인터 방식의 개요
함수 포인터(function pointer) 기반 조건 노드 생성은 일반 함수(free function)를 정의하고 해당 함수의 포인터를 SimpleConditionNode에 전달하여 조건 노드를 등록하는 방식이다. 람다 함수 기반 방식과 동일한 registerSimpleCondition() 메서드를 사용하나, 익명 함수 대신 명명된 함수를 사용한다는 점에서 구별된다(Faconti & Colledanchise, 2022).
2. 함수 포인터 기반 등록
2.1 함수 정의
SimpleConditionNode에서 요구하는 시그니처에 맞추어 일반 함수를 정의한다:
BT::NodeStatus checkBatteryLevel(BT::TreeNode& self)
{
double battery;
auto result = self.getInput("battery_level", battery);
if (!result)
{
return BT::NodeStatus::FAILURE;
}
double threshold;
self.getInput("threshold", threshold);
return (battery >= threshold)
? BT::NodeStatus::SUCCESS
: BT::NodeStatus::FAILURE;
}
2.2 팩토리 등록
정의된 함수를 팩토리에 등록한다:
BT::BehaviorTreeFactory factory;
factory.registerSimpleCondition(
"CheckBatteryLevel",
checkBatteryLevel,
{
BT::InputPort<double>("battery_level", "배터리 잔량 (%)"),
BT::InputPort<double>("threshold", 20.0, "최소 허용 잔량")
}
);
함수 이름 checkBatteryLevel이 함수 포인터로 자동 변환되어 전달된다.
3. 복수의 함수 포인터 기반 조건 노드
프로젝트에서 필요한 조건 함수를 모아서 관리할 수 있다.
// condition_functions.hpp
#pragma once
#include <behaviortree_cpp/bt_factory.h>
BT::NodeStatus isDistanceSafe(BT::TreeNode& self);
BT::NodeStatus isLocalized(BT::TreeNode& self);
BT::NodeStatus isGoalReached(BT::TreeNode& self);
BT::NodeStatus isPathValid(BT::TreeNode& self);
// condition_functions.cpp
#include "condition_functions.hpp"
BT::NodeStatus isDistanceSafe(BT::TreeNode& self)
{
double distance, threshold;
self.getInput("distance", distance);
self.getInput("threshold", threshold);
return (distance > threshold) ? BT::NodeStatus::SUCCESS : BT::NodeStatus::FAILURE;
}
BT::NodeStatus isLocalized(BT::TreeNode& self)
{
double quality;
self.getInput("quality", quality);
return (quality > 0.7) ? BT::NodeStatus::SUCCESS : BT::NodeStatus::FAILURE;
}
BT::NodeStatus isGoalReached(BT::TreeNode& self)
{
double distance_to_goal, tolerance;
self.getInput("distance_to_goal", distance_to_goal);
self.getInput("tolerance", tolerance);
return (distance_to_goal <= tolerance) ? BT::NodeStatus::SUCCESS : BT::NodeStatus::FAILURE;
}
BT::NodeStatus isPathValid(BT::TreeNode& self)
{
bool valid;
auto result = self.getInput("path_valid", valid);
if (!result)
{
return BT::NodeStatus::FAILURE;
}
return valid ? BT::NodeStatus::SUCCESS : BT::NodeStatus::FAILURE;
}
3.1 일괄 등록
void registerConditionNodes(BT::BehaviorTreeFactory& factory)
{
factory.registerSimpleCondition(
"IsDistanceSafe", isDistanceSafe,
{
BT::InputPort<double>("distance", "장애물까지의 거리"),
BT::InputPort<double>("threshold", 1.0, "안전 거리")
});
factory.registerSimpleCondition(
"IsLocalized", isLocalized,
{ BT::InputPort<double>("quality", "위치 추정 품질") });
factory.registerSimpleCondition(
"IsGoalReached", isGoalReached,
{
BT::InputPort<double>("distance_to_goal", "목표까지의 거리"),
BT::InputPort<double>("tolerance", 0.5, "도달 판정 오차")
});
factory.registerSimpleCondition(
"IsPathValid", isPathValid,
{ BT::InputPort<bool>("path_valid", "경로 유효 여부") });
}
4. 함수 포인터 방식과 람다 방식의 비교
| 특성 | 함수 포인터 | 람다 함수 |
|---|---|---|
| 정의 위치 | 함수 별도 정의 | 등록 코드 내 인라인 |
| 외부 변수 접근 | 불가 (전역 변수 제외) | 캡처를 통해 가능 |
| 재사용성 | 높음 (다른 곳에서 호출 가능) | 낮음 (등록 코드에 결합) |
| 단위 테스트 | 함수 단독 테스트 가능 | 등록 맥락 내에서만 테스트 |
| 코드 가독성 | 함수 이름으로 의도 표현 | 인라인 정의로 코드 밀집 |
| 헤더 파일 관리 | 함수 선언 필요 | 불필요 |
5. 함수 포인터 방식의 장점
5.1 단위 테스트 용이성
일반 함수는 SimpleConditionNode와 분리하여 독립적으로 테스트할 수 있다. 모의 TreeNode를 생성하고 블랙보드 값을 설정한 후, 함수를 직접 호출하여 반환값을 검증한다.
TEST(ConditionFunctionTest, IsDistanceSafe_Success)
{
auto config = BT::NodeConfig();
auto bb = BT::Blackboard::create();
config.blackboard = bb;
config.input_ports["distance"] = "{distance}";
config.input_ports["threshold"] = "1.0";
BT::TreeNode::Ptr node =
std::make_unique<BT::SimpleConditionNode>("test", isDistanceSafe, config);
bb->set("distance", 2.0);
EXPECT_EQ(node->executeTick(), BT::NodeStatus::SUCCESS);
bb->set("distance", 0.5);
EXPECT_EQ(node->executeTick(), BT::NodeStatus::FAILURE);
}
5.2 코드 구조화
함수 정의를 별도의 소스 파일로 분리하면, 조건 평가 로직과 트리 설정 코드가 분리되어 코드의 유지보수성이 향상된다.
5.3 함수 이름을 통한 문서화
함수 이름(checkBatteryLevel, isDistanceSafe 등)이 조건의 의미를 직접적으로 표현하여, 코드의 자기 문서화(self-documenting) 특성이 강화된다.
6. 함수 포인터 방식의 한계
함수 포인터 기반 방식은 다음의 한계를 갖는다:
-
외부 상태 접근 제한: 일반 함수는 캡처 기능이 없으므로, 전역 변수나 정적 변수를 통하지 않고는 외부 상태에 접근할 수 없다. ROS2 노드의 참조 등이 필요한 경우 람다 캡처나 클래스 기반 구현을 사용해야 한다.
-
인스턴스별 상태 불가: 동일한 함수를 복수의 노드 인스턴스에서 사용할 때, 인스턴스별로 독립된 상태를 유지할 수 없다.
-
복잡한 초기화 불가: 구독자 설정, 서비스 클라이언트 생성 등 노드 생성 시점의 초기화 로직을 구현할 수 없다.
이러한 한계가 제약이 되는 경우에는 클래스 기반 ConditionNode 구현을 사용해야 한다.
7. 참고 문헌
- Colledanchise, M., & Ogren, P. (2018). Behavior Trees in Robotics and AI: An Introduction. CRC Press.
- Faconti, D., & Colledanchise, M. (2022). BehaviorTree.CPP Documentation. https://www.behaviortree.dev/
version: 0.1.0