언리얼 팀프로젝트

멀티 플레이어 (데디서버) NPC 랜덤 행동 구현하기 -1

Turtle_Jun 2025. 9. 17. 21:01

멀티플레이어 (데디서버) AI 랜덤 좌표 이동

 

멀티플레이어 (데디서버) AI 랜덤 좌표 이동

이번 팀프로젝트에서 NPC 구현 및 서버 보조 역할을 담당받았다. NPC 기능 중 하나인 NavMesh 내부에서 NPC가 랜덤하게 이동하는 기능을 이번 글에서 다뤄보도록 한다. 1. 클래스 생성1-1) NPC (캐릭터

unrealstudyhome.tistory.com

 

 

멀티 플레이어에서 NPC 가 랜덤한 행동을 하는 기능을 구현하는 역할을 맡았다. 

구현해야할 목차는 다음과 같다.

 

1. 필요한 C++ 클래스 생성

 

  • Character 기반: ADCNPC (클래스명 예시) — NPC 본체 (애니/상태/타이머 등)
    • Editor: Add C++ Class → Character → 이름: DCNPC
  • AIController 기반: ADCNPCController — BT/Blackboard 연결 및 실행
    • Editor: Add C++ Class → AIController → 이름: DCNPCController
  • BTService 기반: UDCNPCBTRandomAction — 주기적으로 가중치 랜덤 행동 선택
    • Editor: Add C++ Class → BTService → 이름: DCNPCBTService_RandomAction
  • BTTaskNode 기반: UDCNPCBTT_PlayAction — CurrentState에 맞는 몽타주/행동 시작 (latency 처리)
    • Editor: Add C++ Class → BTTaskNode → 이름: DCNPCBTTask_PlayMontage
  • BTTaskNodeBlackboardBase 기반: UDCNPCBTT_SlideToLocation — 슬라이드(랜덤 방향 300cm) 실행 (Latent)
    • Editor: Add C++ Class → BTTaskNodeBlackboardBase → 이름: DCNPCBTTask_SlideToLocation
  • BTTaskNodeBlackboardBase 기반: UDCNPCBTT_FindRandomLocation — NavMesh에서 랜덤 좌표를 블랙보드에 설정
    • Editor: Add C++ Class → BTTaskNodeBlackboardBase → 이름: DCNPCBTTask_FindRandomLocation
  • 일반 헤더: NPCState.h — ENPCState 열거형  (라이더에서 생성)

 

2) 전체 코드 파일 (목차)

  • Source/DC/NPC/NPCState.h
  • Source/ DC/NPC/DCNPC.h
  • Source/ DC/NPC/DCNPC.cpp
  • Source/ DC/NPC/DCNPCController.h
  • Source/ DC/NPC/DCNPCController.cpp
  • Source/ DC/NPC/DCNPCBTService_RandomAction.h
  • Source/ DC/NPC/DCNPCBTService_RandomAction.cpp
  • Source/ DC/NPC/DCNPCBTTask_PlayMontage.h
  • Source/ DC/NPC/DCNPCBTTask_PlayMontage.cpp
  • Source/ DC/NPC/DCNPCBTTask_SlideToLocation.h
  • Source/ DC/NPC/DCNPCBTTask_SlideToLocation.cpp
  • Source/ DC/NPC/DCBTTask_FindRandomLocation.h 
  • Source/ DC/NPC/DCBTTask_FindRandomLocation.cpp

여기서 Moving에 사용될 FindRandomLocation은 이전에 작성한 글에 있는 Task 내용과 동일하다.

 

 

NPCState.h — 상태 열거형

// Source/DC/NPC/NPCState.h
#pragma once
#include "CoreMinimal.h"
#include "NPCState.generated.h"

/**
 * NPC 상태 정의
 */
UENUM(BlueprintType)
enum class EDCNPCState : uint8
{
    Idle     UMETA(DisplayName="Idle"),
    Moving   UMETA(DisplayName="Moving"),
    Dancing  UMETA(DisplayName="Dancing"),
    Sliding  UMETA(DisplayName="Sliding"),
    Sleeping UMETA(DisplayName="Sleeping")
};

- enum 은 헤더파일로만 추가 후 에디터에서 아래와 같이 블랙보드 키값을 설정해준다.

 

 

 

