
12.1 AI 컨트롤러 및 내비게이션 시스템
프롤로그)
– 언리얼 엔진의 폰은 조종당할 운명을 가진 배우입니다.
이전에는 플레이어가 폰을 수동적으로 제어했습니다.
– 행위수 모델을 이용한 인공지능 컴퓨터 설계,
AI 컨트롤러를 사용하여 AI가 플레이어가 아닌 NPC를 제어하도록 합니다.
농부는 플레이어 컨트롤러와 마찬가지로 AI 컨트롤러가 소유합니다.
Def) NPC(논플레이어 캐릭터)
플레이어가 제어하지 않지만 레벨에 배치되고 독립적으로 행동하는 캐릭터를 NPC라고 합니다.
전. 그려지고 배치된 GWCharacter는 레벨에 가만히 서 있는 NPC입니다.
주) AI 컨트롤러를 생성하여 NPC에게 전달
– 파일 > 새 C++ 클래스 > AIController 상위 클래스 > “GWAIController”
– GWCharacter가 사용할 수 있도록 AIControllerClass 멤버를 설정합니다.
GWAIController 클래스에 할당됩니다.
– AI 스폰 옵션을 PlaceInWorldOrSpawned로 설정합니다.
그런 다음 향후 레벨에 배치하거나 생성하는 각각의 새로운 GWGhost에 대해
GWAIController 액터는 플레이어 제어 캐릭터 없이 생성됩니다.
모든 캐릭터는 GWAIController가 소유합니다.
<hide/>
// GWCharacter.cpp
...
#include "GWAIController.h"
AGWCharacter::AGWCharacter()
...
{
...
AIControllerClass = AGWAIController::StaticClass();
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}
...
주) 네비 메쉬 영역 생성
– NPC는 스스로 이동해야 하므로 이를 도와줄 장치가 필요합니다.
이때 내비게이션 네트가 널리 사용된다.
– 파일 > 현재 레이어를 다른 이름으로 저장 > 다른 이름으로 저장 Step3.
액터 배치 > 볼륨 > NavMeshBoundsVolume을 월드에 배치합니다. 변신 위치를 초기화합니다.
세부정보 > 브러시 설정 > 10000 x 10000 x 500cm
– 뷰포트 > P 키 클릭
그러면 에디터에서 구축한 내비 메시 영역이 뷰포트에 녹색으로 표시됩니다.
– 빙의된 캐릭터에게 대상이 있음을 알리는 명령어가 추가되어 대상을 향해 스스로 이동합니다.
AI 컨트롤러에 타이머를 설치하여 3초마다 캐릭터가 목적지로 이동하도록 명령합니다.
내비게이션 시스템은 무작위로 움직이는 표적을 가져옵니다.
GetRandomPointInNavigableRadius() 함수 및
휴대폰을 목적지로 이동시키기 위한 SimpleMoveToLocation() 함수를 제공합니다.
<hide/>
// GhostWar5.Build.cs
using UnrealBuildTool;
public class GhostWar5 : ModuleRules
{
public GhostWar5(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string() { "Core", "CoreUObject", "Engine", "InputCore", "UMG", "NavigationSystem" });
PrivateDependencyModuleNames.AddRange(new string() { });
}
}
<hide/>
// GWAIController.h
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "GWAIController.generated.h"
UCLASS()
class GHOSTWAR_API AGWAIController : public AAIController
{
GENERATED_BODY()
public:
AGWAIController();
protected:
virtual void OnPossess(APawn* InPawn) override;
virtual void OnUnPossess() override;
private:
void OnRepeatTimer();
private:
FTimerHandle RepeatTimerHandle;
float RepeatInterval;
};
<hide/>
// GWAIController.cpp
#include "GWAIController.h"
#include "NavigationSystem.h"
#include "Blueprint/AIBlueprintHelperLibrary.h"
AGWAIController::AGWAIController()
: RepeatTimerHandle()
, RepeatInterval(3.f)
{
}
void AGWAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
GetWorld()->GetTimerManager().SetTimer(RepeatTimerHandle, this, &AGWAIController::OnRepeatTimer, RepeatInterval, true);
}
void AGWAIController::OnUnPossess()
{
Super::OnUnPossess();
GetWorld()->GetTimerManager().ClearTimer(RepeatTimerHandle);
}
void AGWAIController::OnRepeatTimer()
{
UE_LOG(LogTemp, Error, TEXT("OnRepeatTimer()"));
APawn* ControllingPawn = GetPawn();
if (nullptr != ControllingPawn)
{
UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(GetWorld());
if (nullptr != NavSystem)
{
FNavLocation NextLocation;
if (true == NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.f, NextLocation))
{
UAIBlueprintHelperLibrary::SimpleMoveToLocation(this, NextLocation.Location);
}
}
}
}
12.2 비헤이비어 트리 시스템
Def) 비헤이비어 트리
보다 복잡한 NPC 행동 패턴을 구현하기 위해 행동 트리 모델 및 편집기가 제공됩니다.
이를 통해 AI 컨트롤러가 수행할 동작을 체계적으로 설계할 수 있습니다.
행동 트리는 NPC가 취해야 할 조치를 분석하고 우선 순위에서 조치로 우선 순위를 지정합니다.
NPC가 실행할 트리 구조를 설계하는 기술입니다.
참고) 흑판과 행동 트리
– 칠판
인공 지능 평가에 사용되는 데이터 세트
– 행동 트리
패널 데이터를 기반으로 설계된 트리
주) 게시판 및 행위트리 생성
– 해당 기능을 사용하기 위해서는 AIModule 모듈을 추가해야 합니다.
– 콘텐츠 브라우저 > 콘텐츠 폴더 > “AI” 폴더 생성
AI 폴더의 빈 공간 우클릭 > 인공지능 > 흑판 > “BBGWCharacter” 생성.
다시: AI > 비헤이비어 트리 > “BTGWCharacter” 생성.
– 비헤이비어 트리 > 자산을 두 번 클릭합니다. 편집기 > 작업 공간을 마우스 오른쪽 버튼으로 클릭
작업 그룹 > 대기 작업 만들기.
Wait 작업은 Pawn에게 지정된 시간 동안 대기하도록 지시합니다.
– 작업은 독립적으로 실행할 수 없으며 복합 노드를 통해 실행해야 합니다.
빈 공간 > 합성 패널 > 시퀀스 합성 만들기를 마우스 오른쪽 버튼으로 클릭합니다. 관련 작업
잘못된 결과가 반환될 때까지 작업을 왼쪽에서 오른쪽으로 계속 실행하는 화합물입니다.
시퀀스 조합을 루트에 추가한 다음 대기 작업을 조합에 추가합니다.
– 행동 트리 자산을 저장하고 우리가 만든 칠판 및 행동 트리 자산을 사용하십시오.
GWAIController에서 사용할 코드를 추가했습니다.
이전에 생성된 GWAIController 클래스의 모든 코드가 동작 트리에 추가됩니다.
드라이브에 맞게 다시 작성해야 합니다.
<hide/>
// TestUE4.Build.cs
using UnrealBuildTool;
public class TestUE4 : ModuleRules
{
public TestUE4(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string() { "Core", "CoreUObject", "Engine", "InputCore", "UMG", "HeadMountedDisplay", "NavigationSystem", "AIModule"});
PrivateDependencyModuleNames.AddRange(new string() { });
}
}
<hide/>
// GWAIController.h
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "GWAIController.generated.h"
UCLASS()
class GHOSTWAR5_API AGWAIController : public AAIController
{
GENERATED_BODY()
public:
AGWAIController();
protected:
virtual void OnPossess(APawn* InPawn) override;
private:
UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category=AI, Meta=(AllowPrivateAccess=true))
class UBlackboardData* BBAsset;
UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category=AI, Meta=(AllowPrivateAccess=true))
class UBehaviorTree* BTAsset;
};
<hide/>
// GWAIController.cpp
#include "GWAIController.h"
#include "BehaviorTree/BlackboardData.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardComponent.h"
AGWAIController::AGWAIController()
{
static ConstructorHelpers::FObjectFinder<UBlackboardData> BBObject(TEXT("BlackboardData'/Game/AI/BBGWCharacter.BBGWCharacter'"));
if (true == BBObject.Succeeded())
{
BBAsset = BBObject.Object;
}
static ConstructorHelpers::FObjectFinder<UBehaviorTree> BTObject(TEXT("BehaviorTree'/Game/AI/BTGWCharacter.BTGWCharacter'"));
if (true == BTObject.Succeeded())
{
BTAsset = BTObject.Object;
}
}
void AGWAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
UBlackboardComponent* GWBlackboard = Cast<UBlackboardComponent>(Blackboard);
if (true == UseBlackboard(BBAsset, GWBlackboard))
{
if (true == RunBehaviorTree(BTAsset))
{
UE_LOG(LogTemp, Error, TEXT("RunBehaviorTree"));
}
}
// if (true == UseBlackboard(BBAsset, Blackboard) && false == RunBehaviorTree(BTAsset))
// 같은 소괄호 내에서 어떤 함수를 먼저 판정할지는 정해져 있지 않음.
// 따라서 위와 같은 코드는 작성을 지양.
}
주) 에디터 구조 변경
– 더 큰 공간을 확보하려면 왼쪽 상단의 배우 배치 창을 여세요.
세부정보 창 옆에 첨부합니다.
– 레이아웃이 마음에 들지 않는다면,
창 > 레이아웃 재설정을 클릭하여 원래 레이아웃을 복원할 수 있습니다.
– 비헤이비어 트리 자산을 더블 클릭하여 연 다음 Alt + P를 누릅니다.
뷰포트에서 게임을 하면 비헤이비어 트리의 논리적 흐름을 함께 볼 수 있습니다.
주) 패트롤 기능 구현
– Blackboard는 특정 유형의 데이터를 저장하고
이를 활용하도록 동작 트리를 구성할 수 있습니다.
– NPC 순찰 기능을 구현하기 위해서는 두 가지 유형의 데이터가 필요합니다.
칠판에 NPC 생성 위치 값 벡터 유형으로 키를 생성합니다. 이름은 “홈포스”입니다.
또한 순찰을 위한 위치 정보를 보관할 수 있는 보드 버튼을 추가했습니다. 벡터 유형 코드 “PatrolPos” 생성 후
– AI 컨트롤러에서 보드의 homepos 키 값을 지정하는 로직을 구현했습니다.
비헤이비어 트리를 실행하기 전에 준비하다.
– Blackboard 키의 이름인 “HomePos” 값을 사용하여 GWAIController에 FName 멤버를 추가했습니다.
여기에 HomePos라는 값을 할당합니다.
– 이 코드는 연결된 키 이름이 미래에 절대 변경되지 않는다고 가정합니다.
정적 const를 사용하여 변수의 초기 값을 설정합니다. 이렇게 선언하면 앞으로의 코드에서
연관된 값을 참조하는 것이 편리하지만 변경하려면 값을 하드코딩해야 하는 단점이 있습니다.
– 재생 버튼을 누르면 행동 트리 편집기의 패널에 HomePos 키 값이 표시됩니다.
잘 전달될 수 있도록 합시다.
<hide/>
// GWAIController.h
...
class GHOSTWAR5_API AGWAIController : public AAIController
{
...
public:
static const FName HomePosKey;
static const FName PatrolPosKey;
private:
...
};
<hide/>
// GWAIController.cpp
...
const FName AGWAIController::HomePosKey(TEXT("HomePos"));
const FName AGWAIController::PatrolPosKey(TEXT("PatrolPos"));
...
void AGWAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
UBlackboardComponent* GWBlackboard = Cast<UBlackboardComponent>(Blackboard);
if (true == UseBlackboard(BBAsset, GWBlackboard))
{
GWBlackboard->SetValueAsVector(HomePosKey, InPawn->GetActorLocation());
if (true == RunBehaviorTree(BTAsset))
{
UE_LOG(LogTemp, Error, TEXT("RunBehaviorTree"));
}
}
}
주) 비헤이비어 트리에서 블랙보드 값 업데이트
– GamePlayTasks 모듈 가져오기
– NPC가 이동하는 위치인 PatrolPos 데이터가 생성되어야 합니다.
순찰할 때마다 변경되므로 작업을 생성하세요.
디자인 적으로는 보드에 값을 쓰는 것이 좋습니다.
– 파일 > 새 C++ 클래스 > BTTask 상위 클래스 > “BTTask_GetPatrolPos”
이때 BTTask_ 접두사 부분은 UI에 표현될 때 자동으로 걸러진다.
<hide/>
// TestUE4.Build.cs
using UnrealBuildTool;
public class TestUE4 : ModuleRules
{
public TestUE4(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string() { "Core", "CoreUObject", "Engine", "InputCore", "UMG", "HeadMountedDisplay", "NavigationSystem", "AIModule", "GamePlayTasks"});
PrivateDependencyModuleNames.AddRange(new string() { });
}
}
주) 함수 ExecuteTask()
– 취소 된
작업 실행 중에 중단합니다. 결과적으로 실패했습니다.
– 실패한
작업을 수행했지만 실패했습니다.
– 성공적인
작업이 성공적으로 실행되었습니다.
– 진행 중
작업은 계속됩니다.
과제 수행 결과는 추후 공지될 예정이다.
ExecuteTask() 함수의 실행 결과에 따라 그룹 내 다음 작업이 실행됩니다.
계속할 것인지 취소할 것인지에 대한 결정이 내려질 것입니다. 현재 시퀀스 합성은
실패 지점까지 자신에게 속한 작업을 계속 수행하는 속성이 있습니다.
ExecuteTask() 함수에서 다음 탐색 지점을 찾고 실행 결과를 즉시 반환하는 로직을 구현합니다.
구현된 반환 작업 이름도 다른 이름으로 표시하려면
NodeName 속성을 다른 값으로 설정하기만 하면 됩니다.
<hide/>
// BTTask_GetPatrolPos.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_GetPatrolPos.generated.h"
UCLASS()
class GHOSTWAR5_API UBTTask_GetPatrolPos : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTask_GetPatrolPos();
protected:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};
<hide/>
// BTTask_GetPatrolPos.cpp
#include "BTTask_GetPatrolPos.h"
#include "GWAIController.h"
#include "NavigationSystem.h"
#include "BehaviorTree/BlackboardComponent.h"
UBTTask_GetPatrolPos::UBTTask_GetPatrolPos()
{
NodeName = TEXT("GetPatrolPos");
}
EBTNodeResult::Type UBTTask_GetPatrolPos::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);
APawn* ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
if (nullptr == ControllingPawn)
{
return EBTNodeResult::Failed;
}
UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetNavigationSystem(ControllingPawn->GetWorld());
if (nullptr == NavSystem)
{
return EBTNodeResult::Failed;
}
FVector HomePos = OwnerComp.GetBlackboardComponent()->GetValueAsVector(AGWAIController::HomePosKey);
FNavLocation NextLocation;
if (true == NavSystem->GetRandomPointInNavigableRadius(FVector::ZeroVector, 500.f, NextLocation))
{
OwnerComp.GetBlackboardComponent()->SetValueAsVector(AGWAIController::PatrolPosKey, NextLocation.Location);
return EBTNodeResult::Succeeded;
}
return EBTNodeResult::Type();
}
주) 동작 트리 편집기 변경
– 대기 작업의 오른쪽에 FindPatrolPos를 배치합니다.
순찰 위치 찾기 오른쪽에 MoveTo 작업을 추가합니다.
PatrolPos BlackboardKey 할당
– 복합 시퀀스를 통해 Wait 작업이 성공하면 FindPatrolPos 작업이 실행됩니다.
FindPatrolPos 작업이 성공하면 FindPatrolPos에 지정된 항목이
MoveTo 작업은 PatrolPos 키 값을 참조하여 실행됩니다.
12.3 NPC 추적 기능 구현
참고) NPC 플레이어 사냥
– NPC가 플레이어를 찾으면 해당 플레이어의 정보를 보드에 저장합니다.
객체 유형으로 “Target” 키를 생성합니다.
– Target Key -> Blackboard Details > Key Type > Change Base Class를 GWCharacter로 클릭합니다.
<hide/>
// GWAIController.h
...
class GHOSTWAR5_API AGWAIController : public AAIController
{
...
public:
static const FName HomePosKey;
static const FName PatrolPosKey;
static const FName TargetKey;
private:
...
};
<hide/>
// GWAIController.cpp
...
const FName AGWAIController::HomePosKey(TEXT("HomePos"));
const FName AGWAIController::PatrolPosKey(TEXT("PatrolPos"));
const FName AGWAIController::TargetKey(TEXT("Target"));
...
주) 셀렉터 컴포지트 사용
– NPC의 행동패턴은 플레이어를 발견했는지,
미발견 여부에 따라 추적과 정찰로 나뉜다.
– 추적과 정찰 중 하나를 선택하는 것이므로 Selector Composite를 사용하십시오.
논리의 확장이 적절합니다. 추적에 우선 순위가 부여되며 추적 논리는
동작 트리 디자인을 확장하여 보드의 목표를 향해 이동합니다.

