

Nav Mesh Bounds Volume 을 레벨에 생성 후 p를 누르면 확인할 수 있다.//Enemy.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Enemy.generated.h"
UCLASS()
class TEMP_THIRD_API AEnemy : public ACharacter
{
GENERATED_BODY()
public:
AEnemy();
UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category = "AI")
TArray<AActor*> PatrolPoints;
};
//Enemy.h
//Enemy.cpp
#include "Enemy.h"
#include "EnemyAIController.h"
AEnemy::AEnemy()
{
AIControllerClass = AEnemyAIController::StaticClass();
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}
//Enemy.cpp
TArray 생성. 월드 내에 배치할 Target Point가 AActor이다.캐릭터-컨트롤러 연결, 배치되거나 스폰되면 자동빙의로 설정해준다.//EnemyAIController.h
UCLASS()
class TEMP_THIRD_API AEnemyAIController : public AAIController
{
GENERATED_BODY()
public:
virtual void OnPossess(APawn* InPawn) override;
virtual void BeginPlay() override;
// 이동 완료시 호출되는 함수
virtual void OnMoveCompleted(
FAIRequestID RequestID,
const FPathFollowingResult& Result) override;
protected:
// 반복 순찰을 위한 인덱스
int32 CurrentPatrolPointIndex = 0;
void MoveToCurrentPatrolPoint();
};
//EnemyAIController.h
//EnemyAIController.cpp
void AEnemyAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
//빙의 시 초기화 작업
}
void AEnemyAIController::BeginPlay()
{
Super::BeginPlay();
MoveToCurrentPatrolPoint();
}
void AEnemyAIController::MoveToCurrentPatrolPoint()
{
AEnemy* EnemyCharcter = Cast<AEnemy>(GetPawn());
if (!EnemyCharcter)
{
UE_LOG(LogTemp, Warning, TEXT("Controller cast failed."));
return;
}
if (EnemyCharcter->PatrolPoints.Num() == 0)
{
UE_LOG(LogTemp, Warning, TEXT("There is no Patrol Points."));
return;
}
MoveToActor(
EnemyCharcter->PatrolPoints[CurrentPatrolPointIndex],
5.0f,
true,
true,
false,
nullptr,
true
);
CurrentPatrolPointIndex = (CurrentPatrolPointIndex + 1) % EnemyCharcter->PatrolPoints.Num();
}
//EnemyAIController.cpp
OnPossess - 컨트롤러가 캐릭터에 빙의하면 호출되는 함수. BeginPlay보다 먼저 호출된다.BeginPlay - 이건 알지? 게임 시작시 순찰을 시작하는 MoveToCurrentPatrolPoint 함수를 호출함.MoveToActor - 부모 클래스인 AIController의 메소드.MoveToLocation이나 둘을 합친 MoveTo도 있음.AAIController::MoveToActor(
AActor* Goal, // 목표로 할 액터
float AcceptanceRadius, // 이동 완료로 간주할 거리. 작을수록 목표 액터에 가까이 붙음
bool bStopOnOverlap, // 목표 액터와 충돌하면 멈추는지 여부, false로 하면 정확한 포인트에 도달하려고 함
bool bUsePathfinding, // 길찾기 알고리즘 사용 여부. 반드시 true
bool bCanStrafe, // false로 꺼줘야 함.
TSubclassOf<UNavigationQueryFilter> FilterClass, // 이동 경로에 필터를 적용하는 파라미터
bool bAllowPartialPaths // 전체 경로를 찾지 못했을 때, 유효한 부분 경로라도 이동할지 여부.
)
//EnemyAIController.cpp
void AEnemyAIController::OnMoveCompleted(
FAIRequestID RequestID,
const FPathFollowingResult& Result)
{
Super::OnMoveCompleted(RequestID, Result);
if (Result.Code == EPathFollowingResult::Success)
{
UE_LOG(LogTemp,
Warning,
TEXT("Move is over. Going to next Point."));
MoveToCurrentPatrolPoint();
}
}
//EnemyAIController.cpp
//AIController.cpp
void AAIController::OnMoveCompleted(
FAIRequestID RequestID,
const FPathFollowingResult& Result)
{
ReceiveMoveCompleted.Broadcast(RequestID, Result.Code);
OnMoveCompleted(RequestID, Result.Code);
}
//AIController.cpp
OnMoveCompleted - 이동 완료시 콜백되는 함수. EPathFollowingResult 값은 다음과 같다.namespace EPathFollowingResult
{
enum Type : int
{
/** Reached destination */
Success,
/** Movement was blocked */
Blocked,
/** Agent is not on path */
OffPath,
/** Aborted and stopped (failure) */
Aborted,
/** DEPRECATED, use Aborted result instead */
Skipped_DEPRECATED UMETA(Hidden),
/** Request was invalid */
Invalid,
};
}

