[ Unreal Engine ] TIL 📖 ( 13 )
AI 이동시키기 !
코드로 설명해주셨지만 한번에 받아들이기엔 조금 어려워서 코드 하나하나 자세히 분석해봄
1. Navmesh를 활용한 AI의 기본 이해
- EnemyCharacter 설정하기
1) EnemyCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "EnemyCharacter.generated.h"
UCLASS()
class AIPROJECT_API AEnemyCharacter : public ACharacter
{
GENERATED_BODY()
public:
AEnemyCharacter();
// 순찰 포인트들을 저장해둘 배열.
// 에디터에서 인스턴스 단위로 배열을 수정하거나 설정할 수 있도록
// EditInstanceOnly, BlueprintReadWrite 카테고리를 열어둠.
UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category="AI")
TArray<AActor*> PatrolPoints;
};
추가 분석
class AIPROJECT_API AEnemyCharacter : public ACharacter
AEnemyCharacter 는 ACharacter 클래스를 상속받음
UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category="AI")
EditInstanceOnly = 블루프린트 인스턴스에서만 수정가능
BlueprintReadWrite = 블루프린트에서 읽고 쓰기 가능
Category="AI" = 블루프린트 에디터에서 AI 카테고리로 정리됨
TArray<AActor*> PatrolPoints;
TArray<AActor*> => 액터를 저장할 수 있음.
=AI가 이동할 순찰 포인트를 저장하는 배열 생성
2) EnemyCharacter.cpp
#include "EnemyCharacter.h"
#include "EnemyAIController.h"
AEnemyCharacter::AEnemyCharacter()
{
// 이 캐릭터를 컨트롤할 AIController 클래스를 EnemyAIController로 지정
AIControllerClass = AEnemyAIController::StaticClass();
// 레벨에 배치되거나 스폰되면 자동으로 AIController가 할당되도록 설정
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}
추가 분석
AIControllerClass = AEnemyAIController::StaticClass();
AI가 어떤 AI컨트롤러를 사용할지 지정하는 부분. AIControllerClass 에 AEnemyAIController를 연결함.
=적이 스폰되면 AEnemyAIController가 사용됨.
AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
자동 AI 컨트롤 = AI 캐릭터가 레벨에 배치되거나 스폰될 때 자동으로 동작하도록 지정.
3) EnemyAIController.h
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "EnemyAIController.generated.h"
UCLASS()
class AIPROJECT_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 CurrentPatrolIndex = 0;
// 현재 순찰 지점으로 이동하는 공통 함수
void MoveToCurrentPatrolPoint();
};
추가 분석
virtual void OnPossess(APawn* InPawn) override;
AI 컨트롤러가 Pawn을 소유할 때 호출됨.
이때 Pawn은 적군을 뜻함
AI행동 초기 설정(cpp에서 세부 내용 설정)
virtual void BeginPlay() override;
AI가 처음 시작할 때 호출됨.
보통 초기 순찰 or Behavior Tree를 실행하는 코드
virtual void OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result) override;
AI의 이동이 끝난 후 도착했을 때 호출됨.
다음 포인트로 이동 or 추가 행동(공격)을 수행함.
4) EnemyAIController.cpp
#include "EnemyAIController.h"
#include "Navigation/PathFollowingComponent.h"
void AEnemyAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
}
void AEnemyAIController::BeginPlay()
{
Super::BeginPlay();
MoveToCurrentPatrolPoint();
}
void AEnemyAIController::MoveToCurrentPatrolPoint()
{
ASpartaEnemyCharacter* MyEnemyChar = Cast<ASpartaEnemyCharacter>(GetPawn());
if (!MyEnemyChar)
{
return;
}
// 순찰 포인트가 하나도 없다면 이동할 필요 없음
if (MyEnemyChar->PatrolPoints.Num() == 0)
{
return;
}
MoveToActor(
MyEnemyChar->PatrolPoints[CurrentPatrolIndex],
5.0f, // AcceptanceRadius: 목표 지점 근처 몇 유닛 이내에 도달하면 멈출지
true, // bStopOnOverlap
true, // bUsePathfinding
false, // bCanStrafe: 기본 이동 모드에서 좌우로 회전 없이 이동 가능 여부
nullptr,// FilterClass: 경로 필터. 디폴트 사용
true // bAllowPartialPath: 부분 경로 허용 여부
);
CurrentPatrolIndex = (CurrentPatrolIndex + 1) % MyEnemyChar->PatrolPoints.Num();
}
void AEnemyAIController::OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result)
{
Super::OnMoveCompleted(RequestID, Result);
if (Result.Code == EPathFollowingResult::Success)
{
MoveToCurrentPatrolPoint();
}
}
추가 분석
void AEnemyAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
}
AI의 초기 동작을 지정하는 함수.
기본 동작만 수행하도록 설정함.(기능 추가 가능)
void AEnemyAIController::BeginPlay()
{
Super::BeginPlay();
MoveToCurrentPatrolPoint();
}
게임 시작 시 호출되는 BeginPlay함수.
시작 후 MoveToCurrentPatrolPoint() 호출
=게임 시작 시 첫번째 순찰 지점으로 이동
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
4+) MoveToCurrentPatrolPoint 함수 세부 분석
ASpartaEnemyCharacter* MyEnemyChar = Cast<ASpartaEnemyCharacter>(GetPawn());
if (!MyEnemyChar)
{
return;
}
GetPawn()을 이용해 현재 컨트롤러가 조종하는 Pawn을 가져옴
Cast<ASpartaEnemyCharacter>(GetPawn()) => 가져온 Pawn의 정보가 ASpartaEnemyCharacter인지 확인 후 캐스팅
if문을 이용해서 nullptr이면 실행하지 않음
if (MyEnemyChar->PatrolPoints.Num() == 0)
{
return;
}
AI의 순찰 포인트가 없으면 실행하지 않음.
MoveToActor(
MyEnemyChar->PatrolPoints[CurrentPatrolIndex], // 이동할 목표 순찰 지점
5.0f, // AcceptanceRadius: 목표 지점에서 멈출 거리 (5 유닛 이내 도달 시 멈춤)
true, // bStopOnOverlap: 목표 지점과 겹치면 멈출지 여부 (true = 멈춤)
true, // bUsePathfinding: 내비게이션 메시 사용 여부 (true = AI가 경로 탐색)
false, // bCanStrafe: AI가 옆걸음(좌우 이동) 가능한지 여부 (false = 불가능)
nullptr,// FilterClass: 경로 필터 (기본값 사용)
true // bAllowPartialPath: 부분적인 경로라도 허용할지 여부 (true = 허용)
);
MoveToActor() = AI를 특정 액터(여기서는 PatrolPoints[CurrentPatrolIndex] ) 로 이동시키는 함수
CurrentPatrolIndex = (CurrentPatrolIndex + 1) % MyEnemyChar->PatrolPoints.Num();
순찰 포인트 갱신 (인덱스 증가)
% 를 이용해서 마지막 이후에는 다시 처음부터 순환되도록 이용함.
ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
void AEnemyAIController::OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result)
{
Super::OnMoveCompleted(RequestID, Result);
if (Result.Code == EPathFollowingResult::Success)
{
MoveToCurrentPatrolPoint(); // 이동 완료 후 다음 순찰 지점으로 이동
}
}
Result.Code == EPathFollowingResult::Success => 정상적으로 목적지에 도착하면
다음 지점으로 이동
6) APatrolPath.h (순찰 포인트 관리)
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "PatrolPath.generated.h"
UCLASS()
class AICPPPROJECT_API APatrolPath : public AActor
{
GENERATED_BODY()
public:
APatrolPath();
// 레벨에 배치된 웨이포인트 목록
UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category="Patrol")
TArray<AActor*> Waypoints;
// 인덱스로 웨이포인트를 얻는 함수
AActor* GetWaypoint(int32 Index) const;
// 웨이포인트 개수 반환
int32 Num() const;
};
추가 분석
UPROPERTY(EditInstanceOnly, BlueprintReadWrite, Category="Patrol")
TArray<AActor*> Waypoints;
EditInstanceOnly = 레벨에서 개별 인스턴스마다 웨이포인트 설정 가능
(같은 APatrolPath클래스를 여러 개 만들어도 다른 경로로 지정할 수 있다.)
Waypoints = 이동할 포인트들의 배열
AActor* GetWaypoint(int32 Index) const;
인덱스를 이용해서 웨이포인트 받아오기
int32 Num() const;
AI가 순찰을 진행할 때 웨이포인트 갯수 확인
7) APatrolPath.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();
}
추가 분석
AActor* APatrolPath::GetWaypoint(int32 Index) const
{
if (Waypoints.IsValidIndex(Index))
{
return Waypoints[Index];
}
return nullptr;
}
인덱스를 받아와서 그 웨이포인트를 가져옴.
if문을 이용해서 유효한 범위인지 확인 => Waypoints[Index] 반환
int32 APatrolPath::Num() const
{
return Waypoints.Num();
}
웨이포인트 갯수 확인 ( 경로 이탈 방지 )