DCNPC.h — NPC 캐릭터 헤더 (상태/몽타주 맵/슬라이드 타이머 등)

// 정기준

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "NPC/DCNPCState.h"
#include "DCNPC.generated.h"

class UAnimMontage;
class UBehaviorTree;

// FStateMontage
// 에디터에서 상태멸 몽타주 쉽게 설정하기 위한 구조체
USTRUCT(BlueprintType)
struct FStateMontage
{
    GENERATED_BODY()
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="NPC|Montage")
    EDCNPCState State = EDCNPCState::Idle;

    // 해당 상태에서 재생할  Montage (없으면 재생 안함)
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="NPC|Montage")
    UAnimMontage* Montage = nullptr;
};

UCLASS()
class DC_API ADCNPC : public ACharacter
{
    GENERATED_BODY()

public:
    ADCNPC();

    //  플레이어 로부터 피해를 받았을때  호출
    UFUNCTION()
    void OnAttacked();

    // 상태에 맞는 몽타쥬 반환
    UAnimMontage* GetMontageForState(EDCNPCState State) const;

    // 슬라이딩  몽타주 재생 + TargetLocation으로 이동
    UFUNCTION()
    void StartSlidingToLocation(FVector TargetLocation, float SlideDistance = 300.f);   // 랜덤 위치방향으로 300cm만큼 이동


protected:
    virtual void BeginPlay() override;

    // 상태에 따른 몽타주 배열
    // 상태 -> 몽타주 매핑 ( 에디터에서 추가/ 관리 )
    UPROPERTY(EditAnywhere, Category = "NPC|Montages")
    TArray<FStateMontage> StateMontages;

    // 잠자는 애니 몽타주
    UPROPERTY(EditAnywhere, Category = "NPC|Montages")
    TObjectPtr<UAnimMontage> SleepMontage = nullptr;

    // 애니 끝났을때 콜백할 타이머 핸들러
    FTimerHandle AnimationTimerHandle;

    // 슬라이딩 보간을 위한 타이머 핸들러
    FTimerHandle SlideTimerHandle;



#pragma region Replicate
public:
    // Replicate 함수
    virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;

    void StartStateWithMontage(EDCNPCState NewState, UAnimMontage* Montage, float DurationOverride = 0.f, TFunction<void()> OnFinishedCallback = nullptr);

    // Multicast 모든 클라에서 애니 재생
    UFUNCTION(NetMulticast, Reliable)
    void MulticastPlayMontage(UAnimMontage* Montage, float PlayRate = 1.f);

protected:
    // 현재 상태 ( 서버에서 변경 => 클라로 복제)
    UPROPERTY(ReplicatedUsing = OnRep_CurrentState, VisibleAnywhere, BlueprintReadOnly, Category = "NPC")
    EDCNPCState CurrentState;

    UFUNCTION()
    void OnRep_CurrentState();  // 현재 상태 복제(리플리케이션)될때마다 클라이언트에서 호출


    // 서버에서 CurrentState 설정
    void SetCurrentState_Server(EDCNPCState NewState);

    void OnAnimationFinished(); // 애니 종료시 호출(서버에서만 실행)

    // 행동 중단 (움직임/몽타주 등) - 서버에서 호출
    void InterruptCurrentAction();

#pragma endregion

};

 

void StartStateWithMontage() : 는 서버에서 상태를 바꾸고 Multicast로 클라이언트 애니를 재생한다.

void StartSlidingToLocation() : 은 슬라이드 애니와 병행하여 NPC액터의 위치 이동을 한다.

 

DCNPC.cpp — NPC 실제 동작 구현 (중요: replication / timer 관리)

더보기
#include "NPC/DCNPC.h"

#include "Net/UnrealNetwork.h"
#include "DCNPCController.h"
#include "Animation/AnimMontage.h"
#include "AIController.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "TimerManager.h"


ADCNPC::ADCNPC()
{
    bReplicates = true;
    AIControllerClass = ADCNPCController::StaticClass();    // NPC 컨트롤러 할당
    AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned; // NPC 자동 빙의

    Tags.Add("NPC");    // NPC 태그 추가
}



void ADCNPC::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);

    DOREPLIFETIME(ThisClass, CurrentState); // Replicate
}


void ADCNPC::BeginPlay()
{
	Super::BeginPlay();
}


