PART 2 - 강의 2/3

커스텀 Task/Decorator/Service

C++로 BTTask, BTDecorator, BTService를 직접 구현하고 ExecuteTask, TickNode 패턴을 마스터합니다

01

커스텀 BTTask 구현

UBTTaskNode를 상속한 C++ Task 제작

커스텀 Task는 UBTTaskNode를 상속하여 ExecuteTask()를 오버라이드합니다. 즉시 완료되는 Task는 Succeeded/Failed를, 비동기 Task는 InProgress를 반환하고 나중에 FinishLatentTask()를 호출합니다.

C++// BTTask_FindRandomLocation.h UCLASS() class UBTTask_FindRandomLocation : public UBTTask_BlackboardBase { GENERATED_BODY() public: UBTTask_FindRandomLocation(); virtual EBTNodeResult::Type ExecuteTask( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; virtual FString GetStaticDescription() const override; protected: UPROPERTY(EditAnywhere, Category = "Search") float SearchRadius = 1000.f; }; // BTTask_FindRandomLocation.cpp UBTTask_FindRandomLocation::UBTTask_FindRandomLocation() { NodeName = "Find Random Location"; // BlackboardKey 필터: Vector 타입만 허용 BlackboardKey.AddVectorFilter(this, GET_MEMBER_NAME_CHECKED( UBTTask_FindRandomLocation, BlackboardKey)); } EBTNodeResult::Type UBTTask_FindRandomLocation::ExecuteTask( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) { AAIController* AIController = OwnerComp.GetAIOwner(); if (!AIController) return EBTNodeResult::Failed; const APawn* Pawn = AIController->GetPawn(); if (!Pawn) return EBTNodeResult::Failed; const FVector Origin = Pawn->GetActorLocation(); FNavLocation NavLocation; UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld()); if (NavSys && NavSys->GetRandomReachablePointInRadius( Origin, SearchRadius, NavLocation)) { OwnerComp.GetBlackboardComponent()->SetValueAsVector( GetSelectedBlackboardKey(), NavLocation.Location); return EBTNodeResult::Succeeded; } return EBTNodeResult::Failed; } FString UBTTask_FindRandomLocation::GetStaticDescription() const { return FString::Printf( TEXT("Find random location within %.0f radius"), SearchRadius); }

GetStaticDescription()을 오버라이드하면 BT 에디터에서 노드에 설명 텍스트가 표시되어 디버깅이 편리합니다.

02

비동기(Latent) Task 패턴

InProgress와 FinishLatentTask 활용

이동, 애니메이션 재생 등 시간이 걸리는 Task는 InProgress를 반환하고, 완료 시점에 FinishLatentTask()를 호출합니다. 중단 처리를 위해 AbortTask()도 구현해야 합니다.

C++// BTTask_PlayMontageAndWait.h UCLASS() class UBTTask_PlayMontageAndWait : public UBTTaskNode { GENERATED_BODY() public: virtual EBTNodeResult::Type ExecuteTask( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; virtual EBTNodeResult::Type AbortTask( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; protected: UPROPERTY(EditAnywhere) UAnimMontage* MontageToPlay; UPROPERTY(EditAnywhere) float PlayRate = 1.0f; UFUNCTION() void OnMontageEnded(UAnimMontage* Montage, bool bInterrupted); // OwnerComp 참조 저장 (비동기 콜백용) TWeakObjectPtr<UBehaviorTreeComponent> CachedOwnerComp; }; // BTTask_PlayMontageAndWait.cpp EBTNodeResult::Type UBTTask_PlayMontageAndWait::ExecuteTask( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) { AAIController* Controller = OwnerComp.GetAIOwner(); APawn* Pawn = Controller ? Controller->GetPawn() : nullptr; UAnimInstance* AnimInstance = Pawn ? Pawn->GetMesh()->GetAnimInstance() : nullptr; if (!AnimInstance || !MontageToPlay) return EBTNodeResult::Failed; CachedOwnerComp = &OwnerComp; AnimInstance->Montage_Play(MontageToPlay, PlayRate); FOnMontageEnded EndDelegate; EndDelegate.BindUObject(this, &UBTTask_PlayMontageAndWait::OnMontageEnded); AnimInstance->Montage_SetEndDelegate(EndDelegate, MontageToPlay); return EBTNodeResult::InProgress; // 비동기! } EBTNodeResult::Type UBTTask_PlayMontageAndWait::AbortTask( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) { // 중단 시 몽타주 정지 APawn* Pawn = OwnerComp.GetAIOwner()->GetPawn(); if (UAnimInstance* Anim = Pawn->GetMesh()->GetAnimInstance()) { Anim->Montage_Stop(0.25f, MontageToPlay); } return EBTNodeResult::Aborted; } void UBTTask_PlayMontageAndWait::OnMontageEnded( UAnimMontage* Montage, bool bInterrupted) { if (CachedOwnerComp.IsValid()) { FinishLatentTask(*CachedOwnerComp, bInterrupted ? EBTNodeResult::Failed : EBTNodeResult::Succeeded); } }
주의

AbortTask()를 구현하지 않으면, Observer Aborts로 인한 브랜치 전환 시 리소스 누수가 발생합니다. 비동기 Task는 반드시 정리 로직을 구현하세요.

03

커스텀 Decorator 구현

조건 검사 노드 제작과 FlowAbort 설정

커스텀 Decorator는 UBTDecorator를 상속하여 CalculateRawConditionValue()를 오버라이드합니다. Observer Aborts를 지원하려면 Blackboard 변경 감지를 함께 구현합니다.

