AI 주변에 플레이어가 없으면 주변을 탐색하는 행동은 RPG에서는 매우 흔한일이다.

하지만 현재 구현한 것 만으로는 오로지 탐색만 하기 때문에 플레이어가 접근 시 다른 행동을 하게끔 해야한다.

그것을 할 수 있도록 하는것이 셀렉터이다.

타겟을 찾았으면 왼쪽으로 분기하고 못찾았으면 오른쪽으로 분기하게끔 하면 된다.

 

그렇지만 매 틱마다 계속 서치를 하는것은 부담이 되기 때문에 주기적으로(1~2초) 한번씩 탐색하는 것이 일반적이다.

물론 언리얼은 그것에 대한 것도 준비가 되어있다. 서비스라는 개념이다.

 

우선 서비스부터 만들어 보도록 한다.

 

 

// BTService_SearchTarget.h
public:
    UBTService_SearchTarget();

    virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
    
// BTService_SearchTarget.cpp
#include "MyAIController.h"
#include "MyCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "DrawDebugHelpers.h"

UBTService_SearchTarget::UBTService_SearchTarget()
{
    NodeName = TEXT("SearchTarget");
    Interval = 1.0f; // Tick의 주기 설정
}

void UBTService_SearchTarget::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

    auto CurrentPawn = OwnerComp.GetAIOwner()->GetPawn();
    if (CurrentPawn == nullptr)
        return;

    UWorld* World = CurrentPawn->GetWorld();
    FVector Center = CurrentPawn->GetActorLocation();
    float SearchRadius = 500.f;

    if (World == nullptr)
        return;

    TArray<FOverlapResult> OverlapResults;
    // 3번째 인자는 무시할 액터. 즉, 자기 자신은 무시하겠다는 뜻이다
    FCollisionQueryParams QueryParams(NAME_None, false, CurrentPawn);

    // 월드에 충돌체를 생성해서 충돌된 오브젝트들을 첫번째 인자에 넣어준다
    bool bResult = World->OverlapMultiByChannel(
        OverlapResults,
        Center,
        FQuat::Identity,
        ECollisionChannel::ECC_GameTraceChannel2,
        FCollisionShape::MakeSphere(SearchRadius),
        QueryParams);

    if (bResult)
    {
        for (auto& OverlapResult : OverlapResults)
        {
            // MyCharacter로 캐스팅해서 유효한 경우만 블랙보드에 값을 넘겨준다
            AMyCharacter* MyCharacter = Cast<AMyCharacter>(OverlapResult.GetActor());
            if (MyCharacter && MyCharacter->GetController()->IsPlayerController())
            {
            OwnerComp.GetBlackboardComponent()->SetValueAsObject(TEXT("Target"), MyCharacter);

            DrawDebugSphere(World, Center, SearchRadius, 16, FColor::Green, false, 0.2f);

            return;
            }
        }
        DrawDebugSphere(World, Center, SearchRadius, 16, FColor::Red, false, 0.2f);
    }
    else
    {
        // 서치 실패시 nullptr을 넘겨준다
        OwnerComp.GetBlackboardComponent()->SetValueAsObject(TEXT("Target"), nullptr);
        DrawDebugSphere(World, Center, SearchRadius, 16, FColor::Red, false, 0.2f);
    }
}

 

생성자에서 Interval값을 조절하여 TickNode의 호출 간격을 조절할 수 있다.

 

이제 블랙보드에서 키를 추가해주고 셀렉터에 서비스를 추가해주자.

 

 

 

범위 안에 없을 때

 

범위 안에 있을 때

매 틱마다 실행되는게 아닌 1초마다 실행되고 충돌(서치) 여부를 DrawDebugSphere를 통해 시각적으로 알 수 있다.

 

아직 갈길이 멀다. 범위 안에서 탐지시 타겟을 추적하는 기능이 필요하다.

 

데코레이터를 추가해서 컴포짓 노드의 분기 조건을 만들어준다.

왼쪽은 범위 안에 들어와서 타겟이 있는경우, 오른쪽은 타겟이 없는 경우에 실행된다.

처음에는 범위 밖에 있으므로 오른쪽으로 분기하여 5초 대기-랜덤한 목적지 이동을 실행하지만 이동이 끝난시점에 범위 안에 있다면 왼쪽으로 분기하여 타겟을 추적한다.

 

데코레이터는 if-else문과 유사하다고 생각하면 된다.

 

추적까지 성공했다면 이제 일정 범위 안에 접근시 공격하는것까지 추가해보자.

'공격이 가능한가?' 를 따져야하는 일종의 조건식이므로 데코레이터를 만들어준다.

 

// BTDecorator_CanAttack.h
public:
    UBTDecorator_CanAttack();

    virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemoty) const override;
    
// BTDecorator_CanAttack.cpp
#include "MyAIController.h"
#include "MyCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"

UBTDecorator_CanAttack::UBTDecorator_CanAttack()
{
    NodeName = TEXT("CanAttack");
}