// 상태 바뀔때 호출
void ADCNPC::OnRep_CurrentState()
{
    // Current 변수가 변화될때(복제) 호출
    //UE_LOG(LogTemp, Display, TEXT("Client : OnRep_CurrentState -> %d"), (uint8)CurrentState);
}


// State 설정 (서버에서)
void ADCNPC::SetCurrentState_Server(EDCNPCState NewState)
{
    if (!HasAuthority()) return;    // 클라에선 실행 안함

    CurrentState = NewState;

    // NPC 의 블랙보드도 갱신
    if (ADCNPCController* NPCCon = Cast<ADCNPCController>(GetController()))
    {
        NPCCon->SetNPCState(NewState);
    }

    //UE_LOG(LogTemp, Display, TEXT("Server: CUrretnState Set to %d"), (uint8)NewState);
}

// 상태 전환 전 Movement Montage 정지
void ADCNPC::InterruptCurrentAction()
{
    // 서버에서 Action을 강제 중단 : 이동 정지 몽타주 정지
    if (!HasAuthority()) return;

    // 이동 정지
    if (AAIController* AICon = Cast<AAIController>(GetController()))
    {
        AICon->StopMovement();
    }

    // 애니 몽타주 정지
    if (UAnimInstance* AnimInst = GetMesh()->GetAnimInstance())
    {
        AnimInst->Montage_Stop(0.2f);   // 0.2초동안 부드럽게 정지
    }

    // 타이머 초기화
    GetWorldTimerManager().ClearTimer(AnimationTimerHandle);
}


void ADCNPC::StartStateWithMontage(EDCNPCState NewState, UAnimMontage* Montage, float DurationOverride, TFunction<void()> OnFinishedCallback)
{
    if (!HasAuthority()) return;

    // Sleeping 상태 중복 처리 방지
    if (CurrentState == EDCNPCState::Sleeping && NewState != EDCNPCState::Sleeping)
    {
        return;
    }

    // 상태 전환 전 Movement Montage 정지
    InterruptCurrentAction();

    // 서버에서 상태 설정 및 블랙보드 갱신
    SetCurrentState_Server(NewState);

    // 시작할 상태와 재생할 몽타쥬 로그 출력
   // UE_LOG(LogTemp, Log, TEXT("StartStateWithMontage: State=%d Montage=%s"), (uint8)NewState,
    //Montage ? *Montage->GetName() : TEXT("NULL"));

    MulticastPlayMontage(Montage);  // 모든 클라에서 몽타쥬 재생

    // Duration 계산 : Montage가 있으면 Montage 길이 사용, 아니면 DurationOverride이 없다면  3초
    float Duration = Montage ? Montage->GetPlayLength() : 3.f;

    if (DurationOverride > 0.f) Duration = DurationOverride;    // DurationOverride 값이 0보다 크면 설정

    // 수면 중이라면 5초간
    if (NewState == EDCNPCState::Sleeping)
    {
        Duration = FMath::Max(Duration, 5.f);   // Duration이 5초보다 크다면 Duration 적용
    }

    // 타이머: 애니 종료시 Idle 상태로 전환
    GetWorldTimerManager().ClearTimer(AnimationTimerHandle);
    GetWorldTimerManager().SetTimer(AnimationTimerHandle, FTimerDelegate::CreateLambda([=, this]()
    {
        OnAnimationFinished();
        if (OnFinishedCallback) OnFinishedCallback();   // Task 완료 알림
    }) ,Duration, false);
}

void ADCNPC::OnAnimationFinished()
{
    if (!HasAuthority()) return;

    // NPC 누워지는걸 바로 세우도록 설정
    FRotator CurrentRotator = GetActorRotation();
    SetActorRotation(FRotator(0, CurrentRotator.Yaw, 0));

    // 애니 종료 후 다시 Idle로 설정.
    // 여기서 Idle 후 BTService에서 다시 랜덤으로 설정 예정
    SetCurrentState_Server(EDCNPCState::Idle);

    //UE_LOG(LogTemp, Log, TEXT("OnAnimationFinished -> set Idle"));
}

