상세 컨텐츠

본문 제목

특별편: 언리얼 엔진에서 C++로 캐릭터 AI 만들기

C++

by zmo 2024. 10. 19. 18:33

본문

들어가기에 앞서 우리가 알고가야 하는 것이 두가지 정도 있다

 

첫째. 여기서 말하는 AI란 무엇일까?

우리가 만들 캐릭터 AI란 머신러닝이나 딥러닝을 통해 학습하는 AI가 아니라 플레이어가 조작하지 않아도 최적의 경로를 찾아 이동하거나 특정 상황에 특정 행동을 취하는 NPC 같은 존재들(리그오브레전드라는 게임의 미니언 같은 존재)을 뜻한다

 

둘째. 그러면 파이썬으로 그냥 AI를 작성하면 안되는걸까? 

파이썬은 데이터 처리 위주의 머신러닝과 딥러닝등의 학습을 통해 결과를 도출하는 것에 특화되어 있다 하지만 언리얼 엔진의 주로 게임에서 작동해야 하는 AI는 성장형 컴퓨터가 아닌 고성능의 빠른 실시간 처리가 필요하기 때문에 C++을 통해서 최적화 시켜주는 것이다

 

우리는 오늘 언리얼 엔진에서 제공하는 몇가지 C++ AI 라이브러리에 대해 알아보고 이중 한가지를 골라서 직접 사용해보도록 하자

여담으로 오늘은 "캐릭터 AI"와 "AI 캐릭터"라는 말이 등장하는데 "캐릭터 AI"는 캐릭터의 움직임을 조작하는 정신과 같은 것이고 "AI 캐릭터"는 동네 NPC를 뜻하는 거라고 생각하자

 

 

오늘 알아보기

1. AI 캐릭터를 제어하는 핵심 클래스 AIController 알아보기

2. AI의 상호작용 (물리적인 행동)

3. Behavior Tree 노드 확장하기

4. EQS확장하기(환경을 분석하고 최적의 위치 선택하기)

5. NavMesh로 경로탐색 알고리즘 커스터마이즈하기(특정 장애물 처리하기)

6. Behavior Tree를 사용한 캐릭터 만들기

 

 


 

 

AI 캐릭터를 제어하는 핵심 클래스 AIController 알아보기

캐릭터를 제어하는 Controller 는 두가지로 나눌수 있는데 하나는 플레이어의 컨트롤을 담당하는 PlayerController 과 AI의 행동을 담당하는 AIController 로 나뉜다 그중 우리는 AI캐릭터를 제어하는 AIController를 사용해보도록 하자

다음은 AIController를 상속받은 어떠한 객체가 TargetActor라는 목표를 향해 이동하도록 하는 코드이다

void AMyAIController::BeginPlay()
{
    Super::BeginPlay();
    MoveToActor(TargetActor); // AI가 타겟을 따라가도록 명령하기
}

- BeginPlay : 게임이 시작되거나 객체가 월드에 스폰되었을때 이 코드가 실행되도록 도와준다

- MoveToActor(TargetActor); : AIController 클레스에서 제공하는 MoveToActor(목표를 향해 이동해!)함수를 이용하여 TargetActor이라는 목표를 향해 이동시키는 코드이다

 

 

AI의 상호작용 (물리적인 행동)

이 코드는 Pawn 클래스를 사용하여 특정 위치로 이동하도록 제어하는 코드이다

GetController는 이 Pawn을 제어하는 Controller 객체(AIController)를 가져오는 함수인데 AI Pawn은 보통 AIController에 의해 제어되므로, 이 코드를 통해 위에서 알아본 AIController가 Pawn을 제어하게 된다

// Pawn 클래스에서 이동을 제어
void AMyAIPawn::MoveToLocation(FVector Destination)
{
    GetController()->MoveToLocation(Destination);
}

- void AMyAIPawn::MoveToLocation(FVector Destination) : 이 함수는 AI Pawn이 특정 위치로 이동하도록 설정하는 메서드이다 여기서 FVector Destination은 이동할 목표 위치를 나타내는 벡터 값이므로 100.0f, 200.0f, 300.0f 와 같은 벡터 값을 넣어주기로 하자