데프) 서비스 노드
자신이 속한 복합 노드가 활성화되면 TickNode() 함수가 주기적으로 호출됩니다.
호출 기간은 서비스 노드 내에서 설정된 Interval 속성 값으로 지정할 수 있습니다.
주) 서비스 노드로 반지름에 문자가 있는지 감지
– 새 C++ 클래스 > BTService 상위 클래스 > “BTService_Detect”
– TickNode() 함수에서 NPC의 위치를 기준으로 반경 6미터 이내
문자가 있는지 여부 감지를 구현합니다.
– 반경 내에 다른 NPC TU 캐릭터가 있다고 가정하면,
OverlapMultipleByChannel() 함수를 사용하여 모든 문자를 일치시킵니다.
– 반경 내에서 감지된 모든 문자 정보는 목록 관리에 적합합니다.
데이터 구조 TArray로 전달됩니다.
– 서비스가 생성되면 선택한 컴포지트를 마우스 오른쪽 버튼으로 클릭하고 서비스 메뉴에서
인식을 선택하여 컴포지트에 연결합니다.
<hide/>
// BTService_Detect.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTService.h"
#include "BTService_Detect.generated.h"
UCLASS()
class GHOSTWAR5_API UBTService_Detect : public UBTService
{
GENERATED_BODY()
public:
UBTService_Detect();
protected:
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};
<hide/>
// BTService_Detect.cpp
#include "BTService_Detect.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "GWAIController.h"
UBTService_Detect::UBTService_Detect()
{
NodeName = TEXT("Detect");
Interval = 1.f;
}
void UBTService_Detect::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
APawn* ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
if (nullptr == ControllingPawn)
{
return;
}
UWorld* GWWorld = ControllingPawn->GetWorld();
if (nullptr == GWWorld)
{
return;
}
FVector CenterPos = ControllingPawn->GetActorLocation();
float DetectRadius = 600.f;
TArray<FOverlapResult> OverlapResults;
FCollisionQueryParams CollisionQueryParams(NAME_None, false, ControllingPawn);
bool bResult = GWWorld->OverlapMultiByChannel(
OverlapResults,
CenterPos,
FQuat::Identity,
ECollisionChannel::ECC_GameTraceChannel2,
FCollisionShape::MakeSphere(DetectRadius),
CollisionQueryParams
);
DrawDebugSphere(GWWorld, CenterPos, DetectRadius, 16, FColor::Red, false, 0.2f);
}
주) 선수 식별
– NPC가 감지 범위 내의 캐릭터를 발견하면,
그중에서 우리가 조종하는 캐릭터를 근절해야 합니다.
– 캐릭터를 제어하는 컨트롤러가 플레이어 컨트롤러인지 확인하기 위해
IsPlayerController() 함수 사용. 플레이어 캐릭터가 인식되면 태블릿이
대상 값을 플레이어 캐릭터로 설정하고 그렇지 않으면 nullptr 값으로 설정합니다.
– 플레이어 캐릭터를 인식하면 초록색 구체를 그려주고,
NPC와 캐릭터를 연결하는 추가 선을 그립니다.
<hide/>
// BTService_Detect.cpp
...
void UBTService_Detect::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
...
if (true == bResult)
{
for (auto const& OverlapResult : OverlapResults)
{
ATUCharacter* TUCharacter = Cast<ATUCharacter>(OverlapResult.GetActor());
if (nullptr != TUCharacter)
{
if (true == TUCharacter->GetController()->IsPlayerController())
{
UE_LOG(LogTemp, Error, TEXT("DETECTED"));
OwnerComp.GetBlackboardComponent()->SetValueAsObject(ATUAIController::TargetKey, TUCharacter);
DrawDebugSphere(World, Center, DetectRadius, 16, FColor::Green, false, 0.2f);
DrawDebugPoint(World, TUCharacter->GetActorLocation(), 10.f, FColor::Blue, false, 0.2f);
DrawDebugLine(World, ControllingPawn->GetActorLocation(), TUCharacter->GetActorLocation(), FColor::Blue, false, 0.2f);
return;
}
}
else
{
OwnerComp.GetBlackboardComponent()->SetValueAsObject(ATUAIController::TargetKey, nullptr);
DrawDebugSphere(World, Center, DetectRadius, 16, FColor::Red, false, 0.2f);
}
}
UE_LOG(LogTemp, Error, TEXT("NOT DETECTED"));
}
}
주) NPC 로테이션 수정
– NPC가 움직일 때 부자연스럽게 회전이 꼬이는 것을 볼 수 있습니다.
NPC를 위한 별도의 제어 모드 추가
NPC는 이동 방향에 따라 회전하도록 캐릭터의 이동 설정을 변경합니다.
– NPC의 최대 이동 속도를 플레이어보다 낮게 설정하여
플레이어가 NPC로부터 도망칠 수 있습니다.
<hide/>
// TUCharacter.h
...
class TESTUE4_API ATUCharacter : public ACharacter
{
GENERATED_BODY()
enum class EControlMode
{
GTA,
DIABLO,
NPC,
END
};
public:
...
protected:
virtual void PostInitializeComponents() override;
virtual void BeginPlay() override;
virtual void Tick(float DeltaTime) override;
virtual void PossessedBy(AController* NewController) override;
...
private:
...
};
<hide/>
// TUCharacter.cpp
...
void ATUCharacter::SetControlMode(EControlMode ControlMode)
{
CurrentControlMode = ControlMode;
switch (CurrentControlMode)
{
...
case EControlMode::NPC:
bUseControllerRotationYaw = false;
GetCharacterMovement()->bUseControllerDesiredRotation = false;
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.f, 480.f, 0.f);
break;
default:
break;
}
}
...
void ATUCharacter::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
if (true == IsPlayerControlled())
{
SetControlMode(EControlMode::DIABLO);
GetCharacterMovement()->MaxWalkSpeed = 600.f;
}
else
{
SetControlMode(EControlMode::NPC);
GetCharacterMovement()->MaxWalkSpeed = 300.f;
}
}
...
Def) 데코레이터 노드
패널의 값에 따라 특정 합성을 실행할지 여부를 결정하는 노드입니다.
주) 데코레이터에 따라 추격과 정찰로 구분
– 이제 서비스 실행 결과에 따라 선택자 데코레이터의 왼쪽을 헌팅하고,
오른쪽에는 정찰 로직으로 구분되도록 행동 트리 로직을 정리해보자.
서비스 결과는 블랙보드의 target key에 값이 있는지 여부에 따라 세분화될 수 있습니다.
– 좌측 Sequence Composite 우클릭 > Decorator 추가 > Blackboard 클릭
이 데코레이터를 선택하고 세부 정보 > 흐름 제어로 이동합니다.
해당 키 값의 변경이 감지되면 현재 복합 노드의 실행이 즉시 중단됩니다.
감시자에게 알림을 값이 변경될 때로 설정합니다.
관찰자 중단이 설정되지 않은 경우 네트워크에 속한 작업
모든 것이 준비될 때까지 기다리기 때문에 플레이어가 시야에서 벗어나더라도 NPC가 알아서 해줍니다.
플레이어가 따라잡을 때까지 계속 쫓아가세요. 따라서 Self로 설정하십시오.
Blackboard 섹션에서 Key Query가 설정되고 Target이 Blackboard Key에 할당됩니다.
– 올바른 시퀀스 합성물에 동일한 데코레이터를 추가했습니다.
세부적으로 Flow Control 섹션과 Panel 섹션을 왼쪽의 Decorator와 동일하게 설정합니다.
그러나 오른쪽의 데코레이터는 반대 조건인 Is Not Set로 설정되어 있습니다.

