멀티플레이어 (데디서버) 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에 매핑하여 비헤이비어 트리에서 실행되는 구조이다.
'언리얼 팀프로젝트' 카테고리의 다른 글
| 멀티플레이어(데디 서버) 기획 재구성 (0) | 2025.09.19 |
|---|---|
| 멀티 플레이어 (데디서버) NPC 랜덤 행동 구현하기 (트러블 슈팅) (0) | 2025.09.18 |
| 멀티 플레이어 (데디서버) NPC 랜덤 애니재생하기 (0) | 2025.09.11 |
| 멀티 플레이어 (데디서버) NPC 랜덤 위치 스폰 액터 구현 -1 (0) | 2025.09.10 |
| 멀티플레이어 (데디서버) AI 랜덤 좌표 이동 (0) | 2025.09.10 |