// 상범님 플레이어가 NPC 공격했을때 이 함수 호출해주시면 됩니다.
// 플레이어에게 피격받았을때 함수
void ADCNPC::OnAttacked()
{
    if (!HasAuthority()) return;

    //이미 수면상태라면 로그와함께 실행 안함
    if (CurrentState == EDCNPCState::Sleeping)
    {
        UE_LOG(LogTemp, Log, TEXT("OnAttacked : Already Sleeping"));
        return;
    }

    // 피격받으면 수면상태로 전환
    StartStateWithMontage(EDCNPCState::Sleeping, SleepMontage, 5.f);
}

// 몽타쥬 재생 서버에서 멀티캐스트로 실행
void ADCNPC::MulticastPlayMontage_Implementation(UAnimMontage* Montage, float PlayRate)
{
    if (!Montage) return;

    if (UAnimInstance* AnimInst = GetMesh()->GetAnimInstance())
    {
        //AnimInst->Montage_Play(Montage,PlayRate);

        // Montage 플레이 후 로그 확인
        if (AnimInst->Montage_Play(Montage, PlayRate) > 0.f)
        {
            //UE_LOG(LogTemp, Log, TEXT("Montage %s started playing, PlayRate : %f"), *Montage->GetName(), PlayRate);
        }
        else
        {
            //UE_LOG(LogTemp, Warning, TEXT("Montage %s failed to play"), *Montage->GetName());
        }

        /*// 디버그 : 재생 확인
        if (AnimInst->IsAnyMontagePlaying())
        {
            UAnimMontage* Active = AnimInst->GetCurrentActiveMontage();
            UE_LOG(LogTemp, Log, TEXT("MulticastPlayMontage: playing %s"),
                Active ? *Active->GetName() : TEXT("Unknown"));
        }
        else
        {
            UE_LOG(LogTemp, Warning,
                TEXT("MulticastPlayMontage: Montage_Play called but no montage playing (slot mismatch?)"));
        }*/
    }
}

UAnimMontage* ADCNPC::GetMontageForState(EDCNPCState State) const
{
    for (const FStateMontage& SM : StateMontages)
    {
        if (SM.State == State)
        {
            return SM.Montage;  // 상태값과 일치하는 몽타주 반환
        }
    }

    return nullptr;
}

// 슬라이딩  몽타주 재생 + TargetLocation으로 이동
// TargetLocation : BT에서 제공하는 랜덤좌표 (FindRandomLocation 태스크에서 설정)
// SlidieDistance : 실제 이동 거리(예: 300.f)
void ADCNPC::StartSlidingToLocation(FVector TargetLocation, float SlideDistance)
{
    if (!HasAuthority()) return;

    SetCurrentState_Server(EDCNPCState::Sliding);   // Sliding 상태 설정

    // 슬라이딩 몽타주 가져오기
    UAnimMontage* SlideMontage = nullptr;

    for (const FStateMontage& SM : StateMontages)
    {
        if (SM.State == EDCNPCState::Sliding)
        {
            SlideMontage = SM.Montage;  // SlideMontage 얻어오기
            break;
        }
    }

    //UE_LOG(LogTemp, Display, TEXT("Called StartSlidingTo Location"))
    if (SlideMontage)
    {
        MulticastPlayMontage(SlideMontage); // Slide 몽타주 재생

        // 슬라이드 방향 계산
        FVector StartLocation = GetActorLocation();
        FVector Direction = (TargetLocation-StartLocation);
        //Direction.Z = 0.f;  // 수평 방향만 사용을 위해 z축 초기화
        Direction.Normalize();

        // NPC 방향 전환
        FRotator NewRotator = Direction.Rotation();
        SetActorRotation(NewRotator);

        // 최종 위치 = 현재 위치 + 방향 * 슬라이드 거리(300cm)
        FVector EndLoc = StartLocation + Direction * SlideDistance;

        // 슬라이드 이동 처리(보간 처리 애니 길이에 맞춰서 처리)
        float Duration = 2.f; //SlideMontage->GetPlayLength(); // 몽타주 애니 길이
        float ElapsedTime = 0.f;

        // 타이머 시작 전 기존 타이머 클리어
        GetWorldTimerManager().ClearTimer(SlideTimerHandle);

        GetWorldTimerManager().SetTimer(
            SlideTimerHandle,
            FTimerDelegate::CreateLambda([=, this]() mutable
            {
                ElapsedTime += GetWorld()->GetDeltaSeconds();
                float Alpha = FMath::Clamp(ElapsedTime / Duration, 0.f, 1.f);

                // 보간 위치 (XZ 모두 이동)
                FVector NewLocation = FMath::Lerp(StartLocation, EndLoc, Alpha);

                // 지면에 맞게 위치 보정(수직으로의 LineTrace 범위 설정)
                FVector TraceStart = NewLocation + FVector(0, 0, 50.f);
                FVector TraceEnd = NewLocation - FVector(0, 0, 200.f);

                // NPC 감지 제외
                FHitResult HitResult;
                FCollisionQueryParams Params;
                Params.AddIgnoredActor(this);

                if (GetWorld()->LineTraceSingleByChannel(HitResult, TraceStart, TraceEnd, ECC_Visibility, Params))
                {
                    NewLocation.Z = HitResult.Location.Z;   // Z 위치를 지면에 붙이기
                }

                this->SetActorLocation(NewLocation, true);

                if (Alpha >= 1.f)
                {
                    // 보간이 끝난 후 타이머 클리어
                    this->GetWorldTimerManager().ClearTimer(SlideTimerHandle);
                    this->OnAnimationFinished();  // Idle으로 복귀
                    //UE_LOG(LogTemp, Display, TEXT("Slide Finished, Clearing timer"))
                }
            }),
            0.01f,
            true
            );
    }
}

 

 

