[ Unreal Programming/ C++ #1 TPS-Projects ]

SeungWoo·2024년 9월 19일

AIController

AIController를 이용해서 AI 형태의 캐릭터를 만들수 있다

  • 가장 먼저 Character C++ 클래스를 만들겠습니다.

  • BehaviorTree AI를 사용하기 위해 헤더와 class를 선언한다

CEnemy.h

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BehaviorTree.h"
#include "GameFramework/Character.h"
#include "CEnemy.generated.h"



UCLASS()
class TEST_PROJECT_PLAYER_API ACEnemy : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties
	ACEnemy();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
	
	UBehaviorTree* GetBeHaviorTree() const;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
	class UBehaviorTree* Tree;
};

CEnemy.cpp

#include "CEnemy.h"

// Sets default values
ACEnemy::ACEnemy()
{
 	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	GetMesh()->SetRelativeLocation(FVector(0, 0, -90));
	GetMesh()->SetRelativeRotation(FRotator(0, -90, 0));

}

// Called when the game starts or when spawned
void ACEnemy::BeginPlay()
{
	Super::BeginPlay();
	
}

// Called every frame
void ACEnemy::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

// Called to bind functionality to input
void ACEnemy::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

}

UBehaviorTree* ACEnemy::GetBeHaviorTree() const
{
	return Tree;
}
  • 해당 로직을 사용하고 빌드 후 해당 AI 기능을 사용하기 위해 빌드 파일에 모듈을 추가해준다

~.Bulid.cs

using UnrealBuildTool;

public class TEST_Project_Player : ModuleRules
{
	public TEST_Project_Player(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
	
		PublicDependencyModuleNames.AddRange(new string[] 
		{ 
			"Core",
			"CoreUObject",
			"Engine",
			"InputCore",
			"EnhancedInput",
            "AIModule",
			"GameplayTasks",
			"NavigationSystem"
		});

		PrivateDependencyModuleNames.AddRange(new string[] {  });

		// Uncomment if you are using Slate UI
		// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
		
		// Uncomment if you are using online features
		// PrivateDependencyModuleNames.Add("OnlineSubsystem");

		// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
	}
}
  • AIController 형태에 C++ 클래스도 하나 만들어 제어를 해줄 예정이다

CAIControllerEnemy.h

#pragma once

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

/**
 * 
 */
UCLASS()
class TEST_PROJECT_PLAYER_API ACAIControllerEnemy : public AAIController
{
	GENERATED_BODY()
	
public:
	explicit ACAIControllerEnemy(FObjectInitializer const& objectInitializer);

protected:
	virtual void OnPossess(APawn* Inpawn) override;

};

CAIControllerEnemy.cpp

#include "CAIControllerEnemy.h"
#include "CEnemy.h"

ACAIControllerEnemy::ACAIControllerEnemy(FObjectInitializer const& objectInitializer)
{

}

void ACAIControllerEnemy::OnPossess(APawn* Inpawn)
{
	Super::OnPossess(Inpawn);

	if (ACEnemy* const enemy = Cast<ACEnemy>(Inpawn))
	{
		if (UBehaviorTree* const tree = enemy->GetBeHaviorTree())
		{
			UBlackboardComponent* b;
			UseBlackboard(tree->BlackboardAsset, b);
			Blackboard = b;
			RunBehaviorTree(tree);
		}
	}
}
  • 이 코드는 ACAIControllerEnemy라는 적의 AI 컨트롤러 클래스에 대한 정의입니다. ACAIControllerEnemy는 언리얼 엔진에서 AI 캐릭터를 제어하는 역할을 하는 AI 컨트롤러 클래스를 상속받은 형태

OnPossess 함수

void ACAIControllerEnemy::OnPossess(APawn* Inpawn)
{
    Super::OnPossess(Inpawn);
}
  • 설명 : OnPossess 함수는 AI 컨트롤러가 특정 Pawn(AI가 제어할 캐릭터 또는 객체)을 소유하게 될 때 호출됩니다. 즉, AI가 적 캐릭터를 제어하기 시작하는 시점입니다.
  • Super::OnPossess(Inpawn);는 부모 클래스(AController 또는 AAIController)의 OnPossess 함수를 호출하여 기본적인 처리 과정을 유지합니다.