C++// BTDecorator_IsHealthAbove.h UCLASS() class UBTDecorator_IsHealthAbove : public UBTDecorator { GENERATED_BODY() public: UBTDecorator_IsHealthAbove(); virtual bool CalculateRawConditionValue( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override; virtual FString GetStaticDescription() const override; protected: UPROPERTY(EditAnywhere, meta = (ClampMin="0.0", ClampMax="1.0")) float HealthThreshold = 0.3f; }; // BTDecorator_IsHealthAbove.cpp UBTDecorator_IsHealthAbove::UBTDecorator_IsHealthAbove() { NodeName = "Is Health Above Threshold"; // Observer Aborts 지원 설정 bNotifyTick = false; bNotifyBecomeRelevant = true; bNotifyCeaseRelevant = true; } bool UBTDecorator_IsHealthAbove::CalculateRawConditionValue( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const { AAIController* Controller = OwnerComp.GetAIOwner(); APawn* Pawn = Controller ? Controller->GetPawn() : nullptr; if (IHealthInterface* Health = Cast<IHealthInterface>(Pawn)) { float Ratio = Health->GetHealthRatio(); return Ratio > HealthThreshold; } return false; } FString UBTDecorator_IsHealthAbove::GetStaticDescription() const { return FString::Printf( TEXT("Health > %.0f%%"), HealthThreshold * 100.f); }
참고

Decorator의 Inverse Condition 프로퍼티를 활성화하면 조건 결과를 반전시킬 수 있어, 별도 "Not" 버전 없이 재사용 가능합니다.

04

커스텀 Service 구현

주기적으로 실행되는 업데이트 노드

커스텀 Service는 UBTService를 상속하여 TickNode()를 오버라이드합니다. 해당 브랜치가 활성화된 동안 일정 간격으로 호출되며, 주로 Blackboard 값 업데이트에 사용합니다.

C++// BTService_UpdateNearestEnemy.h UCLASS() class UBTService_UpdateNearestEnemy : public UBTService { GENERATED_BODY() public: UBTService_UpdateNearestEnemy(); virtual void TickNode( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override; // 브랜치 활성화/비활성화 시 virtual void OnBecomeRelevant( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; virtual void OnCeaseRelevant( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; protected: UPROPERTY(EditAnywhere) FBlackboardKeySelector TargetKey; UPROPERTY(EditAnywhere) float DetectionRange = 2000.f; }; // BTService_UpdateNearestEnemy.cpp UBTService_UpdateNearestEnemy::UBTService_UpdateNearestEnemy() { NodeName = "Update Nearest Enemy"; Interval = 0.5f; // 0.5초 간격 RandomDeviation = 0.1f; // +/- 0.1초 랜덤 } void UBTService_UpdateNearestEnemy::TickNode( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) { Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds); AAIController* Controller = OwnerComp.GetAIOwner(); APawn* AIPawn = Controller ? Controller->GetPawn() : nullptr; if (!AIPawn) return; // Perception에서 감지된 적 중 가장 가까운 적 탐색 UAIPerceptionComponent* Perception = Controller->GetPerceptionComponent(); TArray<AActor*> PerceivedActors; Perception->GetCurrentlyPerceivedActors( UAISense_Sight::StaticClass(), PerceivedActors); AActor* NearestEnemy = nullptr; float MinDist = DetectionRange; for (AActor* Actor : PerceivedActors) { float Dist = FVector::Dist( AIPawn->GetActorLocation(), Actor->GetActorLocation()); if (Dist < MinDist) { MinDist = Dist; NearestEnemy = Actor; } } OwnerComp.GetBlackboardComponent()->SetValueAsObject( TargetKey.SelectedKeyName, NearestEnemy); }

Service의 OnBecomeRelevant/OnCeaseRelevant를 활용하면 브랜치 진입/탈출 시 초기화/정리 작업을 수행할 수 있습니다. 예를 들어 전투 브랜치 진입 시 무기를 꺼내는 로직을 구현할 수 있습니다.

SUMMARY

핵심 요약

  • 커스텀 Task는 ExecuteTask()를 오버라이드하며, 비동기 Task는 InProgress 반환 후 FinishLatentTask()를 호출한다
  • AbortTask()를 구현하여 Observer Aborts 시 리소스 정리를 반드시 처리해야 한다
  • 커스텀 Decorator는 CalculateRawConditionValue()로 조건을 검사하고, Observer Aborts를 지원할 수 있다
  • 커스텀 Service는 TickNode()로 주기적 업데이트를 수행하며, Interval과 RandomDeviation으로 호출 빈도를 제어한다
PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: 커스텀 BTTask 구현

UBTTask_BlackboardBase를 상속하여 '지정 위치로 이동 후 3초 대기' 태스크를 구현하세요. ExecuteTask에서 MoveTo를 시작하고, TickTask에서 완료 여부를 체크합니다.

실습 2: 커스텀 Decorator/Service 구현

UBTDecorator_BlackboardBase로 '체력 50% 이상' 조건 데코레이터를, UBTService_BlackboardBase로 '주기적 Perception 업데이트' 서비스를 각각 구현하세요.

심화 과제

FinishLatentTask()를 활용하는 비동기 BTTask를 구현하세요. 비동기 에셋 로드나 네트워크 요청 완료 시 태스크를 종료하는 패턴으로, 게임플레이 어빌리티 발동 태스크를 만듭니다.