생성자에선 리플리케이션을 하고 NPC가 월드에 스폰되자마자 자동 빙의를 설정 해주었다. 

이후 Tags 를 NPC로 추가하여 Player캐릭터가 공격을 했을때 NPC 태그로 구분 할 수 있도록 설계를 해준다.

 

 

 

SetCurrentState_Server() : 서버에서 컨트롤러에 있는 비헤이비어트리가 가지고있는 블랙보드 키값인 CurrentState 값을 새 상태로 전환함

InterruptCurrentAction() :  상태 전환 전 이동과 몽타주 재생을 정지하여 다음 동작을 준비함

 

void StartStateWithMontage() : 서버에 상태 변환 요청과 함께 Multicast를 통해 몽타주 재생을 하여 모든 클라이언트에 동일하게 보여준다.

또한 타이머를 통해 Duration 시간이 지나면 OnAnimtionFinished() 를 통해 애니메이션 끝났을때 로직을 실행함

 

void OnAnimationFinished() : 몽타주 재생이 끝났을때 호출할 함수로 NPC의 회전 값을 바로 세우고 Server에 Idle 상태로 설정을 요청함

 

void OnAttacked() : 플레이어에게 피격 받았을때 호출할 함수로 수면 몽타주를 재생 및 상태 전환을 하여 공격받자마자 수면 상태로 들어가게 설정

 

void MulticastPlayMontage() : 몽타주 재생하는 부분으로 멀티캐스트를 사용하여 모든 클라이언트에 동일한 몽타주가 보이도록 설정

 

 

UAnimMontage* GetMontageForState() const : NPC에서 가지고 있는 Montage 값들을 State와 비교하여 알맞는 몽타주를 반환 한다. IdleState일경우 IdleState에 맞는 Montage 반환

 

void StartSlidingToLocation() : 목표위치와 이동 거리 를 받아 SlidingMontage를 재생하고 TargetLocation 까지 이동을 Timer 람다 함수를 통해 이동 보간을 한다.

 

 

DCNPCController — AIController (BT 실행 / Blackboard 업데이트)

void OnPossess() : 컨트롤러 빙의시 비헤이비어 트리를 재생 및 블랙보드 설정

void SetNPCState() : 블랙보드 컴포넌트에서 CurrentState 값을 설정한다.

 

 

DCNPCBTService_RandomAction — 가중치 기반 랜덤 서비스

더보기
// UDCNPCBTService_RandomAction 헤더파일
#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTService.h"
#include "NPC/DCNPCState.h"
#include "DCNPCBTService_RandomAction.generated.h"

// 가중치 액션 구조체
USTRUCT(BlueprintType)
struct FWeightNPCAction
{
    GENERATED_BODY()

    UPROPERTY(EditAnywhere, Category = "NPC|Action")
    EDCNPCState State = EDCNPCState::Idle;

    // 가중치 최소값은 0으로 제한
    UPROPERTY(EditAnywhere, Category = "NPC|Action", meta = (ClampMin="0"))
    int32 Weight = 10;
};