ACEnemy로 캐스팅

if (ACEnemy* const enemy = Cast<ACEnemy>(Inpawn))
  • 설명 : 이 줄은 전달된 Inpawn이 ACEnemy 타입인지 확인하고, 만약 그렇다면 enemy 변수에 저장합니다. ACEnemy는 적 캐릭터 클래스입니다.
  • Cast는 안전하게 타입을 변환하는 함수로, 변환이 실패하면 nullptr를 반환합니다.
    이 조건이 충족되면 enemy는 ACEnemy 객체로 처리됩니다.

Behavior Tree와 Blackboard 설정

if (UBehaviorTree* const tree = enemy->GetBeHaviorTree())
{
    UBlackboardComponent* b;
    UseBlackboard(tree->BlackboardAsset, b);
    Blackboard = b;
    RunBehaviorTree(tree);
}
  • 설명 : 적 캐릭터(ACEnemy)에서 Behavior Tree를 가져옵니다. GetBeHaviorTree()는 ACEnemy 클래스에 정의된 함수로, 적이 사용할 Behavior Tree를 반환합니다.
  • Behavior Tree: AI 행동을 제어하는 트리 구조로, 캐릭터가 해야 할 작업을 정의합니다.
  • Blackboard: AI가 사용할 변수를 저장하는 데이터베이스 역할을 합니다. Behavior Tree는 이 Blackboard에 저장된 값을 기반으로 행동합니다.
    • UseBlackboard(tree->BlackboardAsset, b)는 Behavior Tree와 관련된 Blackboard를 설정하고, 설정된 Blackboard를 b 변수에 저장합니다.
    • Blackboard = b;는 AI 컨트롤러의 멤버 변수로 설정하여 이후에 접근할 수 있게 합니다.
  • RunBehaviorTree(tree): Behavior Tree를 실행하여 AI가 트리에서 정의된 행동을 시작하게 합니다.

Behavior를 통한 AI 임의 구역 이동

  • Behavior Tree와 Blackboard를 생성한다
    • Behavior Tree이름은 BT_Enemy
    • Blackboard의 이름은 BB_Enemy로 한다

  • 해당 Behavior Tree에 만든 Blackboard를 넣는다

  • Black board에서 vector키를 하나 만들어 놓고 TargetLocation이라고 이름을 짓는다.

  • NavMesh Volume를 만들고 p를 눌러서 범위와 AI가 이동 가능한 영역을 확인한다

  • UBTTask_BlackboardBase형태의 C++ 클래스를 하나 만든다

  • 이 코드는 BTTask_FindRandomLocation이라는 클래스를 구현한 것으로, 언리얼 엔진에서 AI 행동 트리(Task) 노드로 사용됩니다.

  • 이 클래스는 UBTTask_BlackboardBase를 상속받으며, AI가 네비게이션 메쉬(NavMesh) 내에서 무작위 위치를 찾아 블랙보드에 저장하는 역할

BTTask_FindRandomLocation.h

#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/Tasks/BTTask_BlackboardBase.h"
#include "BTTask_FindRandomLocation.generated.h"

/**
 * 
 */
UCLASS()
class TEST_PROJECT_PLAYER_API UBTTask_FindRandomLocation : public UBTTask_BlackboardBase
{
	GENERATED_BODY()
	
public:
	explicit UBTTask_FindRandomLocation(FObjectInitializer const& ObjectInitializer);

	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;


private:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI", meta = (AllowPrivateAccess="true"))
	float SearchRadius = 1500.0f;
};
  • 주요 설명
    • UBTTask_FindRandomLocation 클래스: UBTTask_BlackboardBase를 상속받아 블랙보드를 사용하여 작업을 처리하는 Behavior Tree의 Task를 구현한 클래스입니다.
    • SearchRadius 변수: AI가 랜덤 위치를 검색할 때 사용할 반경을 설정합니다. 기본값은 1500.0f입니다.
    • ExecuteTask 함수: 이 함수는 Behavior Tree에서 Task가 실행될 때 호출되며, AI가 특정 작업을 수행하는 실제 로직을 구현합니다. 여기서는 무작위 위치를 찾고 그 위치를 블랙보드에 저장하는 역할을 합니다.

BTTask_FindRandomLocation.cpp