애니메이션 블루프린트에 있다.
bShouldMove값에 따라 결정되는데 이미지를 보면 알 수 있듯 Get Current Acceleration은 플레이어의 입력이 있어야만 값이 존재하기 때문. bShouldMove의 값이 달라지도록 하면 된다.
Try Get Pawn Owner 와 Is Player Controlled 노드로 해당 폰의 소유권이 플레이어 컨트롤러인지 확인한다.true : 기존 bShouldMove로 연결될 값 그대로 연결false : Ground Speed의 값만 bShouldMove로 전달되도록 설정

PatrolPath 클래스를 만들어보자.//PatrolPath.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PatrolPath.generated.h"
UCLASS()
class TEMP_THIRD_API APatrolPath : public AActor
{
GENERATED_BODY()
public:
APatrolPath();
UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category = "Patrol")
TArray<AActor*> Waypoints;
AActor* GetWaypoint(int32 index) const;
int32 Num() const;
};
//PatrolPath.h
WaypointsGetWaypoint()//PatrolPath.cpp
#include "PatrolPath.h"
APatrolPath::APatrolPath()
{
PrimaryActorTick.bCanEverTick = false;
}
AActor* APatrolPath::GetWaypoint(int32 index) const
{
if (Waypoints.IsValidIndex(index))
{
return Waypoints[index];
}
return nullptr;
}
int32 APatrolPath::Num() const
{
return Waypoints.Num();
}
//PatrolPath.cpp

Target Point 들을 배정해준다.BTBehavior Tree가 필요로 하는 데이터를 저장, 공유하는 공간BBAI Perception 컴포넌트와 AI Stimuli Source 컴포넌트 필요함
BT와 BB를 생성한다
BB에서 BT에 쓰일 키(변수라고 생각하면 편함)를 등록해준다.


Decorator - 노드의 보조 실행 조건

키 중 PlayerDetected가 Is Set일 때 이 시퀀스가 실행된다. (예시 이미지. 실제 실행은 플레이어 감지되지 않아야 순찰을 돌기 때문에 Is Not Set이어야 함..)순찰/추격 상황을 분류했으니, 해당 상황에 실행될 명령어 모음을 만들어야 하는데, 이것이 Task이다.

BTTask_Patrol과 BTTask_Chase를 만들어준다.
Task의 내부에 이벤트를 구성해주는 것이다.

Execute, ExecuteAITask가 활성화 될 때 한 번 호출되는 이벤트(BeginPlay처럼). AI가 붙으면 AI컨트롤러도 파라미터로 전달받음.Tick,TickAITask가 실행되는 동안 매 프레임 호출되는 이벤트.Abort, AbortAITask가 실행 종료될 때 한 번 호출되는 이벤트.AbortAI 이벤트에 등록된 내용이 실행되는 것이라 할 수 있겠지?)Get Blackboard Value as <TYPE> 노드를 생성한다. 이때 <TYPE>엔 블랙보드에서 생성한 키와 같은 타입이 들어가야 한다.
Instance Editable 체크박스에 체크해주면 왼쪽 변수 목록의 눈모양 아이콘이 변하게 된다.