/**
 *  Service : 주기적으로 가중치 기반으로 행동 결정후
 *  Blackboard에 있는 CurrentState를 설정
 */
UCLASS()
class DC_API UDCNPCBTService_RandomAction : public UBTService
{
	GENERATED_BODY()

public:
    UDCNPCBTService_RandomAction();
    // 가중치 리스트 (에디터에서 추가/수정)
    UPROPERTY(EditAnywhere, Category = "NPC|Random")
    TArray<FWeightNPCAction> NPCActions;

    virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};

 

// UDCNPCBTService_RandomAction Cpp

#include "NPC/DCNPCBTService_RandomAction.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "AIController.h"

UDCNPCBTService_RandomAction::UDCNPCBTService_RandomAction()
{
    // 틱 주기 5초에 한번
    Interval = 5.f;
    RandomDeviation = 0.5f; // 주기 랜덤 차 .5초로 해당 서비스 틱 노드는 4.5~5.5 초 사이로 반복

    NodeName = TEXT("NPC Random Action");

    NPCActions.Add({EDCNPCState::Moving, 20});
    NPCActions.Add({EDCNPCState::Dancing, 30});
    NPCActions.Add({EDCNPCState::Sliding, 30});
    NPCActions.Add({EDCNPCState::Idle, 20});
}

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

    // Get Blackboard
    UBlackboardComponent* BbComp = OwnerComp.GetBlackboardComponent();
    if (!BbComp) return;

    uint8 Current = BbComp->GetValueAsEnum(FName("CurrentState"));
    EDCNPCState CurrentState = static_cast<EDCNPCState>(Current);   // Sate를 EDCNPCState 구조체로 캐스팅

    // 이미 Sleeping 이면 변경 안함
    //if (BbComp->GetValueAsEnum(FName("CurrentState")) == (uint8)EDCNPCState::Sleeping) return;
    if (CurrentState == EDCNPCState::Sleeping) return;



    // 총 가중치 계산
    int32 TotalWeight = 0;
    for (const FWeightNPCAction& A : NPCActions)
    {
        TotalWeight += FMath::Max(0,A.Weight);
    }
    if (TotalWeight <= 0) return;   // 총 가중치가 0이하면 안되므로 실행 안함


    int32 Pick = FMath::RandRange(1, TotalWeight);
    int32 Accum = 0;
    EDCNPCState SelectedState = EDCNPCState::Idle;
    for (const FWeightNPCAction& A : NPCActions)
    {
        Accum += FMath::Max(0,A.Weight);
        if (Pick <= Accum)
        {
            SelectedState = A.State;
            break;
        }
    }

    BbComp->SetValueAsEnum(FName("CurrentState"), (uint8)SelectedState);

    //UE_LOG(LogTemp, Log, TEXT("Random Action Selected State : %d"), (uint8)SelectedState);
}

서비스 태스크를 통해 CurretnState가 Idle일때만 랜덤하게 설정 하는 로직을 구현 

 

 

 

DCNPCBTTask_PlayMontage — 상태 기반으로 몽타주 재생 (Latent)

더보기
// UDCNPCBTTaskNode_PlayMontage.h

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "DCNPCBTTaskNode_PlayMontage.generated.h"

/**
 *
 */
UCLASS()
class DC_API UDCNPCBTTaskNode_PlayMontage : public UBTTaskNode
{
	GENERATED_BODY()

public:
    UDCNPCBTTaskNode_PlayMontage();

    UPROPERTY(EditAnywhere, Category = "NPC|Blackboard")
    struct FBlackboardKeySelector CurrentStateKey;

    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};

 

// UDCNPCBTTaskNode_PlayMontage.cpp


#include "NPC/DCNPCBTTaskNode_PlayMontage.h"

#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "NPC/DCNPC.h"

UDCNPCBTTaskNode_PlayMontage::UDCNPCBTTaskNode_PlayMontage()
{
    NodeName = TEXT("PlayMontage (State->Montage)");
}