#include "BTTask_FindRandomLocation.h"

#include "NavigationSystem.h"
#include "CAIControllerEnemy.h"
#include "BehaviorTree/BlackboardComponent.h"

UBTTask_FindRandomLocation::UBTTask_FindRandomLocation(FObjectInitializer const& ObjectInitializer)
{
	NodeName = "Find Random Location In NavMesh";
}

EBTNodeResult::Type UBTTask_FindRandomLocation::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	// get AI Controller and its enemy
	if (auto* const cont = Cast<ACAIControllerEnemy>(OwnerComp.GetAIOwner()))
	{
		if (auto* const enemy = cont->GetPawn())
		{
			// obtain enemy location to use as an origin
			auto const Origin = enemy->GetActorLocation();

			// get the navigation system and generate a random locaiton
			if (auto* const NavSys = UNavigationSystemV1::GetCurrent(GetWorld()))
			{
				FNavLocation Loc;
				if (NavSys->GetRandomPointInNavigableRadius(Origin, SearchRadius, Loc))
				{
					OwnerComp.GetBlackboardComponent()->SetValueAsVector(GetSelectedBlackboardKey(), Loc.Location);
				}

				FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
				return EBTNodeResult::Succeeded;
			}
		}
	}


	return Super::ExecuteTask(OwnerComp, NodeMemory);
}

UBTTask_FindRandomLocation 생성자

NodeName = "Find Random Location In NavMesh";
  • 생성자에서 Task의 이름을 "Find Random Location In NavMesh"로 설정합니다. 이 이름은 Behavior Tree 에디터에서 노드를 시각적으로 구분하는 데 사용됩니다.

  • ExecuteTask 함수
    Behavior Tree에서 이 Task가 실행될 때 호출되는 함수

EBTNodeResult::Type UBTTask_FindRandomLocation::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
  • AI 컨트롤러 캐스팅
    if (auto* const cont = Cast<ACAIControllerEnemy>(OwnerComp.GetAIOwner()))
    • OwnerComp에서 AI 컨트롤러를 가져와 ACAIControllerEnemy로 캐스팅합니다. OwnerComp.GetAIOwner()는 이 Task를 실행하는 AI 캐릭터의 컨트롤러를 반환합니다.
  • 적(Pawn) 가져오기
    if (auto* const enemy = cont->GetPawn())
    • AI 컨트롤러가 제어하는 Pawn(적 캐릭터)을 가져옵니다. 적 캐릭터의 위치가 무작위 위치 생성의 기준점이 됩니다.
  • 네비게이션 시스템에서 무작위 위치 생성
    if (auto* const NavSys = UNavigationSystemV1::GetCurrent(GetWorld()))
    • UNavigationSystemV1을 사용하여 AI가 이동할 수 있는 네비게이션 영역 내에서 무작위 위치를 생성합니다. GetRandomPointInNavigableRadius 함수를 사용해 기준점에서 SearchRadius 반경 내에서 무작위 위치를 얻습니다.
  • 블랙보드에 위치 저장
    OwnerComp.GetBlackboardComponent()->SetValueAsVector(GetSelectedBlackboardKey(), Loc.Location);
    • 생성된 무작위 위치(Loc.Location)를 블랙보드의 선택된 키에 저장합니다. 블랙보드는 AI가 상태나 데이터를 유지하는 데 사용됩니다.
  • 작업 완료 처리
    FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
    return EBTNodeResult::Succeeded;
     - 작업이 성공적으로 완료되었음을 알리고, EBTNodeResult::Succeeded를 반환하여 Behavior Tree에서 다음 노드로 이동하도록 합니다.


  • 다시 BT로 넘어가서 트리를 완성하고 해당 Detail창에 TargetLocation를 넣어준다

  • wait은 3.0 이 기존 default 값인데 쫌더 빠르게 하기 위해 1로 해줬다

  • BP_Enemy에 pawn 설정을 가서 만든 클래스를 끼워준다
  • BP_Enemy에 AI 설정을 가서 만든 클래스를 끼워준다

animation 적용

  • 기존 제공하는 만들어진 애니메이션을 넣고, 약간의 수정을 한다

  • 해당 블루프린트 노드를 추가한다
profile
This is my study archive

0개의 댓글