12.4 NPC 공격
주) 추격 후 공격 구현
– NPC의 행동은 플레이어와의 거리에 따라 추격과 공격으로 분기되어야 합니다.
분기를 위해 왼쪽 노드 그룹의 시퀀스 조합을 선택기 조합으로 변경합니다.
– 선택기 컴포지트를 추가하고 데코레이터를 기존 왼쪽 시퀀스 컴포지트로 설정
선택기 컴포지트에 끌어다 놓습니다.

주) 플레이어가 공격 범위에 있는지 여부를 결정하는 데코레이터
새 C++ 클래스 > BTDecorator 상위 클래스 > BTDecorator_IsInAttackRange
Decorator 클래스는 CalculateRawConditionValue() 함수를 상속합니다.
그 목적은 원하는 조건이 충족되었는지 여부를 결정하는 것입니다.
이 함수는 const로 선언되어 있기 때문에 데코레이터 클래스의 멤버 변수 값은 변경할 수 없습니다.
<hide/>
// BTDecorator_IsInAttackRange.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTDecorator.h"
#include "BTDecorator_IsInAttackRange.generated.h"
UCLASS()
class GHOSTWAR_API UBTDecorator_IsInAttackRange : public UBTDecorator
{
GENERATED_BODY()
public:
UBTDecorator_IsInAttackRange();
protected:
virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;
};
<hide/>
// BTDecorator_IsInAttackRange.cpp
#include "BTDecorator_IsInAttackRange.h"
#include "GWAIController.h"
#include "GWCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"
UBTDecorator_IsInAttackRange::UBTDecorator_IsInAttackRange()
{
NodeName = TEXT("IsInAttackRange");
}
bool UBTDecorator_IsInAttackRange::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
bool bResult = Super::CalculateRawConditionValue(OwnerComp, NodeMemory);
APawn* ControllingPawn = OwnerComp.GetAIOwner()->GetPawn();
if (nullptr == ControllingPawn)
{
return false;
}
AGWCharacter* Target = Cast<AGWCharacter>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(AGWAIController::TargetKey));
if (nullptr == Target)
{
return false;
}
bResult = (Target->GetDistanceTo(ControllingPawn) <= 200.f);
return bResult;
}
주) 공격 관련 행위 트리 로직 구현
– 가장 왼쪽 시퀀스 합성물에 IsInAttackRange 데코레이터를 연결합니다.
새 데코레이터의 조건이 참이 되면 공격을 수행해야 합니다.
– 공격 기능은 아직 구현되지 않았습니다. 먼저 공격이 1.5초 동안 수행되었다고 가정하면,
대기 작업을 왼쪽 시퀀스 합성물에 연결합니다. 이렇게 논리를 구성하면
NPC가 플레이어를 따라잡으면 NPC는 1.5초를 기다린 후 다시 플레이어를 쫓습니다.
– 올바른 시퀀스 합성물에도 IsInAttackRange 데코레이터를 추가합니다.
InverseCondition 속성 값을 확인하여 조건을 반전시킵니다.