- MoveToLocation(Destination) : Controller에서 제공되는 함수로, 지정된 Destination 좌표로 AI가 이동하도록 명령한다 이 명령을 실행하면 AI가 자동으로 해당 좌표로 경로를 계산하고 이동한다

 

 

Behavior Tree 노드 확장하기

AI의 행동을 제어하는 Behavior Tree 노드 중 하나인 UBTTaskNode를 상속받아 UMyCustomTask라는 커스텀 클래스를 만들어본 코드이다

우리가 블루프린트를 통해 쉽게 추가하는 기능들에는 조금더 효율적이고 상황에 맞게 기능을 바꾸고 싶을때가 있을 것이다 언리얼 엔진에서는 지형이나 여러가지 변수 때문에 커스텀 클래스가 필요할 때가 올것이다 그 때 사용하는 방식이라 보면 될 것이다

UCLASS()
class MYGAME_API UMyCustomTask : public UBTTaskNode
{
    GENERATED_BODY()

protected:
    virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override
    {
        // 커스텀 AI 동작하기
        return EBTNodeResult::Succeeded;
    }
};

 - UCLASS() : 언리얼 엔진에서 이 클래스를 UCLASS 매크로로 선언하여 이 클래스를 엔진에서 사용할 수 있게 해준다 여기서는 UMyCustomTask가 Behavior Tree의 한 노드로 사용하게 한다

- GENERATED_BODY() : 이 매크로는 클래스에 필요한 메타데이터와 필수 코드를 자동으로 생성해 준다 모든 언리얼 클래스에 들어가는 코드이다

- ExecuteTask : AI가 작업을 수행할 때 호출되어 이를 오버라이드하면 커스텀 동작을 구현할수 있다

 

 

EQS확장하기(환경을 분석하고 최적의 위치 선택하기)

이 코드는 EQS(Environment Query System)를 사용한. 즉 AI가 환경에 대해 정보를 수집하고 그 결과를 처리하는 구조를 가지고 있는 코드이다

void AMyAIController::RunEQSQuery()를 통해 EQS 쿼리를 실행시켜서 주변환경을 분석하고 행동할수 있게 해준다 예를 들어 앞에 적을 탐색하고 공격하거나 벽을 넘는 등의 행동을 지정해줄수 있다

void AMyAIController::RunEQSQuery()
{
    FEnvQueryRequest QueryRequest(MyQuery, this);
    QueryRequest.Execute(EEnvQueryRunMode::AllMatching, this, &AMyAIController::OnQueryFinished);
}

void AMyAIController::OnQueryFinished(TSharedPtr<FEnvQueryResult> Result)
{
    // 쿼리 결과 처리하기
}

- EEnvQueryRunMode::AllMatching : 은 쿼리 실행 모드를 지정하며, 여기서는 조건에 맞는 모든 결과를 반환하도록 해준다

- OnQueryFinished : 쿼리가 끝났을 때 호출될 콜백 함수를 넣어주자

- void AMyAIController::OnQueryFinished(TSharedPtr<FEnvQueryResult> Result) : Result 는 쿼리의 결과를 저장하는 객체이다 이 결과는 쿼리가 끝나고 다양한 데이터를 포함하고, 그 데이터에 기반하여 AI가 어떻게 행동할지 결정한다

 

 

 

NavMesh로 경로탐색 알고리즘 커스터마이즈하기(특정 장애물 처리하기)

이 코드는 UNavigationSystemV1를 통해 내비게이션 시스템을 사용하고, AI가 목표 위치로 이동하게 하는 함수이다

이 내비게이션이 작동하기 위해서는 언리얼 엔진에서 내비게이션 메시 바운드 볼륨을 통해 AI가 이동할 수 있는 공간을 미리 설정해두자 그리고 GetCurrent(GetWorld())라는 함수를 사용하면 현재 월드에서 내비게이션 시스템 인스턴스를 가져와  NavSys라는 포인터에 저장해 사용할수 있게 해준다

UNavigationSystemV1* NavSys = UNavigationSystemV1::GetCurrent(GetWorld());
NavSys->SimpleMoveToLocation(MyController, Destination);

 

 

Behavior Tree를 사용한 캐릭터 만들기

여기서는 Behavior Tree를 사용한 캐릭터인 “AiCharacter”을 만들어본 코드이다(가능하면 “AiCharacter”같은 기본적인 이름은 삼가하도록 하자)

 