AIController 클래스 안에 구현했던 캐릭터의 이동을 담당하는 함수와, 이동 완료 후 호출되는 함수가 Task 안에 들어가있다.
AIController 클래스의 MoveTo에 해당하는 것이 AI MoveTo 노드이다. 이 노드에 필요한 파라미터는 움직일 액터와 목표 대상 혹은 좌표. 여기선 Target Point로 이동할 것이기에 타겟 액터에 연결된 모습.Controlled Pawn에서 바로 가져오면 되지만 목표 대상인 Waypoints를 가져오기 위해 캐스트가 필요.PatrolPathKey 변수를 만들고 그 변수를 BP_PatrolPath에 캐스트->BP_PatrolPath의 멤버인 TArray<AActor*> Waypoints에 접근한다.CurrentWaypointIndexKey 변수를 생성하고 블랙보드 변수에 연결Waypoints 소켓에서 배열에 접근하는 get (a copy) 노드를 생성하여 Target Actor 소켓에 연결. 
AI MoveTo의 Succes 소켓에서 연결되는 부분이 이동 완료 후 호출되는 부분이다.CurrentWaypointIndexKey가 1 증가하는 것.Bind Event to MoveCompleted 노드는 AIController가 이미 가지고 있는 이동 완료 후 호출되는 이벤트 MoveCompleted를 오버라이드하는 노드이다.Key : CurrentWaypointIndexValue : CurrentWaypointIndex = (CurrentWaypointIndex +1) % Waypoints.length()Finish Execute 연결해주면 끝.
BT에서 이루어지기 때문에 빙의시 BT를 실행하도록 해야한다. PatrolPath와 레벨에 배치된 PatrolPath가 동일하다는 것을 엔진에 알려준다. 이때 Key Name은 반드시 블랙보드의 키와 동일해야 한다!



Senses Config에서 원하는 감각 추가 가능
Sense 토글 버튼을 누르면 자세한 설정 가능

순찰/추적의 상황이 PlayerDetected 변수값에 따라 정해지는데, 이 값을 자극-수용의 결과로 결정할 수 있도록 해야한다.AIPerception 컴포넌트의 디테일 패널에서 밑으로 내려보면 다양한 상황의 이벤트가 있다.
On Target Perception Updated 이벤트를 이용해 타겟을 포착했을 때 쫓아오는 행동을 설정하게 된다.AISense_Sight에 포착된 액터가 플레이어라면PlayerDetected 키값을 true로 설정TargetActor 키값을 플레이어 캐릭터로 설정.
2025.4.22(화)추가.모든 레벨에서 내브메시 연산을 수행하게 되면 비효율적이고 리소스를 많이 차지한다. 상용 게임에서 그래픽 최적화를 위해 플레이어 시선이 닿는 부분만 렌더랑하듯, 내브메시 연산도 플레이어 일정 반경에서만 실행되도록 할 수 있다.
관련 문서 : 언리얼 엔진에서 내비게이션 인보커 사용하기

NavigationInvokerComponent 사용을 위해 가장 먼저 해야할 것은 빌드 파일 수정.NavigationSystem을 추가해준다....
// 인보커를 달기 위해 필요한 헤더
#include "NavigationInvokerComponent.h"
...
class ANBC_CH5Character : public ACharacter
{
GENERATED_BODY()
...
// NavigationInvoke 세팅
UPROPERTY(BlueprintReadWrite, Category = "Navigation", meta = (AllowPrivateAccess = "true"))
UNavigationInvokerComponent* NavInvoker;
public:
...
// 내브메시 생성 반경과 제거 반경
float NavGenerattionRadius;
float NavRemovalRadius;
/** Returns NavInvoker subobject**/
FORCEINLINE class UNavigationInvokerComponent* GetNavInvoker() const { return NavInvoker; }
}
...
#include "NavigationInvokerComponent.h"
...
ANBC_CH5Character::ANBC_CH5Character()
{
...
// 생성자에서 초기 설정
NavGenerattionRadius = 10.0f;
NavRemovalRadius = 15.0f;
NavInvoker = CreateDefaultSubobject<UNavigationInvokerComponent>(TEXT("NavInvoker"));
NavInvoker->SetGenerationRadii(NavGenerattionRadius, NavRemovalRadius);
}

Runtime Generation 을 Dynamic으로 변경
Generate Navigation Only Around Navigation Invokers 활성화