bool UBTDecorator_CanAttack::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemoty) const
{
    bool bResult = Super::CalculateRawConditionValue(OwnerComp, NodeMemoty);

    auto CurrentPawn = OwnerComp.GetAIOwner()->GetPawn();
    if (CurrentPawn == nullptr)
        return false;

    auto Target = Cast<AMyCharacter>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(FName(TEXT("Target"))));

    if (Target == nullptr)
        return false;

    // 타겟과의 거리가 200 미만이면 공격 실행을 위해 true반환
    return bResult && Target->GetDistanceTo(CurrentPawn) <= 200.f;
}

 

공격이 불가능한 경우에는 추적을 해야하므로 오른쪽 분기는 inversed(반대조건)을 체크해준다.

이제 조건과 분기는 만들었으므로 태스크를 만들어서 실제로 실행해 볼 때이다.

 

 

// BTTask_Attack.h
public:
    UBTTask_Attack();

    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
    virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;

private:
    bool bIsAttacking = false;

// BTTask_Attack.cpp
#include "MyAIController.h"
#include "MyCharacter.h"

UBTTask_Attack::UBTTask_Attack()
{
    NodeName = TEXT("Attack");
    
    // 기본은 false이고 true 설정시 TickTask가 실행된다
    bNotifyTick = true;
    bIsAttacking = false;
}

EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);

    auto MyCharacter = Cast<AMyCharacter>(OwnerComp.GetAIOwner()->GetPawn());
    if (MyCharacter == nullptr)
        return EBTNodeResult::Failed;

    MyCharacter->Attack();
    bIsAttacking = true;

    return Result;
}

void UBTTask_Attack::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);

    // 공격이 끝났으면 작업의 끝을 알린다
    if (bIsAttacking == false)
        FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}

 

타겟을 탐지하고(서비스:SearchTarget) -> 200만큼 범위안에 들어왔는지 체크하고(데코레이터:CanAttack) -> 공격

이 순서로 작동하게 될 것이다.

다만 현재는 태스크 실행시 Attack 함수를 호출하여 애니메이션 몽타주를 재생하고  bIsAttacking을 true로 변경하는 것은 있지만 다시 false로 되돌리는 부분이 없다.

 

그럼 공격이 끝난 시점은 어떻게 구분하면 되는가?

일전에 MyCharacter에서도 공격 시작시 애니메이션 몽타주가 재생되면서 IsAttacking이 true로 설정되었다가 몽타주 재생이 끝나면 OnAttackMontageEnded 함수에 의해 false로 되돌리는 부분이 있었다. 그것을 OnMontageEnded 델리게이트에 등록함으로써 몽타주 종료시 호출되게끔 했었다.

 

그것과 마찬가지로 델리게이트에 등록해서 몽타주 재생이 끝날 시 호출하면 될 것이다. 다만 직접 호출은 아니고 공격 모션이 끝나면 bIsAttacking을 false로 변경하는 함수를 구독시키고 Broadcast로 쏴줄 것이다.

 

 

// MyCharacter.h
DECLARE_MULTICAST_DELEGATE(FOnAttackEnd);

public:
    FOnAttackEnd OnAttackEnd;
    
// MyCharacter.cpp
void AMyCharacter::OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
    IsAttacking = false;
    // 구독자가 누군지 몰라도 등록된 콜백을 호출하라고 신호를 보냄
    OnAttackEnd.Broadcast();
}

 

// BTTask_Attack.cpp
EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
    MyCharacter->OnAttackEnd.AddLambda([this]()
        {
            bIsAttacking = false;
        });
}

 

이제 공격 시작시 true로 변경하고 몽타주 재생이 끝나면 콜백이 호출되어 false로 변경되고 TickTask에 의해 계속 검사되고 있었으므로 작업의 끝을 알리게 된다.

 

마지막으로 만들어준 Attack 노드를 붙여주면 범위 내에 감지 후 일정거리내에 도달시 공격 모션을 실행하게 된다.

 

 

[정리]

 

  • 태스크 : 보라색 노드. AI의 행동이나 블랙보드의 값 조정같은 작업을 한다. 반드시 컴포짓 노드를 거쳐 실행되어야 한다.
  • 데코레이터 : 파란색 노드. 특정 노드의 실행 여부를 결정짓는 노드이다.
  • 서비스 : 초록색 노드. 노드가 실행되는 동안 같이 실행된다.
  • 비헤이비어 트리가 복잡해질수록 이점은 있지만 초기에 세팅하는게 어렵다.

'언리얼 엔진 > 언리얼 엔진4 입문' 카테고리의 다른 글

[UE4 입문] 언리얼 컨테이너  (0) 2022.09.12
[UE4 입문] 샘플 분석  (0) 2022.09.12
[UE4 입문] Behavior Tree #1  (0) 2022.09.12
[UE4 입문] AI Controller  (0) 2022.09.11
[UE4 입문] UI 실습  (0) 2022.09.03

+ Recent posts