// 몽타주 재생하는 로직을 담당하는 태스크 노드
// 애니재생 끝나기전 EBTNodeResult::Succeeded 를 반환하면 비헤이비어 트리는 처음부터 다시 실행하게됨
// 따라서 재생이 덜 끝난 몽타주 재생을 중복적으로 호출함
// 이를 위해 애니메이션 끝날때 까지 InProgress 상태를 유지
// 끝나면 FinishLatentTask()를 호출
EBTNodeResult::Type UDCNPCBTTaskNode_PlayMontage::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    // Get AIController
    AAIController* AICon = OwnerComp.GetAIOwner();
    if (!AICon) return EBTNodeResult::Failed;

    //Get NPC
    ADCNPC* NPC = Cast<ADCNPC>(AICon->GetPawn());
    if (!NPC) return EBTNodeResult::Failed;

    // Blackboard에서 CurrentState 읽기
    uint8 StateValue = OwnerComp.GetBlackboardComponent()->GetValueAsEnum(CurrentStateKey.SelectedKeyName);
    EDCNPCState CurrentState = static_cast<EDCNPCState>(StateValue);

    if (NPC->HasAuthority())
    {
        // 상태값과 일치하는 몽타주를 얻어옴
        UAnimMontage* Montage = NPC->GetMontageForState(CurrentState);

        if (!Montage)
        {
            // 몽타주 없을시 로그와 함께 태스크 Fail처리
            UE_LOG(LogTemp, Warning, TEXT("Not mapping State & Montage"));
            return EBTNodeResult::Failed;
        }
        // 상태에 맞는 몽타주 재생
        // NPC 가 애니 종료 후 Task완료 콜백 호출
        NPC->StartStateWithMontage(CurrentState, Montage, -1.f, [this, &OwnerComp]()
        {
            FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);  // 태스크를 성공적으로 끝냄
        });

        return EBTNodeResult::InProgress;   // 진행 중
    }

    return EBTNodeResult::Succeeded;
}

ExecuteTask에서 바로 성공을 반환하지 않고 InProgress를 통해 태스크 진행중으로 유지한다.

이후 콜백함수 FinishLatentTask() 를 호출하면 BT가 다음 노드로 이어지도록 구현.

 

 

 

DCNPCBTTask_SlideToLocation — 슬라이드 Task (Latent)

더보기
// UDCNPC_BTTask_SlideToLocation.h

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/Tasks/BTTask_BlackboardBase.h"
#include "DCNPC_BTTask_SlideToLocation.generated.h"

/**
 *
 */
UCLASS()
class DC_API UDCNPC_BTTask_SlideToLocation : public UBTTask_BlackboardBase
{
	GENERATED_BODY()

public:
    UDCNPC_BTTask_SlideToLocation();

protected:
    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};

 

// UDCNPC_BTTask_SlideToLocation.cpp

#include "NPC/DCNPC_BTTask_SlideToLocation.h"

#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "NPC/DCNPC.h"
#include "GameFramework/CharacterMovementComponent.h"

UDCNPC_BTTask_SlideToLocation::UDCNPC_BTTask_SlideToLocation()
{
    NodeName = "SlideToLocation";
}

EBTNodeResult::Type UDCNPC_BTTask_SlideToLocation::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
    if (AAIController* AICon = OwnerComp.GetAIOwner())
    {
        if (ADCNPC* NPC = Cast<ADCNPC>(AICon->GetPawn()))
        {
            UBlackboardComponent* BbComp = OwnerComp.GetBlackboardComponent();
            FVector TargetLocation = BbComp->GetValueAsVector(GetSelectedBlackboardKey());      // 에디터에서 선택한 블랙보드 키값 얻기(TargetLocation)

            // 슬라이딩 상태로 전환 + 애니메이션 재생
            NPC->StartSlidingToLocation(TargetLocation, 300.f);
            return EBTNodeResult::Succeeded;
        }
    }

    return EBTNodeResult::Failed;
}

NPC에 있는 StartSlidingToLocation()에 슬라이딩 상태로 전환 + 애니 재생을 실행한다.

 

 

3. 언리얼 에디터 설정

비헤이비어 트리는 위처럼 설정해준다. 각 태스크엔 블랙보드에 있는 TargetLocation을 지정해준다.

 

 

NPC 블루프린트에선 StateMontages에서 DCNPCState.h 에 정의한 상태값들과 일치하는 몽타주들을 지정해준다.

해당 몽타주들을 State에 매핑하여 비헤이비어 트리에서 실행되는 구조이다.