먼저 ACharacter 클래스를 상속받아 AI 캐릭터의 기능을 구현할수 있게 해보자

// AiCharacter.h 
// .h는 헤더파일이라는 뜻이다

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "AiCharacter.generated.h"

UCLASS()
class MYGAME_API AAiCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    // 기본값 설정하기
    AAiCharacter();

protected:
    // 게임 시작이나 스폰될때 호출
    virtual void BeginPlay() override;

public:	
    // 모든 프레임에 호출함
    virtual void Tick(float DeltaTime) override;

    // AIController를 사용해 Behavior Tree를 실행할 함수
    void StartAI();

private:
    // Behavior Tree 설정
    UPROPERTY(EditAnywhere, Category = "AI")
    class UBehaviorTree* BehaviorTree;
};

 

Behavior Tree를 사용해 AI의 행동을 설정하자

// AiCharacter.cpp
// .cpp은 소스 파일이다

#include "AiCharacter.h"
#include "AIController.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Kismet/GameplayStatics.h"

AAiCharacter::AAiCharacter()
{
    // AIController를 사용하여 AI 행동을 제어
    AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
}

void AAiCharacter::BeginPlay()
{
    Super::BeginPlay();

    // AI 행동 트리 시작
    StartAI();
}

void AAiCharacter::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
}

void AAiCharacter::StartAI()
{
    // AIController를 가져와서 Behavior Tree를 실행
    AAIController* AIController = Cast<AAIController>(GetController());
    if (AIController && BehaviorTree)
    {
        AIController->RunBehaviorTree(BehaviorTree);
    }
}

 

AiController.h은 AAIController클래스에서 Behavior Tree가 실행될수 있도록 구성해주자

// AiController.h

#pragma once

#include "CoreMinimal.h"
#include "AIController.h"
#include "AiController.generated.h"

UCLASS()
class MYGAME_API AAiController : public AAIController
{
    GENERATED_BODY()

protected:
    virtual void BeginPlay() override;

public:
    // AI가 특정 목표로 이동하도록 명령
    void MoveToTarget(AActor* TargetActor);
};
// AiController.cpp

#include "AiController.h"
#include "Kismet/GameplayStatics.h"
#include "BehaviorTree/BlackboardComponent.h"

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

void AAiController::MoveToTarget(AActor* TargetActor)
{
    // 목표 지점으로 이동
    if (TargetActor)
    {
        MoveToActor(TargetActor);
    }
}

 

 

 


 

 

이렇게 코드를 직접 작성하지 않아도 블루프린트를 이용해 기본적인 AI를 구현할수 있지만 특정 구역이나 특정 레벨에서 여러 기믹을 사용했을때 커스터마이징 하는 방법을 알아놓으면 꽤 도움이 될것 같다 AI 캐릭터들이 튀어나온 돌에 끼어있는 것도 방지할수 있고 내 게임의 방향성에 맞는 특별한 움직임을 구현할수 있다

또한 AI가 여러 시스템과 상호작용이 필요할 때(물리시스템+애니메이션+실시간 네트워킹 등과 같이 복잡한 상황) 통합시켜서 관리할수 있는 이점도 있다고 한다

 

같은 이동이라도 여러가지 방법으로 이동을 지정할수 있어서 왜 그럴까 의문점도 드는 시간이었다 언리얼 엔진의 AI에 대해서 더 자세히 알아보니 파이썬 AI를 사용하는 곳은 거의 모델링과 그 움직임을 학습시켜 캐릭터를 양산하는 데에 쓰이는 것 같다 지금까지 우리가 접할수 있었던 게임들에서 캐릭터 하나하나 디자이너가 한땀한땀 만들었다고 생각하니 그 시간과 노력을 많이 단축할수 있는 시대가 되었다는 생각이 들었다 

 

다음주는 무엇을 공부할까? 생각해보도록 하자..

'C++' 카테고리의 다른 글

C++ 예외처리  (2) 2024.09.28
C++ 람다 표현식 사용하기(2)  (1) 2024.09.07
C++ 람다 표현식 사용하기  (1) 2024.09.01
언리얼 엔진과 C++의 연산자 오버로딩  (0) 2024.08.17
C++이란 어떤 언어일까?  (1) 2024.08.11

관련글 더보기