참고) 기다리지 않고 공격과제 생성
– 새 C++ 클래스 > 상위 클래스 BTTaskNode > “BTTask_Attack”
– 공격 태스크는 공격 애니메이션이 끝날 때까지 기다려야 하는 지연 태스크입니다.
따라서 ExecuteTask()는 InProgress를 한 번 반환합니다. 공격이 끝나면
작업이 완료되었음을 알려줍니다.
– 이를 알려주는 함수가 FinishLatentTask() 함수이다.
작업이 나중에 이 함수를 호출하지 않는 경우
행동 트리 시스템은 현재 작업과 함께 유지됩니다.
– 나중에 FinishLatentTask() 함수를 호출할 수 있도록
노드의 Tick() 함수를 활성화하고 Tick()의 조건을 얻은 후
작업을 종료하는 명령을 실행해야 합니다.
<hide/>
// BTTask_Attack.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_Attack.generated.h"
UCLASS()
class GHOSTWAR_API UBTTask_Attack : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTask_Attack();
protected:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};
<hide/>
// BTTask_Attack.cpp
#include "BTTask_Attack.h"
UBTTask_Attack::UBTTask_Attack()
{
bNotifyTick = true;
}
EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);
return EBTNodeResult::InProgress;
}
void UBTTask_Attack::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}
주) 공격 종료 시 태스크 종료 구현
– 먼저 AI 컨트롤러가 공격 명령을 내릴 수 있도록
GWGhost 클래스 Attack() 함수의 액세스 권한을 public으로 변경합니다.
– 플레이어의 공격이 종료되면 대리인이 공격 작업에 알림을 보내도록 설정됩니다.
새로운 것을 선언하고 공격이 끝날 때 호출하는 논리를 구현하십시오.
– 작업의 적절한 대리자에 Lambda 함수를 등록합니다. Tick()의 함수 논리에서 알아보십시오.
FinishLatentTask() 함수를 호출하여 작업 종료 구현.
– 임시로 붙인 대기태스크 삭제 > 공격태스크로 교체
<hide/>
// AGWGhost.h
...
DECLARE_MULTICAST_DELEGATE(FOnAttackEndedDelegate)
...
class GHOSTWAR_API AGWGhost : public ACharacter
{
...
public:
...
void Attack();
FOnAttackEndedDelegate OnAttackEnded;
protected:
...
};
<hide/>
// GWGhost.cpp
...
void AGWGhost::OnAttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
...
OnAttackEnd.Broadcast();
}
...
<hide/>
// BTTask_Attack.h
...
class GHOSTWAR_API UBTTask_Attack : public UBTTaskNode
{
...
private:
bool IsAttacking;
};
<hide/>
// BTTask_Attack.cpp
#include "BTTask_Attack.h"
#include "GWAIController.h"
#include "GWGhost.h"
UBTTask_Attack::UBTTask_Attack()
: IsAttacking(false)
{
bNotifyTick = true;
}
EBTNodeResult::Type UBTTask_Attack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);
AGWGhost* GWGhost = Cast<AGWGhost>(OwnerComp.GetAIOwner()->GetPawn());
if (nullptr == GWGhost)
{
return EBTNodeResult::Failed;
}
GWGhost->Attack();
IsAttacking = true;
GWGhost->OnAttackEnded.AddLambda((this)() -> void {
IsAttacking = false;
});
return EBTNodeResult::InProgress;
}
void UBTTask_Attack::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);
if (false == IsAttacking)
{
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}
}
주) FMath::RInterpTo 함수로 회전 구현
– 플레이어가 NPC 뒤로 후퇴해도 같은 지점을 공격합니다.
이를 보완하기 위해 플레이어를 향해 공격하고 방향을 바꾸는 기능을 구현했습니다.
– 새 C++ 클래스 > 상위 클래스 BTTaskNode > “BTTask_TurnToTarget”
작업은 일정한 속도로 캐릭터를 향해 회전하는 것입니다.
FMath::RInterpTo() 함수를 사용하여 회전 함수 구현
<hide/>
// BTTask_TurnToTarget.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_TurnToTarget.generated.h"
UCLASS()
class GHOSTWAR_API UBTTask_TurnToTarget : public UBTTaskNode
{
GENERATED_BODY()
public:
UBTTask_TurnToTarget();
protected:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};
<hide/>
// BTTask_TurnToTarget.cpp
#include "BTTask_TurnToTarget.h"
#include "GWAIController.h"
#include "GWGhost.h"
#include "GWCharacter.h"
#include "BehaviorTree/BlackboardComponent.h"
UBTTask_TurnToTarget::UBTTask_TurnToTarget()
{
NodeName = TEXT("TurnToTarget");
}
EBTNodeResult::Type UBTTask_TurnToTarget::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);
AGWGhost* ControllingGhost = Cast<AGWGhost>(OwnerComp.GetAIOwner()->GetPawn());
if (nullptr == ControllingGhost)
{
return EBTNodeResult::Failed;
}
AGWCharacter* TargetCharacter = Cast<AGWCharacter>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(AGWAIController::TargetKey));
if (nullptr == TargetCharacter)
{
return EBTNodeResult::Failed;
}
FVector LookVector = TargetCharacter->GetActorLocation() - ControllingGhost->GetActorLocation();
LookVector.Z = 0.f;
FRotator TargetRot = FRotationMatrix::MakeFromX(LookVector).Rotator();
ControllingGhost->SetActorRotation(FMath::RInterpTo(ControllingGhost->GetActorRotation(), TargetRot, GetWorld()->GetDeltaSeconds(), 2.f));
return EBTNodeResult::Succeeded;
return Result;
}
주) 싱글 병렬 접속
공격 로직에 사용된 시퀀스 연합을 단순 병렬 연합으로 교체하십시오.
캐릭터의 공격을 메인 퀘스트로, 로테이션을 사이드 퀘스트로 설정합니다.
간단한 병렬 연결로 캐릭터가 공격할 수 있고
동시에 캐릭터로 전환하는 작업을 수행합니다.

