언리얼 Behavior Tree 노드 구현 핵심과 함정 (BTTask, Service, Decorator)
언리얼 AI 학습 노트. 강의 8.3~8.8 개념과 함정만. BT 커스텀 노드 만들 때 다시 보기.
AIController에 BT/Blackboard 연결 구조
AIController 생성자에서 Blackboard 컴포넌트와 BrainComponent(UBehaviorTreeComponent) 생성. BeginPlay에서 UseBlackboard() 후 RunBehaviorTree(). EndPlay에서 BehaviorTreeComponent->StopTree().
BrainComponent는 AIController에 이미 정의된 속성(UBrainComponent ← UBehaviorTreeComponent). 새로 만드는 게 아니라 채우는 것.
디버깅: BT 애셋 열고 Alt+P 후 플레이하면 로직 흐름이 그래프에 실시간 표시. AI 디버깅의 기본.
Blackboard 키 이름 관리 함정
키 이름을 static const FName으로 AIController에 선언하는 패턴을 씀. 다른 코드에서 참조하기 편함. 단점: 하드코딩이라 키 이름 바뀌면 코드도 바꿔야 함. 키 이름은 안 바뀐다는 가정 하에 쓰는 것.
블랙보드 값 읽기/쓰기: GetBlackboardComponent()->GetValueAsVector/Object/..., SetValueAs.... 키는 위 FName 상수로 참조.
BTTask 커스텀 노드
BTTaskNode 상속. ExecuteTask() override가 핵심. NodeName으로 BT 그래프에 보일 이름 지정.
지연 Task 패턴 (공격처럼 애니메이션 끝까지 기다려야 하는 경우):
- ExecuteTask()에서 일단 EBTNodeResult::InProgress 반환
- 동작 끝나는 시점에 FinishLatentTask()로 완료 통보
- 함정: FinishLatentTask()를 안 부르면 BT가 그 Task에 영원히 머묾. 가장 흔한 버그
- TickTask를 쓰려면 생성자에서 bNotifyTick = true 설정
지연 Task의 끝 감지는 보통 델리게이트로 함. 공격이면 AnimMontage의 EndDelegate에 EndAttack 바인드 → bIsNowAttacking 플래그 내림 → TickTask에서 그 플래그 보고 FinishLatentTask 호출. (앞 챕터의 "동작 종료는 직접 타이밍 재지 말고 델리게이트로 잡는다" 패턴 그대로)
BTService 커스텀 노드
BTService 상속. TickNode() override. 생성자에서 Interval로 호출 주기 설정. Selector 같은 Composite에 부착하면 그게 활성화된 동안 주기적으로 실행.
용도: 주위 캐릭터 감지 같은 주기 작업. OverlapMultiByChannel로 범위 내 캐릭터 탐색 → 플레이어면 블랙보드의 TargetCharacter 키에 세팅, 없으면 nullptr. 이 키 유무가 추격/정찰 분기의 기준이 됨.
BTDecorator 커스텀 노드
BTDecorator 상속. CalculateRawConditionValue() override. 이 함수는 const라 Decorator 속성 변경 불가. 조건 충족 여부(bool)만 반환.
용도: 공격 범위 안인지 같은 조건 판단. 블랙보드의 TargetCharacter 가져와서 거리 계산 → AttackRange 이내면 true. 이 결과로 Composite 실행 분기.
Decorator의 Notify Observer와 Observer Aborts (중요 함정)
Notify Observer:
- On Value Change: 키 값이 변하면 발생
- On Result Change: 조건식 결과가 바뀔 때만 발생
- 예) 조건이 1.f < x. x가 0.8→0.9면 Value Change만. x가 1.f→1.1이면 Value Change + Result Change 둘 다
Observer Aborts (Notify 발생 시 무엇을 중단할지):
- None: 아무것도 중단 안 함. 자식 Task 다 끝날 때까지 안 끊김
- Self: 해당 Composite 자식들 즉시 중단
- Lower Priority: 자기 우측 노드 전부 중단
- Both: Self + Lower Priority
추격/정찰 분기 구현 시 이걸 잘못 잡으면 플레이어를 발견해도 정찰을 안 끊고 계속 돌거나, 반대로 계속 끊겨서 행동을 못 함. 추격 쪽 Decorator를 On Value Change + Self(또는 Both)로 두고, 정찰 쪽은 반대 조건(Is Not Set)으로 두는 게 기본 패턴.
SimpleParallel Composite
Main Task + Background Task 구조. Main이 핵심 동작, Background가 그동안 같이 돌릴 보조 동작.
용도: 공격(Main)하면서 동시에 타겟 쪽으로 회전(Background). 회전은 FMath::RInterpTo로 부드럽게 보간. Sequence를 SimpleParallel로 교체하고 Attack을 Main, TurnToTarget을 Background에 연결.
NPC 회전/속도 보정 함정
NPC 이동 시 회전이 부자연스럽게 꺾이고 속도가 너무 빠른 문제. BeginPlay에서 IsPlayerControlled()가 false일 때만:
- bUseControllerRotationYaw = false
- bOrientRotationToMovement = false
- bUseControllerDesiredRotation = true
- RotationRate로 회전 속도 제한
- MaxWalkSpeed 너프
플레이어/NPC가 같은 캐릭터 베이스를 공유할 때, 이 설정을 IsPlayerControlled() 분기 없이 하면 플레이어 조작감까지 망가짐. 반드시 NPC일 때만 적용.
NPC 죽음 처리 함정
NPC가 죽어도 시체 상태로 공격하거나 회전하는 문제. 원인은 BT가 계속 돌기 때문. TakeDamage override에서 HP가 0 근처(KINDA_SMALL_NUMBER)면 AIController->EndAI()로 BT를 명시적으로 중단해야 함.
죽음 = HP 0 처리만으로 끝이 아니라 "BT 정지"까지 한 세트라는 걸 기억. 캐릭터 죽음 처리(콜리전/무브먼트 끄기)와 별개로 AI는 따로 꺼줘야 함.
'학습 > Unreal' 카테고리의 다른 글
| 언리얼 UI 정리 (WidgetComponent, HUD, 초기화 타이밍 함정) (0) | 2026.05.16 |
|---|---|
| 언리얼 게임 데이터 관리 정리 (StatusComponent, PlayerState, GameInstance, DataTable) (0) | 2026.05.16 |
| 언리얼 AI 기초 정리 (AIController, Behavior Tree, Blackboard) (0) | 2026.05.16 |
| 언리얼 데미지 프레임워크와 죽음 처리 정리 (TakeDamage) (0) | 2026.05.16 |
| 언리얼 충돌 시스템 핵심 정리 (콜리전 프리셋, 트레이싱) (0) | 2026.05.16 |
