본문 바로가기

Unreal Engine 4/C++

<Unreal C++> 78 - Action RPG (Enemy Melee AI)

 

필요한 개념


필요한 작업 세팅

 

AIController 세팅 및 CBehaviorComponent 생성

AI는 근거리용(Melee) AI와 원거리(Wizard) AI로 나눠서 할 것임
AI 움직일라면 AI 컨트롤러 세팅

* CEnemy를 상속 받는 CEnemy_AI class 생성 및 세팅

private:
	// 수정가능하도록 EditDefaultsOnly
	UPROPERTY(EditDefaultsOnly, Category = "AI")
		class UBehaviorTree* BehaviorTree; // 운용할 BehaviorTree



UPROPERTY(EditDefaultsOnly, Category = "AI")
	uint8 TeamID = 1; // TeamID : 이후에 피아를 구분하기 위한 값



public:
	FORCEINLINE class UBehaviorTree* GetBehaviorTree() { return BehaviorTree; }
	FORCEINLINE uint8 GetTeamID() { return TeamID; }




* ActorComponent를 상속받는 CBehaviorComponent 생성
-> AI의 행동상태를 관리하고, Blackboard 컴포넌트의 값을 관리하는 용도로 사용

private:
	// 모든 AI가 블랙보드를 공유해서 쓸 것인데, 그때 블랙보드 키가 됨
	UPROPERTY(EditAnywhere)
		FName BehaviorKey = "Behavior"; // Behavior 블랙보드에 키를 넣을 것임

	UPROPERTY(EditAnywhere)
		FName PlayerKey = "Player"; // 플레이어 키



public:
	// 블루프린트에서 콜 할수 있도록, 리턴 받을 수 있도록
	UFUNCTION(BlueprintPure)
		bool IsWaitMode();

	UFUNCTION(BlueprintPure)
		bool IsApproachMode();

	UFUNCTION(BlueprintPure)
		bool IsActionMode();

	UFUNCTION(BlueprintPure)
		bool IsPatrolMode();

	UFUNCTION(BlueprintPure)
		bool IsHittedMode();

	UFUNCTION(BlueprintPure)
		bool IsAvoidMode();

	// 외부에서 블랙보드 세팅가능
	FORCEINLINE void SetBlackboard(class UBlackboardComponent* InBlackboard) { Blackboard = InBlackboard; }



public:
	UCBehaviorComponent();

public:
	void SetWaitMode();
	void SetApproachMode();
	void SetActionMode();
	void SetPatrolMode();
	void SetHittedMode();
	void SetAvoidMode();

	// 가지고 있는 타겟 리턴
	class ACPlayer* GetTargetPlayer();



protected:
	virtual void BeginPlay() override;

private:
	void ChangeType(EBehaviorType InType);
	EBehaviorType GetType(); // 현재 타입 리턴

public:
	// BP에서 할당 가능하도록
	UPROPERTY(BlueprintAssignable)
		FBehaviorTypeChanged OnBehaviorTypeChanged;

private:
	class UBlackboardComponent* Blackboard;




void UCBehaviorComponent::ChangeType(EBehaviorType InType)
{
	EBehaviorType type = GetType();
	// 블랙보드에 AI 상태 값 세팅
	// blackboard에서는 enum을 uint8로 받는다.
	Blackboard->SetValueAsEnum(BehaviorKey, (uint8)InType);

	if (OnBehaviorTypeChanged.IsBound())
		OnBehaviorTypeChanged.Broadcast(type, InType);
}

ACPlayer* UCBehaviorComponent::GetTargetPlayer()
{
	// 블랙보드에 캐릭터가 있었다면 그것을 리턴
	return Cast<ACPlayer>(Blackboard->GetValueAsObject(PlayerKey));
}

EBehaviorType UCBehaviorComponent::GetType()
{
	// 블랙보드에 세팅되어있는 키에 따른 enum 값(EBehaviorType)을 가져옴
	// uint8을 다시 EBehaviorType로 캐스팅함
	return (EBehaviorType)Blackboard->GetValueAsEnum(BehaviorKey);
}



* AIController를 상속받는 CAIController(AI 컨트롤러) 생성

private:
	UPROPERTY(EditAnywhere)
		float BehaviorRange = 150; // 공격범위

	UPROPERTY(EditAnywhere)
		bool bDrawDebug = true; // 디버그로 할지(디버그시에 구역을 보여줄 수 있도록 함)

	UPROPERTY(EditAnywhere)
		float AdjustCircleHeight = 50; // 디버깅용 원을 그릴때 높이를 조정할 값




private:
	UPROPERTY(VisibleDefaultsOnly)
		class UAIPerceptionComponent* Perception; // 감지용 컴포넌트

	UPROPERTY(VisibleDefaultsOnly)
		class UCBehaviorComponent* Behavior;

public:
	FORCEINLINE float GetBehaviorRange() { return MeleeActionRange; }

public:
	ACAIController();

	float GetSightRadius(); // 감지시야설정하면 시야를 리턴

	// Actor에 Tick()을 재정의
	virtual void Tick(float DeltaTime) override;

protected:
	virtual void BeginPlay() override;

	virtual void OnPossess(APawn* InPawn) override; // 빙의 되면 호출됨
	virtual void OnUnPossess() override; //빙의가 끝날때 호출됨



private:
	// 감지가 되면 델리게이트에 연결될 함수
	UFUNCTION()
		void OnPerceptionUpdated(const TArray<AActor*>& UpdateActors);

private:
	class ACEnemy_AI* OwnerEnemy; // 소유자
	// Perception 정의할때 시야로 감지하는데, 감지 정의하는 클래스
	class UAISenseConfig_Sight* Sight;

 

ACAIController::ACAIController()
{
	PrimaryActorTick.bCanEverTick = true; // 틱 동작시켜줌

	// AI 컨트롤러에는 이미 BlackBoard라는 변수가 정의되어 있다.
	CHelpers::CreateActorComponent<UBlackboardComponent>(this, &Blackboard, "Blackboard");
	CHelpers::CreateActorComponent<UAIPerceptionComponent>(this, &Perception, "Perception");
	CHelpers::CreateActorComponent<UCBehaviorComponent>(this, &Behavior, "Behavior");

	//AAIController::PostInitializeComponents() : 모든 컴포넌트 들이 초기화된 이후에 컴포넌트 값을 세팅해줄때
	// FindComponentByClass() : 기존 생성되어 할당된 것이 있다면 할당해줘라
	// 생성자가 PostInitializeComponents()보다 먼저 콜되서
	// NULL이 아니니까 패스함, NULL이면 기존 생성된 거 찾아서 있다면 할당해줌
	//void AAIController::PostInitializeComponents()
	//{
	// Super::PostInitializeComponents();
	//
	// if (bWantsPlayerState && !IsPendingKill() && (GetNetMode() != NM_Client))
	// {
	// InitPlayerState();
	// }
	//
	// if (BrainComponent == nullptr)
	// {
	// BrainComponent = FindComponentByClass<UBrainComponent>();
	// }
	// if (Blackboard == nullptr)
	// {
	// Blackboard = FindComponentByClass<UBlackboardComponent>();
	// }


	// CreateDefaultSubobject : 객체를 생성자에서 동적으로 할당하기 위해 사용
	// NewObject : BeginPlay 이후의 게임모드에서 동적할당 할때 사용
	Sight = CreateDefaultSubobject<UAISenseConfig_Sight>("Sight");
	Sight->SightRadius = 600; // 반경
	Sight->LoseSightRadius = 800; // 벗어났을때 잃게 되는 범위
	Sight->PeripheralVisionAngleDegrees = 90; // 시야각
	Sight->SetMaxAge(2); // 잃었을떄 얼마만큼 유지시킬 건지

	// DetectionByAffiliation : 감지를 할 관계를 설정
	Sight->DetectionByAffiliation.bDetectEnemies = true; // 적만 감지
	Sight->DetectionByAffiliation.bDetectNeutrals = false; // 중립은 감지 안함
	Sight->DetectionByAffiliation.bDetectFriendlies = false; // 아군은 감지 안함

	// 나중에 TeamID로 적인지 아군인지 판단

	Perception->ConfigureSense(*Sight);
	// SetDominantSense() : 만약 여러개의 감지를 넣는 다면, 어느 감지가 우선되는지
	Perception->SetDominantSense(*Sight->GetSenseImplementation());
}



float ACAIController::GetSightRadius()
{
	return Sight->SightRadius;
}

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



void ACAIController::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);

	// 빙의되었을때

	// InPawn은 빙의된 애여서 저장함
	OwnerEnemy = Cast<ACEnemy_AI>(InPawn);

	// SetGenericTeamId() : TeamID가 같으면 아군, 다르면 적군, 255이면 중립으로 간주하고 감지를 수행함, 단, 값이 0 이상이어야함
	SetGenericTeamId(OwnerEnemy->GetTeamID());

	Perception->OnPerceptionUpdated.AddDynamic(this, &ACAIController::OnPerceptionUpdated);

	// UseBlackboard() : BlackboardAsset을 BlackboardComponent에 등록해서 사용하겠단 의미
	// 비헤이비어트리안에 블랙보드를 등록하기 때문에
	// 어차피 비헤이비어 트리 안에 블랙보드가 존재한다.
	// AIController에 있는 Blackboard 변수에 할당함
	UseBlackboard(OwnerEnemy->GetBehaviorTree()->BlackboardAsset, Blackboard);

	// 우리가 정의한 UCBehaviorComponent에 Blackboard를 세팅해서 운용하도록 해줌
	Behavior->SetBlackboard(Blackboard);

	// BehaviorTree 실행시킴
	RunBehaviorTree(OwnerEnemy->GetBehaviorTree());
}




void ACAIController::OnUnPossess()
{
	Super::OnUnPossess();

	// 빙의 해제되었을때

	// 감지된거 다 날려줌(이벤트 클리어)
	Perception->OnPerceptionUpdated.Clear();
}

void ACAIController::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	// 디버깅 모드 일떄만 실행함
	CheckFalse(bDrawDebug);

	// 공격 범위나 범위를 그려줌
	FVector center = OwnerEnemy->GetActorLocation();
	center.Z -= AdjustCircleHeight; // 높이를 조정(정확히 일치하면 잘안보이는 경우가 있어서)

	// Segments : 정밀도
	// YAxis : FVector::RightVector, ZAxis : FVector::ForwardVector -> 원을 그릴 회전 방향
	// -> 즉, ZAxis 부터 YAxis까지 방향으로 그림(언리얼은 항상 시계방향으로 그려서)
	// 전방부터 오른쪽으로 그려지게 됨
	// center부터 감지 시야까지 그려줌
	DrawDebugCircle(GetWorld(), center, Sight->SightRadius, 300, FColor::Green, false, -1, 0, 0, FVector::RightVector, FVector::ForwardVector);

	// center 부터 BehaviorRange(공격범위) 까지 그려줌
	DrawDebugCircle(GetWorld(), center, BehaviorRange, 300, FColor::Red, false, -1, 0, 0, FVector::RightVector, FVector::ForwardVector);
}



void ACAIController::OnPerceptionUpdated(const TArray<AActor*>& UpdateActors)
{
	TArray<AActor*> actors;
	// Perception에 감지된 액터들을 받음(SenseToUse가 none이면 현재 어떤 식으로든 인식되는 모든 액터가 가져옴)
	Perception->GetCurrentlyPerceivedActors(NULL, actors);

	ACPlayer* player = NULL;
	for (AActor* actor : actors)
	{
		player = Cast<ACPlayer>(actor);

		// 플레이어가 있다면 감지되었다는 뜻
		if (!!player)
			break;
	}

	// blackboard에 Object로 Player 세팅해줌
	// 감지가 되었다면 player가 들어가고 안되었다면 NULL이 들어갈 것이다.
	Blackboard->SetValueAsObject("Player", player);
}





* CEnemy 수정(Action을 가져와서 TwoHandMode로 시작시 세팅하려고)

// 블루프린트에서 읽을 수 있도록
protected:
	UPROPERTY(BlueprintReadOnly, VisibleDefaultsOnly)
		class UCActionComponent* Action;

// 외부에서 접근 못하도록
private:
	UPROPERTY(VisibleDefaultsOnly)
		class UCMontagesComponent* Montages;

	UPROPERTY(VisibleDefaultsOnly)
		class UCStatusComponent* Status;

	UPROPERTY(VisibleDefaultsOnly)
		class UCStateComponent* State;





* CActionComponent 수정

 

UFUNCTION(BlueprintCallable)
	void SetUnarmedMode();

// BP에서 콜 할 수 있도록
UFUNCTION(BlueprintCallable)
	void SetTwoHandMode();



* CEnemy_AI 기반 BP 생성(BP_CEnemy_AI_Melee)

 

 

- Action을 가져와서 Beginplay()시에 TwoHandMode로 시작시 세팅

 

 

- 속성에 Pawn에 AI Controller Class에서 BP_AIController_Melee 지정해줌(그러면 빙의가 된다.)


- BehaviorTree 변수에 BehaviorTree 세팅해준다.

 


* CAIController 기반 BP 생성(BP_CAIController_Melee)
-> AIController에 BP_CEnemy_AI_Melee을 빙의시킬(Possess) 것이다.

* 인공지능->비헤이비어트리 BP 생성(BT_Melee 생성)

결과를 보면 Debug모드여서 영역이 잘 그려지는 것을 볼 수 있음

Tip)
' 키 할시 Debug로 볼 수 있음
Num에 있는 키 4번을 누르면 Perception 영역(감지 영역)이 보임



* Enemy가 플레이어를 감지를 못하고 있음
-> 플레이어에게 TeamID가 없어서 감지를 못하고 있었음(중립으로 판단), 플레이어에게 TeamID를 주면 됨


AIController는 SetGenericTeamId()함수가 있어서 TeamID를 지정해주면 되지만 플레이어는 그렇지 않음
AI가 아닌 TeamID를 정의할 때는 IGenericTeamAgentInterface를 상속받아야 한다.
(참고로 AIController는 IGenericTeamAgentInterface 이미 상속 되어있음)
-> 인터페이스는 다중 상속이 가능함(클래스는 다중상속이 안됨, 하나만 가능)


<Player TeamID 부여>

CPlayer가 IGenericTeamAgentInterface 상속 받으면 됨

...
#include "GenericTeamAgentInterface.h"
#include "CPlayer.generated.h"

UCLASS()
class UONLINE_04_ACTION_API ACPlayer : public ACharacter, public IICharacter, public IGenericTeamAgentInterface
{
	...
}


- 이제 IGenericTeamAgentInterface에 있는 것들을 재정의 하면 됨

// 아래 두개를 재정의 할 수 있음
virtual void SetGenericTeamId(const FGenericTeamId& TeamID) {}
virtual FGenericTeamId GetGenericTeamId() const { return FGenericTeamId::NoTeam; } // NoTeam : 255(중립)




CPlayer에서 재정의 하면 됨

// 어차피 enum은 byte형이어서 uint8로 처리해줘도 됨
// TeamID가 적이랑 다르면 적군이 된다.
UPROPERTY(EditDefaultsOnly)
	uint8 TeamID = 0;

 

virtual FGenericTeamId GetGenericTeamId() const override;



FGenericTeamId ACPlayer::GetGenericTeamId() const
{
	return FGenericTeamId(TeamID);
}



- 결과를 보면 Player가 적에게 감지되는 것을 볼 수 있다.


<Behavior Tree 서비스 할당하는 것을 C로 제작>
서비스나 이런거를 직접 상속받아서 구현할 것임
BP에서 할때는 BTService_BlueprintBase(블루프린트용)를 상속받아서 구현했었다.
이번에는 C에서 제작할 것이어서 BTService를 상속받는 CBTService_Melee를 생성

 

서비스(Service) : 컴포짓 노드에 분기가 실행되는 동안 정해진 빈도에 맞춰서 실행된다. 보통 검사를 하고 그 검사를 바탕으로 블랙보드의 내용을 업데이트하는데 사용된다

* CBTService_Melee 정의
-> 상태에 따라 Behavior(행동)를 바꿔줄 것임
감지하면 쫓아올거고, 일정거리안에 들어오면 공격할거고, 순찰을 진행

public:
	UCBT_Service_Melee();

protected:
	// 매프레임마다 호출
	virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;

	// 이 노드에 들어올때
	// virtual void OnSearchStart(FBehaviorTreeSearchData& SearchData);




UCBT_Service_Melee::UCBT_Service_Melee()
{
	// NodeName에 이름을 세팅해주면 BehaviorTree에 이름이 나타남
	NodeName = "Melee";
}





* 각 상황에 따라 상태 세팅
이전에는 EQS가지고 주변을 배회했었는데
이번에는 주변 배회하는 거 빼고, EQS는 원거리 공격때 사용

void UCBT_Service_Melee::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

	//BehaviorTreeComponent에 있는 GetOwner를 해주면 됨
	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	// AIController도 Actor로 부터 상속받아서 GetComponent가능
	UCBehaviorComponent* behavior = CHelpers::GetComponent<UCBehaviorComponent>(controller);

	// 빙의되어있는 Pawn가져오면 됨
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
	UCStateComponent* state = CHelpers::GetComponent<UCStateComponent>(ai);

	// 내가 피격상태라면
	if (state->IsHittedMode())
	{
		behavior->SetHittedMode();

		return;
	}

	// <각 상황에 따른 상태 세팅>

	ACPlayer* target = behavior->GetTargetPlayer();
	// 타겟이 없다면 wait모드
	if (target == NULL)
	{
		//TODO : 패트롤 모드

		behavior->SetWaitMode();

		return;
	}
	else
	{
		UCStateComponent* targetState = CHelpers::GetComponent<UCStateComponent>(target);
		// 타겟이 있는데 죽은 상태라면
		if (targetState->IsDeadMode())
		{
			behavior->SetWaitMode();

			return;
		}
	}

	// 공격 가능한 거리에 있는지
	// ai와 target의 거리를 구함
	float distance = ai->GetDistanceTo(target);
	if (distance < controller->GetBehaviorRange())
	{
		// 공격
		behavior->SetActionMode();

		return;
	}

	// 감지 범위 안에 있다면
	if (distance < controller->GetSightRadius())
	{
		// 추적모드로 변경
		behavior->SetApproachMode();

		return;
	}
}



* 컴파일 해서 보면 링크 에러 LNK2001이 나옴
-> 라이브러리가 링크되지 않았을 때의 에러코드
언리얼도 기본적으로 쓰는 모듈외에 다른 모듈은 링크를 시켜야함
IGameplayTaskOwnerInterface 알지 못하는 에러가 나오는데
IGameplayTaskOwnerInterface를 구글링 해보면
Module이 GameplayTasks라고 나온다.

이걸 그대로 복사해서 프로젝트Bulid.cs 파일로 가서

using UnrealBuildTool;

public class UOnline_04_Action : ModuleRules
{
	public UOnline_04_Action(ReadOnlyTargetRules Target) : base(Target)
	{
		PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

		// 여기에 모듈명을 추가시켜주면 된다.(추가할 모듈)
		PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "GameplayTasks" });
		PrivateDependencyModuleNames.AddRange(new string[] { });

		// ModuleDirectiory 디렉터리는 Sources 폴더 밑 프로젝트 폴더
		// 이 폴더를 기준으로 헤더를 불러들일 수 있도록 세팅
		PublicIncludePaths.Add(ModuleDirectory);
	}
}



-> 이렇게 하면 컴파일이 정상적으로 됨


* BT_Melee(Behavior Tree)로 가서

Selector(셀럭터(컴포짓))에 우클릭해서 서비스 추가해서 우리가 만든 Melee 추가하면 된다.

 

 

서비스 클릭해서 디테일 보면
Interval(간격) : 0.25 -> 0.25초마다 실행
Random Devivation(틱 간격에 랜덤범위를 적용) : 0.0(사용하지 않음)
NodeName : 우리가 넣었던 이름(Melee)

 




상태에 따라 속도를 조절할 것이다.
순찰 상태 속도, 추적 속도 등을 다르게 줌
그런걸 세팅할 수 있도록 Task(SetSpeed)를 하나더 만듬

* 우선 CStatusComponent에 속도 관련 열거체 만듬

UENUM(BlueprintType)
enum class ECharacterSpeed : uint8
{
	Walk, Run, Sprint, Max,
};
// Sprint : 완전빠르게 달릴때

// Speed관련 하나의 배열로 통합



// 에디터에서 속도 조정 가능
UPROPERTY(EditDefaultsOnly, Category = "Speed")
	float Speed[(int32)ECharacterSpeed::Max] = { 200, 400, 600 };

FORCEINLINE float GetWalkSpeed() { return Speed[(int32)ECharacterSpeed::Walk]; }
FORCEINLINE float GetRunSpeed() { return Speed[(int32)ECharacterSpeed::Run]; }
FORCEINLINE float GetSprintSpeed() { return Speed[(int32)ECharacterSpeed::Sprint]; }



// MaxWalkSpeed에 해당 Speed모드의 값으로 세팅
void SetSpeed(ECharacterSpeed InType);
void UCStatusComponent::SetSpeed(ECharacterSpeed InType)
{
	UCharacterMovementComponent* movment = CHelpers::GetComponent<UCharacterMovementComponent>(GetOwner());

	movment->MaxWalkSpeed = Speed[(int32)InType];
}




* BTTaskNode를 상속받는 CBTTaskNode_SetSpeed 클래스 생성
Tip) BTTaskNode_Blueprint(블루프린트 전용)을 제외한 나머지 애들은 그대로 상속 받아서 사용해도 됨

- UBTTaskNode의 함수 (부모클래스 virtual 함수) 분석

// Task가 실행될때 
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory);

// Task가 중지될때
virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory);

// Task 중일때
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds);

// Task가 완료되었을때
virtual void OnTaskFinished(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult);



* UBTTaskNode클래스 부모 UBTNode클래스에 virtual 함수 보면

// GameplayTask가 초기화를 마친 후 호출됨
virtual void OnGameplayTaskInitialized(UGameplayTask& Task) override;



* CBTTaskNode_SetSpeed 추가


private:
	UPROPERTY(EditAnywhere)
		ECharacterSpeed SpeedType;

public:
	UCBTTaskNode_SetSpeed();

protected:
	// Task가 실행될때 
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;



UCBTTaskNode_SetSpeed::UCBTTaskNode_SetSpeed()
{
	NodeName = "Speed";
}



EBTNodeResult::Type UCBTTaskNode_SetSpeed::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	Super::ExecuteTask(OwnerComp, NodeMemory);

	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	// 컨트롤러에서 해당 컴포넌트 가져옴
	UCBehaviorComponent* behavior = CHelpers::GetComponent<UCBehaviorComponent>(controller);

	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
	UCStatusComponent* status = CHelpers::GetComponent<UCStatusComponent>(ai);

	// 해당 스피드타입에 맞도록 캐릭터의 MaxWalkSpeed 세팅
	status->SetSpeed(SpeedType);

	// Task는 종료가 중요(종료안하면 대기상태임)
	// EBTNodeResult
	// Succeeded, Faild, InProgress(현재 작업이 완료되지 않은 상태)가 있음
	// 리턴을 안하면 InProgress 상태가 됨

	// BP에서는 InProgress가 따로 없음, 상태를 안주면 InProgress 상태를 유지함
	// C에서는 InProgress로 해서 공격이 완료되었을 때 Success를 시키거나 해야함 
	return EBTNodeResult::Succeeded;
}



Enemy에도 칼을 장착하도록 함, 칼을 장착하게 되면 Asset자체가 메모리 공유가 일어남
적하고, 플레이어에 모두 칼이 장착되버림, 그래서 서로간의 메모리 공유가 일어나서 발생
-> 메모리 공유를 사본으로 만들어서 해결함

* BB_Enemy(블랙보드 기반 BP 생성)
어차피 Enemy는 이 하나로 통합해서 사용할 것이다.

 

* BB_Enemy



새키 Key Type : Object, Base Class : Character형(AC_Player로 해도됨), Entry Name : Player으로 생성
새키 Key Type : Enum, EnumName : EBehaviorType, Entry Name : Behavior

 

* Enum Name이 일치할 경우만 Enum Type에 등록되어 Behavior Tree에서 사용할 수 있다.
-> 자동으로 Enum Name만 넣으면 찾아주는데 Enum Type에 뜨면 찾은것이고, 없음이라고 나오면 못찾은 것이다.

 

 


* BT_Melee에 루트에 Backboard(BB_Enemy) 연결

 


<상태에 따라 세팅>

* BT_Melee(BehaviorTree)에 데코레이션 Blackboard 추가

 

 


* Wait 상태
관찰자 중단 : Self(자기 자신만 취소)
NodeName : Wait
Key Query : Is Equal To(같은지)
Key Value : Wait
Blackboard Key : Behavior

* Approach(추적) 상태
컴포짓을 Sequence 추가하고 데코레이션 Blackboard 추가
비슷하게 NodeName : Approach를 만들어주고(Key Value : Approach)
SetSpeed(우리가 만든 Task)를 노드 추가 및 Approach로 세팅
MoveTo 테스크 추가해서 Player(Blackboard에 있는 Target)으로 이동


* 맵에 액터 배치 -> NavMeshBoundsVolume 추가

 

 

위치(0, 0, 0), 디테일에 Brush Settings(X, Y, Z) 조절 하면 사이즈가 조절된다.
P키를 누르면 적용된 공간이 초록색으로 보임

<같은 에셋 참조해서 메모리 공유 발생, 사본을 만들어줌>

* BP_CEnemy_AI_Melee에서 BeginPlay()시에 SetTwoHandMode() 했는데, 칼이 등장하지 않는 이유
-> BP_CEnemy_AI_Melee ActionComponent에 각 액션에 DataAsset을 추가한다.(지금은 Unarmed, TwoHand만 세팅)

* 그런데 결과를 보면 Enemy가 아닌 플레이어가 무기를 장착하고 있음(문제 발생)
언리얼에서는 메모리를 최소화하기 위해 같은 Asset을 참조한다면 메모리 공유가 일어나는 방식으로 설계되어 있다.
-> 플레이어, Enemy의 ActionComponent에서 DA_TwoHand 같은 에셋을 참조하고 있다.
(둘 다 같은 에셋을 참조하고 있어서 플레이어의 DA_TwoHand와 적의 DA_TwoHand는 메모리 공유가 일어남, 둘다 장착은 되었지만, 어느놈것이 장착이 될지 모름)
-> BP_CEnemy_AI_Melee에서 BeginPlay()시에 SetTwoHandMode() 한것 내부로 들어가서 보니까 DA_TwoHand를 가져와서 사용하는데, 이 에셋이 메모리 공유가 되어있음

CActionData에서 BeginPlay()할때 Attachment를 스폰하고 하는데, 이것조차 메모리 공유가 일어나게 됨
 
아이템같은 경우 다른애들이 공유가 되면 안됨(적과 플레이어가 가진 아이템이 다르지만, 플레이어가 아이템을 강화하면, 적의 아이템과 강화되면 안된다.)

 

* 에셋을 가져다 쓰면 메모리 공유가 일어남
-> 그래서 사본을 만든다.
원본 DataAsset을 불러온 후 사본 DataAsset을 만들어 서로 간에 메모리 공유가 일어나지 않도록 처리해 줌

 

* UObject를 상속받는 CAction 클래스를 만듬
CActionData가 원본이 될거고, 이 클래스는 사본을 관리한다.
그래서 실제 데이터는 이제 CAction쪽에서 가져다 쓸 것임

CActionData에 있는 구조체들(FEquipmentData, FDoActionData) 전부 이쪽으로 넘김(실제 데이터는 여기서 사용할 것이다.)
사본을 만들 데이터들도 옮겨준다.


public:
	// UCActionData에서 여기 private 값을 세팅하도록 접근 허용(UActionData에서 이 클래스 객체를 생성 후에 값들을 초기화해줄 것이어서)
	friend class UCActionData;
public:
	FORCEINLINE class ACEquipment* GetEquipment() { return Equipment; }
	FORCEINLINE class ACDoAction* GetDoAction() { return DoAction; }
	FORCEINLINE class ACAttachment* GetAttachment() { return Attachment; }

	FORCEINLINE FLinearColor GetEquipmentColor() { return EquipmentColor; }

private:
	// 사본을 만들 데이터들
	class ACEquipment* Equipment;
	class ACAttachment* Attachment;
	class ACDoAction* DoAction;






- CActionData에서는 사본을 만들어서 리턴해줄 거기 때문에 BeginPlay인자로 UCAction을 추가

// 객체를 생성해서 값을 설정한 후 리턴해주기 위해 이차 포인터를 사용(1차 포인터로 쓰면 여기의 지역변수로 취급되어서 의미가 없음)
public:
	void BeginPlay(class ACharacter* InOwnerCharacter, class UCAction** OutAction);




// UCAction(사본) 생성 후 리턴
void UCActionData::BeginPlay(class ACharacter* InOwnerCharacter, class UCAction** OutAction)
{
	// 기존에는 SpawnActor를 사용했는데 SpawnActor는 등장과 동시에
	// BeginPlay()가 호출됨
	// 지금은 객체만 생성한 후 값을 세팅하고 등장시키는 방법을 사용하는데
	// 등장은 우리가 시키라고 할때 등장됨(즉, 객체 생성만 일어나고, 등장은 아직 하지 않음)
	// 이렇게 처리해주는 것이 SpawnActorDeffered

	FTransform transform;

	ACAttachment* attachment = NULL;

	if (!!AttachmentClass)
	{
		// 등장만 시켜놈
		attachment = InOwnerCharacter->GetWorld()->SpawnActorDeferred<ACAttachment>(AttachmentClass, transform, InOwnerCharacter);
		attachment->SetActorLabel(GetLableName(InOwnerCharacter, "Attachment"));
		// 어디에 붙일지는 CAttachment의 OnEquip()때 결정
		// Attachment->AttachToComponent(InOwnerCharacter->GetMesh(), FAttachmentTransformRules(EAttachmentRule::KeepRelative, true));
		UGameplayStatics::FinishSpawningActor(attachment, transform);
	}

	//  지역변수는 NULL 초기화가 자동으로 안되지만, UPROPERTY() 변수는 아무것도 안쓰면 자동 NULL초기화됨
	// NULL 초기화 해줘야 !!가 가능
	ACEquipment* equipment = NULL;

	if (!!EquipmentClass)
	{
		// 객체 생성만 완료됨, 등장은 안 시킴
		// 여기서 transform은 확정시키지 않으면 BeginPlay()가 끝나면 확정이 됨, 그때 등장할 위치
		equipment = InOwnerCharacter->GetWorld()->SpawnActorDeferred<ACEquipment>(EquipmentClass, transform, InOwnerCharacter);
		// mesh에다가 붙여넣음
		equipment->AttachToComponent(InOwnerCharacter->GetMesh(), FAttachmentTransformRules(EAttachmentRule::KeepRelative, true));
		// 할당 받은 데이터를 Equipment로 넘겨줌
		equipment->SetActorLabel(GetLableName(InOwnerCharacter, "Equipment"));
		equipment->SetData(EquipmentData);
		equipment->SetColor(EquipmentColor);

		// FinishSpawningActor() : 확정해 등장시킬 액터와 Transform을 입력
		// transform : 확정되서 등장시킬 위치
		UGameplayStatics::FinishSpawningActor(equipment, transform);

		if (!!attachment)
		{
			// 연결해줌
			equipment->OnEquipmentDelegate.AddDynamic(attachment, &ACAttachment::OnEquip);
			equipment->OnUnequipmentDelegate.AddDynamic(attachment, &ACAttachment::OnUnequip);
		}
	}

	ACDoAction* doAction = NULL;

	if(!!DoActionClass)
	{
		doAction = InOwnerCharacter->GetWorld()->SpawnActorDeferred<ACDoAction>(DoActionClass, transform, InOwnerCharacter);
		doAction->AttachToComponent(InOwnerCharacter->GetMesh(), FAttachmentTransformRules(EAttachmentRule::KeepRelative, true));
		doAction->SetActorLabel(GetLableName(InOwnerCharacter, "DoAction"));
		doAction->SetDatas(DoActionDatas);
		UGameplayStatics::FinishSpawningActor(doAction, transform);

		if (!!equipment)
		{
			doAction->SetEquipped(equipment->GetEquipped());
		}

		if (!!attachment)
		{
			// 충돌 함수 연결해줌
			attachment->OnAttachmentBeginOverlap.AddDynamic(doAction, &ACDoAction::OnAttachmentBeginOverlap);
			attachment->OnAttachmentEndOverlap.AddDynamic(doAction, &ACDoAction::OnAttachmentEndOverlap);

			attachment->OnAttachmentCollision.AddDynamic(doAction, &ACDoAction::OnAttahmentCollision);
			attachment->OffAttachmentCollision.AddDynamic(doAction, &ACDoAction::OffAttahmentCollision);
		}
	}

	// 객체 생성 후 값을 초기화
	*OutAction = NewObject<UCAction>();
	(*OutAction)->Attachment = attachment;
	(*OutAction)->Equipment = equipment;
	(*OutAction)->DoAction = doAction;
	(*OutAction)->EquipmentColor = EquipmentColor;
}


이전에는 맴버변수로 해서 값이 공유가 일어났지만, 여기에선 지역변수로 별도로 해서 객체마다 생성해서 리턴해줌

(즉, 사본생성해서, 메모리 공유가 일어나지 않음)


- UCActionData::BeginPlay(class ACharacter* InOwnerCharacter, class UCAction** OutAction)

함수를 콜하는 쪽은 CActionComponent다.

* CActionComponent 수정

private:
	UPROPERTY(EditDefaultsOnly, Category = "Weapons")
		class UCActionData* DataAssets[(int32)EActionType::Max];
public:
	UFUNCTION(BlueprintPure)
		FORCEINLINE class UCAction* GetCurrent() { return Datas[(int32)Type]; }

private:
	class UCAction* Datas[(int32)EActionType::Max]; // 사본

 

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

	ACharacter* character = Cast<ACharacter>(GetOwner());
	for (int32 i = 0; i < (int32)EActionType::Max; i++)
	{
		// 사본 생성
		if (!!DataAssets[i])
			DataAssets[i]->BeginPlay(character, &Datas[i]);
	}
}



정리하자면
- 에셋을 로딩하여 사용하면 에셋끼리의 메모리 공유가 일어남
- 데이터 에셋을 로딩한 후 사본을 만들어 메모리 공유를 해제함


* BP_CPlayer, BP_CEnemy_AI_Melee에서 ActionComponent의 Data Assets 변수를 다시 세팅해줌

(변수 이름이 바뀌어서 다시 초기화됨)

* ABP_CEnemy도 CPlayer처럼 각종 모드에 따른 StateMachine(스테이트 머신)을 세팅해줌

 


* 플레이해보면 아까같은 상황(플레이어가 시작하자마자, 무기 장착되는 상황)이 벌어지지 않음
-> 데이터공유가 더 이상 일어나지 않음




Tip) 클래스 생성했는데, 파일이 비주얼 스튜디오 상에서 안보이면


파일 -> Visual Studio 프로젝트 새로고침을 해준다.
만약 그랬는데도 안뜬다면
프로젝트 솔루션(.Sin) 파일을 지우고 새로 Generate(파일 -> Visual Stuido 프로젝트 생성)해준다.


<공격할 수 있도록 추가>
Enemy_AI가 Approach(접근)되었으면 Action(공격)에 들어가야 함
-> 액션을 만듬(Action(공격)을 관할하는 Task를 만듬)

* BTTaskNode 클래스를 상속받는 CBTTaskNode_Action 클래스 생성
이 클래스는 Action(공격)을 관할한다.
장착되어 있는 무기에 따라 공격을 플레이 해줌

private:
	// EditAnywhere 붙여줘야 밖에 테스트 할때 나타남
	UPROPERTY(EditAnywhere, Category = "AI")
		float Delay = 2.0f; // 공격 딜레이(공격하고 다시 공격 들어갈때 딜레이)

public:
	UCBTTaskNode_Action();

protected:
	// Task가 실행될때
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;

public:
	// Tick()이 있어야함
	// 공격이 완료된 상태에서 InProgress(Task 중인)상태를 Tick()에서 종료시켜줌
	virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;

private:
	float TotalTime = 0.0f; // 경과시간(일정 시간이 흐른 후 종료시키기 위함)




UCBTTaskNode_Action::UCBTTaskNode_Action()
{
	// true로 해주어야 Tick이 실행될 수 있음
	bNotifyTick = true;

	NodeName = "Action";
}



EBTNodeResult::Type UCBTTaskNode_Action::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	Super::ExecuteTask(OwnerComp, NodeMemory);

	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
	UCActionComponent* action = CHelpers::GetComponent<UCActionComponent>(ai);

	TotalTime = 0.0f;
	// 공격 실행됨
	action->DoAction();

	// InProgress(대기) 상태는 현재 태스크가 종료되지 않은 상태로 현재 태스크를 계속 실행하고 있는 상태가 됨
	return EBTNodeResult::InProgress;
}



void UCBTTaskNode_Action::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);

	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
	UCStateComponent* state = CHelpers::GetComponent<UCStateComponent>(ai);


	// 경과한 시간이 됨
	TotalTime += DeltaSeconds;

	// idle 모드로 바뀌었다면(Action이 끝났다면 Idle 상태가 됨)
	// 경과시간(TotalTime)이 Delay보다 크다면 태스크가 완료된(Finish) 상태임
	if (state->IsIdleMode() && TotalTime > Delay)
	{
		// FinishLatetAbort() : 중단으로 종료
		// FinishLatentTask() : 성공이나, 실패로 태스크 종료
		// Abort : 중단된 걸로 종료, FinishLatetAbort()로 종료시키는 것이 좋음
		// InProgress(대기)
		// Succeeded(완료)
		// 완료된 것으로 처리
		FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
	}
}



Tip) InProgress : 현재 태스크에서 완료되지 않은 상태로 머물게 만듬
BP에서는 없음

(완전히 Finish 되기 전까지 무조건 InProgress 단계이지만, C에서는 무조건 InProgress 상태를 만들어 줘야 한다.)

* BT_Melee(Behavior Tree) Action Task 추가, 데코레이션 Blackboard Action이랑 상태가 같다면 실행 
- BlackBoard 데코레이션 추가 할때, 관찰자 중단을 self로 해준다.
관찰자 중단 : self(자신 - 자신과 이 노드 아래 실행중인 서브 트리도 중단합니다)

 

 


* Observer Aborts (관찰자 중단)
None : 없음 - 아무것도 중단하지 않습니다.
Self : 자신 - 자신과 이 노드 아래 실행중인 서브 트리도 중단합니다.
Lower Priority : 하위 우선권 - 이 노드보다 오른쪽에 있는 노드를 중단합니다.
Both : 양쪽 - 자신, 그 아래 실행중인 서브 트리, 이 노드 오른쪽에 있는 노드를 중단합니다.


* 적이 너무 가까이 달라붙어서 공격한다.
BP_CAIController_Melee의 우리가 공개한 변수 Melee Action Range 값을 150으로 변경한다.

 



<Patrol Mode 생성>
* 패트롤 포인트를 관리할 CPatrolComponent 클래스 생성
패트롤 Path를 만들어 줘야 이 패트롤 Path를 가져다 관리할 수 있음

* Actor를 기반으로 하는 CPatrolPath 클래스 생성
스플라인(Spline)가지고 순찰 경로를 지정해줄 클래스


Construction Script(컨스트럭션 스크립트)
* 블루프린트 내의 각각의 인스턴스에 다양성을 줄 수 있는 스크립트.
- 게임 내 레벨을 구축하거나, 블루프린트 내의 프로퍼티를 업데이트 할 때 등등을 구현한다.
- 블루프린트 인스턴스 생성시 컴포넌트 리스트 다음에 실행되는 부분이다. 따라서 블루프린트 인스턴스에서 필요한 초기화 작업을 할 수 있다.
- 블루프린트와 관련된 가장 최신의 정보를 제공
- 같은 블루프린트로 만든 인스턴스라도 이 Construction Script 을 통해 인스턴스마다 개별적인 특성을 부여할 수 있다.
- 이 Construction Script 안에서 각각의 인스턴스마다 다르게 부여할 프로퍼티들을 public 변수로 에디터에서도 열고 에디터에서 각각의 인스턴스마다 이 변수 값들을 설정하게 한 후 이를 Setting 하는 작업을 한다.

블루프린트에서 수정하면 모든 인스턴스에도 수정 사항이 똑같이 반영되는데, 이렇게 Construction Script 를 활용하면 인스턴스 각각의 설정값을 다르게 해줄 수 있어 좋다!

BP에서는 내부적으로 OnConstruction(const FTransform& Transform)를 먼저 콜하고 BP의 Construction Script를 콜되고 있음
-> 즉, 우리는 OnConstruction(const FTransform& Transform)을 통해 C에서 Construction Script를 처리한다.

private:
	// 컨스트럭션 스크립트에서 다루기 위해서(C에서 컨스트럭션 스크립트를 다루는 방법이 있음)
	UPROPERTY(EditAnywhere, Category = "Loop")
		bool bLoop;

private:
	UPROPERTY(VisibleDefaultsOnly)
		class USceneComponent* Scene;

	UPROPERTY(VisibleDefaultsOnly)
		class USplineComponent* Spline;

	UPROPERTY(VisibleDefaultsOnly)
		class UTextRenderComponent* Text;



public:
	FORCEINLINE class USplineComponent* GetSpline() { return Spline; }

public:
	ACPatrolPath();

	// BP의 컨스트럭트 스크립트(Construction Script)와 동일한 함수
	// 월드에서 변수들 속성 수정하면 인스턴스 각각 적용해서 처리할 수 있음
	virtual void OnConstruction(const FTransform& Transform) override;

protected:
	virtual void BeginPlay() override;



ACPatrolPath::ACPatrolPath()
{
	CHelpers::CreateComponent<USceneComponent>(this, &Scene, "Scene");
	CHelpers::CreateComponent<USplineComponent>(this, &Spline, "Spline", Scene);
	CHelpers::CreateComponent<UTextRenderComponent>(this, &Text, "Text", Scene);

	// 땅에 뭍히지 않도록 위로 좀 올림
	Spline->SetRelativeLocation(FVector(0, 0, 30));

	Text->SetRelativeLocation(FVector(0, 0, 120));
	Text->SetRelativeRotation(FRotator(0, 180, 0));
	Text->HorizontalAlignment = EHorizTextAligment::EHTA_Center;
	Text->TextRenderColor = FColor::Red;
}



void ACPatrolPath::OnConstruction(const FTransform& Transform)
{
	// 배치 될때 이름 할당하기 위해
	Text->Text = FText::FromString(GetActorLabel());

	// 스플라인이 닫힌 루프인지 여부를 지정합니다. 루프 위치는 마지막 포인트의 입력 키 이후 1.0이 됩니다.
	Spline->SetClosedLoop(bLoop);
}

 

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

	Text->SetVisibility(false);
}



정리
- InProgress : 완료가 되지 않고 현재 태스크에서 머무는 상태
- TickTask()를 이용하여 완료 상태로 만들어줌

- OnConstruction()은 BP의 Constrction Script와 동일한 함수

* BP_CPatrolPath 생성

 

월드에 배치후 SplineComponent를 클릭해 각 포인트를 클릭한후 오른쪽 마우스 버튼 클릭하고 Spline 복제를 하고, 쭉 늘어트리면서 연결해준다.(회전도 주면 좋아짐)

 


그 후 마지막까지 연결이 되면 우리가 만든 bLoop를 체크하면 마지막 지점과 첫번쨰 지점이 자동으로 연결된다.
bLoop의 역할은 SplineComponent의 Closed Loop를 체크해주는 역할과 동일

(Closed Loop : 스플라인을 닫힌 루프로 간주할지)

 



* SplineComponent로 뱅글뱅글 돌 수 있도록 순찰경로를 월드에 만들기
-> 적 AI가 이 경로를 통해 순찰하게 된다.

* PatrolComponent는 이 순찰경로를 관리 한다.

private:
	UPROPERTY(EditAnywhere)
		class ACPatrolPath* Path;

	UPROPERTY(EditAnywhere)
		int32 Index; // 현재 몇번을 타고 있는지

	UPROPERTY(EditAnywhere)
		bool bReverse; // 뒤집어 졌는지(역으로 진행할건지)

	UPROPERTY(EditAnywhere)
		float AcceptanceRadius = 50; // 도달반경(얼마만큼 근방으로 갔을때 도달로 간주할지)



public:
	// 있는지
	FORCEINLINE bool IsValid() { return Path != NULL; }

	// 어디로 이동할지(어느위치로 이동할지, 거리가 얼마나 되는지) 
	bool GetMoveTo(FVector& OutLocation, float& OutAcceptanceRadius);

	// 도달했으면 다음으로 넘겨줘라
	void UpdateNextIndex();

public:
	UCPatrolComponent();

protected:
	virtual void BeginPlay() override;




bool UCPatrolComponent::GetMoveTo(FVector& OutLocation, float& OutAcceptanceRadius)
{
	OutLocation = FVector::ZeroVector;
	OutAcceptanceRadius = AcceptanceRadius;
	CheckNullResult(Path, false);

	// GetLocationAtSplinePoint() : 해당 포인트의 위치를 반환
	// 1. 포인트 인덱스, 2 : 어떤 좌표 방식으로 위치를 받을지(우리는 월드)
	OutLocation = Path->GetSpline()->GetLocationAtSplinePoint(Index, ESplineCoordinateSpace::World);

	return true;
}



void UCPatrolComponent::UpdateNextIndex()
{
	CheckNull(Path);

	// 포인트가 몇개인지 가져옴
	int32 count = Path->GetSpline()->GetNumberOfSplinePoints();

	// 역으로 가는 것 계산
	if (bReverse)
	{
		if (Index > 0)
		{
			Index--;

			return;
		}

		// 닫힌 루프라면
		if (Path->GetSpline()->IsClosedLoop())
		{
			Index = count - 1;

			return;
		}

		// 다 도달 했다면
		Index = 1;
		bReverse = false;

		return;
	}

	// 정방향이라면
	if (Index < count - 1)
	{
		Index++;

		return;
	}

	if (Path->GetSpline()->IsClosedLoop())
	{
		Index = 0;

		return;
	}

	// 0, 1, 2, 3, 4 갔다가 다시 3으로 돌아와야 하니까
	Index = count - 2;
	// 다시 뒤집어줌
	bReverse = true;
}



* Patrol(순찰)용 BTTaskNode를 상속받는 CBTTaskNode_Patrol 클래스를 만듬(Task)


public:
	UCBTTaskNode_Patrol();

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

public:
	virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;



UCBTTaskNode_Patrol::UCBTTaskNode_Patrol()
{
	bNotifyTick = true;

	NodeName = "Patrol";
}



EBTNodeResult::Type UCBTTaskNode_Patrol::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	Super::ExecuteTask(OwnerComp, NodeMemory);

	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
	UCPatrolComponent* patrol = CHelpers::GetComponent<UCPatrolComponent>(ai);

	FVector location; // 어느 위치로 이동할건지
	float acceptance; // 도달반경

	// 갈 수 없는 곳이라면 Failed 상태
	if (patrol->GetMoveTo(location, acceptance) == false)
		return EBTNodeResult::Failed;

	// 도달할때 까지가 InProgress이다.(InProgress면 반드시 완료를 시켜줘야 함)
	return EBTNodeResult::InProgress;
}



void UCBTTaskNode_Patrol::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);

	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
	UCPatrolComponent* patrol = CHelpers::GetComponent<UCPatrolComponent>(ai);

	FVector location; // 어느 위치로 이동할건지
	float acceptance; // 도달반경

	patrol->GetMoveTo(location, acceptance);
	// MoveToLocation() : AI Controller를 해당 위치로 이동시키는 함수
	// AIMoveTo 함수가 내부적으로 이 함수를 호출해 이동하게 된다.
	// 이 함수가 도달할수 있는지, 가고 있는지를 리턴해줌
	// 1. 어디로 갈지, 2. 얼마만큼 갔을때 도달로 간주할지, 3 : 두개가 겹쳤을때 정지할 건지, 4 bUsePathFinding : 우리는 네비게이션 메쉬사용하니까 기본적으로 true로 준다.
	// 기타 인자 bCanStrafe : 옆걸음질 칠건지
	EPathFollowingRequestResult::Type type = controller->MoveToLocation(location, acceptance, false);

	/*
	UENUM(BlueprintType)
	namespace EPathFollowingRequestResult
	{
		enum Type
		{
			Failed, // 갈 수 없는 위치
			AlreadyAtGoal, // 현재 도달했다면
			RequestSuccessful // 현재 완전히 정상적으로 종료가 되었다(갈 수 있는 위치인지를 확인할때 사용)
		};
	}
	*/

	if (type == EPathFollowingRequestResult::Failed)
	{
		// 갈 수 없다면 Failed로 해줌
		FinishLatentTask(OwnerComp, EBTNodeResult::Failed);

		return;
	}

	// 현재 도달했다면
	if (type == EPathFollowingRequestResult::AlreadyAtGoal)
	{
		FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);

		patrol->UpdateNextIndex();
	}
}



* CEnemy_AI에 CPatrolComponent 할당

private:
	UPROPERTY(VisibleDefaultsOnly)
		class UCPatrolComponent* Patrol;



CHelpers::CreateActorComponent<UCPatrolComponent>(this, &Patrol, "Patrol");




* BT_Melee(Behavior Tree) Patrol Task 추가, 데코레이션 Blackboard Patrol이랑 상태가 같다면 실행 
- BlackBoard 데코레이션 추가 할때, 관찰자 중단을 self로 해준다.
- 속도 조절을 위해 Sequence(실패하면 중지)를 사용하고, SetSpeed Task로 Enemy의 속도를 조절해준다.

*&nbsp;BT_Melee(Behavior&nbsp;Tree)



* 월드에 배치되어있는 BP_CEnemy_AI_Melee에 PatrolComponent에 Path에 월드에 있는 Path 액터를 넣어준다.


* CBT_Service에서 Patrol모드 세팅 추가

void UCBT_Service_Melee::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	...
	UCPatrolComponent* patrol = CHelpers::GetComponent<UCPatrolComponent>(ai);

	...
	if (target == NULL)
	{
		if (patrol != NULL && patrol->IsValid())
		{
			// patrol이 있고, Path가 Null이 아니라면
			behavior->SetPatrolMode();

			return;
		}

	...
	}
...
}




* 이동 지점에 도달했다면 잠시 대기를 주는 방법으로 작업해도 좋다.


<피격되었을 때 Hitted 모드 처리>

이미 Hitted 처리는 다 되어있어서, 우리는 Hitted시에 Wait하고 관찰자 중단을 Both로 실행중인거 모두 취소하도록 처리한다.(Wait 할동안 피격 애니메이션 플레이되고.. 잠깐 대기상태)

* BT_Melee(Behavior Tree) Wait Task 추가, 데코레이션 Blackboard Wait이랑 상태가 같다면 실행 
BlackBoard 데코레이션 추가 할때, 관찰자 중단을 Both로 해준다.

 

*&nbsp;BT_Melee(Behavior&nbsp;Tree)



Both : 양쪽 - 자신, 그 아래 실행중인 서브 트리, 이 노드 오른쪽에 있는 노드를 중단합니다.(모두 중단)

* BP_CEnemy_AI_Melee MontagesComponent에 DataTable에 Enemy 데이터 테이블 세팅(Hitted 몽타주 플레이 되기 위해서)

 


* 결과를 보면 정확히 피격이 들어감

 

 

 

 

CEnemy_AI.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "Characters/CEnemy.h"
#include "CEnemy_AI.generated.h"

UCLASS()
class UONLINE_04_ACTION_API ACEnemy_AI : public ACEnemy
{
	GENERATED_BODY()

private:
	// 수정가능하도록 EditDefaultsOnly
	UPROPERTY(EditDefaultsOnly, Category = "AI")
		class UBehaviorTree* BehaviorTree; // 운용할 BehaviorTree

	UPROPERTY(EditDefaultsOnly, Category = "AI")
		uint8 TeamID = 1; // TeamID : 이후에 피아를 구분하기 위한 값

private:
	UPROPERTY(VisibleDefaultsOnly)
		class UCPatrolComponent* Patrol;

public:
	FORCEINLINE class UBehaviorTree* GetBehaviorTree() { return BehaviorTree; }
	FORCEINLINE uint8 GetTeamID() { return TeamID; }

public:
	ACEnemy_AI();
	
};

 

 

 

 

 

 

 

CEnemy_AI.cpp


더보기
#include "CEnemy_AI.h"
#include "Global.h"
#include "Components/CPatrolComponent.h"

ACEnemy_AI::ACEnemy_AI()
{
	CHelpers::CreateActorComponent<UCPatrolComponent>(this, &Patrol, "Patrol");
}

 

 

CAIController.h


더보기
#pragma once

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

UCLASS()
class UONLINE_04_ACTION_API ACAIController : public AAIController
{
	GENERATED_BODY()

private:
	UPROPERTY(EditAnywhere)
		float BehaviorRange = 150; // 행동범위(액션을 하기 위한 범위)
	
	UPROPERTY(EditAnywhere)
		bool bDrawDebug = true; // 디버그로 할지(디버그시에 구역을 보여줄 수 있도록 함)

	UPROPERTY(EditAnywhere)
		float AdjustCircleHeight = 50; // 디버깅용 원을 그릴때 높이를 조정할 값 


private:
	UPROPERTY(VisibleDefaultsOnly)
		class UAIPerceptionComponent* Perception; // 감지용 컴포넌트
	
	UPROPERTY(VisibleDefaultsOnly)
		class UCBehaviorComponent* Behavior;

public:
	FORCEINLINE float GetBehaviorRange() { return BehaviorRange; }

public:
	ACAIController();

	float GetSightRadius(); // 감지시야설정하면 시야를 리턴

	// Actor에 Tick()을 재정의
	virtual void Tick(float DeltaTime) override;

protected:
	virtual void BeginPlay() override;

	virtual void OnPossess(APawn* InPawn) override; // 빙의 되면 호출됨
	virtual void OnUnPossess() override; //빙의가 끝날때 호출됨

private:
	// 감지가 되면 델리게이트에 연결될 함수
	UFUNCTION()
		void OnPerceptionUpdated(const TArray<AActor*>& UpdateActors);

private:
	class ACEnemy_AI* OwnerEnemy; // 소유자
	// Perception 정의할때 시야로 감지하는데, 감지 정의하는 클래스
	class UAISenseConfig_Sight* Sight;


};

 

 

 

 

 

 

 

CAIController.cpp


더보기
#include "CAIController.h"
#include "Global.h"
#include "CEnemy_AI.h"
#include "CPlayer.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "BehaviorTree/BehaviorTree.h"
#include "Components/CBehaviorComponent.h"
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig_Sight.h"


ACAIController::ACAIController()
{
	PrimaryActorTick.bCanEverTick = true; // 틱 동작시켜줌
	
	// AI 컨트롤러에는 이미 BlackBoard라는 변수가 정의되어 있다.
	CHelpers::CreateActorComponent<UBlackboardComponent>(this, &Blackboard, "Blackboard");
	CHelpers::CreateActorComponent<UAIPerceptionComponent>(this, &Perception, "Perception");
	CHelpers::CreateActorComponent<UCBehaviorComponent>(this, &Behavior, "Behavior");

	//AAIController::PostInitializeComponents() : 모든 컴포넌트 들이 초기화된 이후에 컴포넌트 값을 세팅해줄때
	// FindComponentByClass() : 기존 생성되어 할당된 것이 있다면 할당해줘라
	// 생성자가 PostInitializeComponents()보다 먼저 콜되서
	// NULL이 아니니까 패스함, NULL이면 기존 생성된 거 찾아서 있다면 할당해줌
	//void AAIController::PostInitializeComponents()
	//{
	//	Super::PostInitializeComponents();
	//
	//	if (bWantsPlayerState && !IsPendingKill() && (GetNetMode() != NM_Client))
	//	{
	//		InitPlayerState();
	//	}
	//
	//	if (BrainComponent == nullptr)
	//	{
	//		BrainComponent = FindComponentByClass<UBrainComponent>();
	//	}
	//	if (Blackboard == nullptr)
	//	{
	//		Blackboard = FindComponentByClass<UBlackboardComponent>();
	//	}


	// CreateDefaultSubobject : 객체를 생성자에서 동적으로 할당하기 위해 사용
	// NewObject : BeginPlay 이후의 게임모드에서 동적할당 할때 사용
	Sight = CreateDefaultSubobject<UAISenseConfig_Sight>("Sight");
	Sight->SightRadius = 600; // 반경
	Sight->LoseSightRadius = 800; // 벗어났을때 잃게 되는 범위
	Sight->PeripheralVisionAngleDegrees = 90; // 시야각
	Sight->SetMaxAge(2); // 잃었을떄 얼마만큼 유지시킬 건지
	
	// DetectionByAffiliation : 감지를 할 관계를 설정
	Sight->DetectionByAffiliation.bDetectEnemies = true; // 적만 감지
	Sight->DetectionByAffiliation.bDetectNeutrals = false; // 중립은 감지 안함
	Sight->DetectionByAffiliation.bDetectFriendlies = false; // 아군은 감지 안함

	// 나중에 TeamID로 적인지 아군인지 판단

	Perception->ConfigureSense(*Sight);
	// SetDominantSense() : 만약 여러개의 감지를 넣는 다면, 어느 감지가 우선되는지
	Perception->SetDominantSense(*Sight->GetSenseImplementation());
}

float ACAIController::GetSightRadius()
{
	return Sight->SightRadius;
}

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

void ACAIController::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);

	// 빙의되었을때

	// InPawn은 빙의된 애여서 저장함
	OwnerEnemy = Cast<ACEnemy_AI>(InPawn);

	// SetGenericTeamId() : TeamID가 같으면 아군, 다르면 적군, 255이면 중립으로 간주하고 감지를 수행함
	SetGenericTeamId(OwnerEnemy->GetTeamID());

	Perception->OnPerceptionUpdated.AddDynamic(this, &ACAIController::OnPerceptionUpdated);

	// UseBlackboard() : BlackboardAsset을 BlackboardComponent에 등록해서 사용하겠단 의미
	// 비헤이비어트리안에 블랙보드를 등록하기 때문에
	// 어차피 비헤이비어 트리 안에 블랙보드가 존재한다.
	// AIController에 있는 Blackboard 변수에 할당함
	UseBlackboard(OwnerEnemy->GetBehaviorTree()->BlackboardAsset, Blackboard);
	
	// 우리가 정의한 UCBehaviorComponent에 Blackboard를 세팅해서 운용하도록 해줌
	Behavior->SetBlackboard(Blackboard);

	// BehaviorTree 실행시킴
	RunBehaviorTree(OwnerEnemy->GetBehaviorTree());
}


void ACAIController::OnUnPossess()
{
	Super::OnUnPossess();

	// 빙의 해제되었을때

	// 감지된거 다 날려줌(이벤트 클리어)
	Perception->OnPerceptionUpdated.Clear();
}

void ACAIController::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	// 디버깅 모드 일떄만 실행함
	CheckFalse(bDrawDebug);

	// 공격 범위나 범위를 그려줌
	FVector center = OwnerEnemy->GetActorLocation();
	center.Z -= AdjustCircleHeight; // 높이를 조정(정확히 일치하면 잘안보이는 경우가 있어서)

	// Segments : 정밀도
	// YAxis : FVector::RightVector, ZAxis : FVector::ForwardVector -> 원을 그릴 회전 방향
	// -> 즉, ZAxis 부터 YAxis까지 방향으로 그림(언리얼은 항상 시계방향으로 그려서)
	// 전방부터 오른쪽으로 그려지게 됨
	// center부터 감지 시야까지 그려줌
	DrawDebugCircle(GetWorld(), center, Sight->SightRadius, 300, FColor::Green, false, -1, 0, 0, FVector::RightVector, FVector::ForwardVector);

	// center 부터 BehaviorRange(공격범위) 까지 그려줌
	DrawDebugCircle(GetWorld(), center, BehaviorRange, 300, FColor::Red, false, -1, 0, 0, FVector::RightVector, FVector::ForwardVector);
}

void ACAIController::OnPerceptionUpdated(const TArray<AActor*>& UpdateActors)
{
	TArray<AActor*> actors;
	// Perception에 감지된 액터들을 받음(SenseToUse가 none이면 현재 어떤 식으로든 인식되는 모든 액터가 가져옴)
	Perception->GetCurrentlyPerceivedActors(NULL, actors);

	ACPlayer* player = NULL;
	for (AActor* actor : actors)
	{
		player = Cast<ACPlayer>(actor);

		// 플레이어가 있다면 감지되었다는 뜻
		if (!!player)
			break;
	}
	
	// blackboard에 Object로 Player 세팅해줌
	// 감지가 되었다면 player가 들어가고 안되었다면 NULL이 들어갈 것이다.
	Blackboard->SetValueAsObject("Player", player);
}

 

 

CBehaviorComponent.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "CBehaviorComponent.generated.h"


UENUM(BlueprintType)
enum class EBehaviorType : uint8
{
	Wait, Approach, Action, Patrol, Hitted, Avoid,

};

// Wait : 대기 상태
// Approach : 추격 상태
// Action : 액션 상태
// Patrol : 순찰 상태
// Hitted : 공격 받은 상태
// Avoid : 회피 상태(원거리 공격 떄 나옴)


// 상태가 변경되었을 때, 델리게이트
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FBehaviorTypeChanged, EBehaviorType, InPrevType, EBehaviorType, InNewType);

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UONLINE_04_ACTION_API UCBehaviorComponent : public UActorComponent
{
	GENERATED_BODY()
		
private:
	// 모든 AI가 블랙보드를 공유해서 쓸 것인데, 그때 블랙보드 키가 됨
	UPROPERTY(EditAnywhere)
		FName BehaviorKey = "Behavior"; // Behavior 블랙보드에 키를 넣을 것임

	UPROPERTY(EditAnywhere)
		FName PlayerKey = "Player"; // 플레이어 키

public:
	// 블루프린트에서 콜 할수 있도록, 리턴 받을 수 있도록
	UFUNCTION(BlueprintPure)
		bool IsWaitMode();

	UFUNCTION(BlueprintPure)
		bool IsApproachMode();

	UFUNCTION(BlueprintPure)
		bool IsActionMode();

	UFUNCTION(BlueprintPure)
		bool IsPatrolMode();

	UFUNCTION(BlueprintPure)
		bool IsHittedMode();

	UFUNCTION(BlueprintPure)
		bool IsAvoidMode();

	// 외부에서 블랙보드 세팅가능
	FORCEINLINE void SetBlackboard(class UBlackboardComponent* InBlackboard) { Blackboard = InBlackboard; }

public:
	UCBehaviorComponent();

public:
	void SetWaitMode();
	void SetApproachMode();
	void SetActionMode();
	void SetPatrolMode();
	void SetHittedMode();
	void SetAvoidMode();


	// 가지고 있는 타겟 리턴
	class ACPlayer* GetTargetPlayer();

protected:
	virtual void BeginPlay() override;

private:
	void ChangeType(EBehaviorType InType);
	EBehaviorType GetType(); // 현재 타입 리턴

public:
	// BP에서 할당 가능하도록
	UPROPERTY(BlueprintAssignable)
		FBehaviorTypeChanged OnBehaviorTypeChanged;

private:
	class UBlackboardComponent* Blackboard;
};

 

 

 

 

 

 

 

CBehaviorComponent.cpp


더보기
#include "CBehaviorComponent.h"
#include "Global.h"
#include "Characters/CPlayer.h"
#include "BehaviorTree/BlackboardComponent.h"

bool UCBehaviorComponent::IsWaitMode()
{
	return GetType() == EBehaviorType::Wait;
}

bool UCBehaviorComponent::IsApproachMode()
{
	return GetType() == EBehaviorType::Approach;
}

bool UCBehaviorComponent::IsActionMode()
{
	return GetType() == EBehaviorType::Action;
}

bool UCBehaviorComponent::IsPatrolMode()
{
	return GetType() == EBehaviorType::Patrol;
}

bool UCBehaviorComponent::IsHittedMode()
{
	return GetType() == EBehaviorType::Hitted;
}

bool UCBehaviorComponent::IsAvoidMode()
{
	return GetType() == EBehaviorType::Avoid;
}

UCBehaviorComponent::UCBehaviorComponent()
{
	
}

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

}

void UCBehaviorComponent::SetWaitMode()
{
	ChangeType(EBehaviorType::Wait);
}

void UCBehaviorComponent::SetApproachMode()
{
	ChangeType(EBehaviorType::Approach);
}

void UCBehaviorComponent::SetActionMode()
{
	ChangeType(EBehaviorType::Action);
}

void UCBehaviorComponent::SetPatrolMode()
{
	ChangeType(EBehaviorType::Patrol);
}

void UCBehaviorComponent::SetHittedMode()
{
	ChangeType(EBehaviorType::Hitted);
}

void UCBehaviorComponent::SetAvoidMode()
{
	ChangeType(EBehaviorType::Avoid);
}

void UCBehaviorComponent::ChangeType(EBehaviorType InType)
{
	EBehaviorType type = GetType();
	// 블랙보드에 AI 상태 값 세팅
	// blackboard에서는 enum을 uint8로 받는다.
	Blackboard->SetValueAsEnum(BehaviorKey, (uint8)InType);

	if (OnBehaviorTypeChanged.IsBound())
		OnBehaviorTypeChanged.Broadcast(type, InType);
}

ACPlayer* UCBehaviorComponent::GetTargetPlayer()
{
	// 블랙보드에 캐릭터가 있었다면 그것을 리턴
	return Cast<ACPlayer>(Blackboard->GetValueAsObject(PlayerKey));
}

EBehaviorType UCBehaviorComponent::GetType()
{
	// 블랙보드에 세팅되어있는 키에 따른 enum 값(EBehaviorType)을 가져옴
	// uint8을 다시 EBehaviorType로 캐스팅함
	return (EBehaviorType)Blackboard->GetValueAsEnum(BehaviorKey);
}

 

 

CEnemy.h 수정된 내용


더보기
...

UCLASS()
class UONLINE_04_ACTION_API ACEnemy : public ACharacter, public IICharacter
{
	GENERATED_BODY()
...
protected:
	UPROPERTY(BlueprintReadOnly, VisibleDefaultsOnly)
		class UCActionComponent* Action;
};

 

 

 

 

 

CActionComponent.h 수정된 내용


더보기
...

UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class UONLINE_04_ACTION_API UCActionComponent : public UActorComponent
{
	GENERATED_BODY()

private:
	UPROPERTY(EditDefaultsOnly, Category = "Weapons")
		class UCActionData* DataAssets[(int32)EActionType::Max];

public:
	// 현재 타입에 맞는 ActionData 가져옴
	UFUNCTION(BlueprintPure)
		FORCEINLINE class UCAction* GetCurrent() { return Datas[(int32)Type]; }

public:
	...
    
	UFUNCTION(BlueprintCallable)
		void SetUnarmedMode();

	// BP에서 콜 할 수 있도록
	UFUNCTION(BlueprintCallable)
		void SetTwoHandMode();

	UFUNCTION(BlueprintCallable)
		void SetIceBallMode();

	UFUNCTION(BlueprintCallable)
		void SetWarpMode();
	
	void SetFistMode();
	void SetOneHandMode();
	void SetFireStormMode();
    
    
private:
	EActionType Type;
	class UCAction* Datas[(int32)EActionType::Max];

};

 

 

 

 

 

 

 

CActionComponent.cpp 추가된 내용


더보기
...

void UCActionComponent::BeginPlay()
{
	Super::BeginPlay();
	
	ACharacter* character = Cast<ACharacter>(GetOwner());
	for (int32 i = 0; i < (int32)EActionType::Max; i++)
	{
		// 사본 생성
		if (!!DataAssets[i])
			DataAssets[i]->BeginPlay(character, &Datas[i]);
	}
}

 

 

CPlayer.h 추가된 내용


더보기
...
#include "GenericTeamAgentInterface.h"
...

UCLASS()
class UONLINE_04_ACTION_API ACPlayer : public ACharacter, public IICharacter, public IGenericTeamAgentInterface
{
	GENERATED_BODY()

private:
	...

	// 어차피 enum은 byte형이어서 uint8로 처리해줘도 됨
	// TeamID가 적이랑 다르면 적군이 된다.
	UPROPERTY(EditDefaultsOnly)
		uint8 TeamID = 0;

public:
	...
	virtual FGenericTeamId GetGenericTeamId() const override;

};

 

 

 

 

 

 

 

CPlayer.cpp 추가된 내용


더보기
...

FGenericTeamId ACPlayer::GetGenericTeamId() const
{
	return FGenericTeamId(TeamID);
}

 

 

CBTService_Melee.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTService.h"
#include "CBTService_Melee.generated.h"

UCLASS()
class UONLINE_04_ACTION_API UCBTService_Melee : public UBTService
{
	GENERATED_BODY()

public:
	UCBTService_Melee();

protected:
	// 매프레임마다 호출
	virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
	
	// 이 노드에 들어올때
	// virtual void OnSearchStart(FBehaviorTreeSearchData& SearchData);
};

 

 

 

 

 

 

 

CBTService_Melee.cpp


더보기
#include "CBTService_Melee.h"
#include "Global.h"
#include "Characters/CPlayer.h"
#include "Characters/CAIController.h"
#include "Characters/CEnemy_AI.h"
#include "Components/CBehaviorComponent.h"
#include "Components/CStateComponent.h"
#include "Components/CPatrolComponent.h"

UCBTService_Melee::UCBTService_Melee()
{
	// NodeName에 이름을 세팅해주면 BehaviorTree에 이름이 나타남
	NodeName = "Melee";
}

void UCBTService_Melee::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

	//BehaviorTreeComponent에 있는 GetOwner를 해주면 됨
	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	// AIController도 Actor로 부터 상속받아서 GetComponent가능
	UCBehaviorComponent* behavior = CHelpers::GetComponent<UCBehaviorComponent>(controller);

	// 빙의되어있는 Pawn가져오면 됨
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
	UCStateComponent* state = CHelpers::GetComponent<UCStateComponent>(ai);
	UCPatrolComponent* patrol = CHelpers::GetComponent<UCPatrolComponent>(ai);

	// 내가 피격상태라면
	if (state->IsHittedMode())
	{
		behavior->SetHittedMode();

		return;
	}

	ACPlayer* target = behavior->GetTargetPlayer();
	// 타겟이 없다면 wait모드
	if (target == NULL)
	{
		if (patrol != NULL && patrol->IsValid())
		{
			// patrol이 있고, Path가 Null이 아니라면
			behavior->SetPatrolMode();

			return;
		}

		behavior->SetWaitMode();

		return;
	}
	else
	{
		UCStateComponent* targetState = CHelpers::GetComponent<UCStateComponent>(target);
		// 타겟이 있는데 죽은 상태라면
		if (targetState->IsDeadMode())
		{
			behavior->SetWaitMode();

			return;
		}
	}

	// 공격 가능한 거리에 있는지
	// ai와 target의 거리를 구함
	float distance = ai->GetDistanceTo(target);
	if (distance < controller->GetBehaviorRange())
	{
		// 공격
		behavior->SetActionMode();

		return;
	}

	// 감지 범위 안에 있다면
	if (distance < controller->GetSightRadius())
	{
		// 추적모드로 변경
		behavior->SetApproachMode();

		return;
	}
}

 

 

CBTTaskNode_SetSpeed.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "Components/CStatusComponent.h"
#include "CBTTaskNode_SetSpeed.generated.h"

UCLASS()
class UONLINE_04_ACTION_API UCBTTaskNode_SetSpeed : public UBTTaskNode
{
	GENERATED_BODY()

private:
	UPROPERTY(EditAnywhere)
		ECharacterSpeed SpeedType;

public:
	UCBTTaskNode_SetSpeed();

protected:
	// Task가 실행될때 
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};

 

 

 

 

 

 

 

CBTTaskNode_SetSpeed.cpp


더보기
#include "CBTTaskNode_SetSpeed.h"
#include "Global.h"
#include "Characters/CAIController.h"
#include "Characters/CEnemy_AI.h"

UCBTTaskNode_SetSpeed::UCBTTaskNode_SetSpeed()
{
	NodeName = "Speed";
}

EBTNodeResult::Type UCBTTaskNode_SetSpeed::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	Super::ExecuteTask(OwnerComp, NodeMemory);

	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
	UCStatusComponent* status = CHelpers::GetComponent<UCStatusComponent>(ai);

	// 해당 스피드타입에 맞도록 캐릭터의 MaxWalkSpeed 세팅
	status->SetSpeed(SpeedType);

	// Task는 종료가 중요(종료안하면 대기상태임)
	// EBTNodeResult
	// Succeeded, Faild, InProgress(현재 작업이 완료되지 않은 상태)가 있음
	// 리턴을 안하면 InProgress 상태가 됨
	
	// BP에서는 InProgress가 따로 없음, 상태를 안주면 InProgress 상태를 유지함
	// C에서는 InProgress로 해서 공격이 완료되었을 때 Success를 시키거나 해야함 
	return EBTNodeResult::Succeeded;
}

 

 

 

 

 

 

CStatusComponent.h 수정된 내용


더보기
...

UENUM(BlueprintType)
enum class ECharacterSpeed : uint8
{
	Walk, Run, Sprint, Max,
};
// Sprint : 완전빠르게 달릴때

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UONLINE_04_ACTION_API UCStatusComponent : public UActorComponent
{
	GENERATED_BODY()

private:
	...
    
	// 에디터에서 속도 조정 가능
	UPROPERTY(EditDefaultsOnly, Category = "Speed")
		float Speed[(int32)ECharacterSpeed::Max] = { 200, 400, 600 };

public:
	...
	FORCEINLINE float GetWalkSpeed() { return Speed[(int32)ECharacterSpeed::Walk]; }
	FORCEINLINE float GetRunSpeed() { return Speed[(int32)ECharacterSpeed::Run]; }
	FORCEINLINE float GetSprintSpeed() { return Speed[(int32)ECharacterSpeed::Sprint]; }
	
    
public:
	...
	// MaxWalkSpeed에 해당 Speed모드의 값으로 세팅
	void SetSpeed(ECharacterSpeed InType);
}

 

 

 

 

 

 

 

CStatusComponent.cpp 수정된 내용


더보기
...

void UCStatusComponent::SetSpeed(ECharacterSpeed InType)
{
	UCharacterMovementComponent* movment = CHelpers::GetComponent<UCharacterMovementComponent>(GetOwner());

	movment->MaxWalkSpeed = Speed[(int32)InType];
}

 

 

 

 

 

 

CActionData.h 수정된 내용


더보기
#pragma once

#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "CActionData.generated.h"



UCLASS()
class UONLINE_04_ACTION_API UCActionData : public UDataAsset
{
	GENERATED_BODY()

public:
	// BP에서 해당 클래스BP 세팅가능
	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		TSubclassOf<class ACAttachment> AttachmentClass;

	// 등장 시킬 클래스 타입
	// 즉, 이 클래스를 BP화 해도 사용할 수 있다는 얘기
	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		TSubclassOf<class ACEquipment> EquipmentClass;

	// DataAsset에서 할당할 수 있게 되는 애들
	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FEquipmentData EquipmentData;

	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		FLinearColor EquipmentColor;

	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		TSubclassOf<class ACDoAction> DoActionClass;

	UPROPERTY(BlueprintReadOnly, EditAnywhere)
		TArray<FDoActionData> DoActionDatas;

public:
	// 객체를 생성해서 값을 설정한 후 리턴해주기 위해 이차 포인터를 사용
	void BeginPlay(class ACharacter* InOwnerCharacter, class UCAction** OutAction);

private:
	// 이름 출력해 줄 애
	FString GetLableName(class ACharacter* InOwnerCharacter, FString InName);
	
};

 

 

 

 

 

 

 

CActionData.cpp 수정된 내용


더보기
#include "CActionData.h"
#include "Global.h"
#include "CAction.h"
#include "CAttachment.h"
#include "CEquipment.h"
#include "CDoAction.h"
#include "GameFramework/Character.h"
#include "Components/SkeletalMeshComponent.h"

void UCActionData::BeginPlay(class ACharacter* InOwnerCharacter, class UCAction** OutAction)
{
	// 기존에는 SpawnActor를 사용했는데 SpawnActor는 등장과 동시에
	// BeginPlay()가 호출됨
	// 지금은 객체만 생성한 후 값을 세팅하고 등장시키는 방법을 사용하는데
	// 등장은 우리가 시키라고 할때 등장됨(즉, 객체 생성만 일어나고, 등장은 아직 하지 않음)
	// 이렇게 처리해주는 것이 SpawnActorDeffered

	FTransform transform;

	ACAttachment* attachment = NULL;
	
	if (!!AttachmentClass)
	{
		// 등장만 시켜놈
		attachment = InOwnerCharacter->GetWorld()->SpawnActorDeferred<ACAttachment>(AttachmentClass, transform, InOwnerCharacter);
		attachment->SetActorLabel(GetLableName(InOwnerCharacter, "Attachment"));
		// 어디에 붙일지는 CAttachment의 OnEquip()때 결정
		// Attachment->AttachToComponent(InOwnerCharacter->GetMesh(), FAttachmentTransformRules(EAttachmentRule::KeepRelative, true));
		UGameplayStatics::FinishSpawningActor(attachment, transform);
	}	
	
	// 지역변수는 NULL 초기화가 자동으로 안되지만, UPROPERTY() 변수는 아무것도 안쓰면 자동 NULL초기화됨
	ACEquipment* equipment = NULL;
	
	if (!!EquipmentClass)
	{
		// 객체 생성만 완료됨, 등장은 안 시킴
		// 여기서 transform은 확정시키지 않으면 BeginPlay()가 끝나면 확정이 됨, 그때 등장할 위치
		equipment = InOwnerCharacter->GetWorld()->SpawnActorDeferred<ACEquipment>(EquipmentClass, transform, InOwnerCharacter);
		// mesh에다가 붙여넣음
		equipment->AttachToComponent(InOwnerCharacter->GetMesh(), FAttachmentTransformRules(EAttachmentRule::KeepRelative, true));
		// 할당 받은 데이터를 Equipment로 넘겨줌
		equipment->SetActorLabel(GetLableName(InOwnerCharacter, "Equipment"));
		equipment->SetData(EquipmentData);
		equipment->SetColor(EquipmentColor);

		// FinishSpawningActor() : 확정해 등장시킬 액터와 Transform을 입력
		// transform : 확정되서 등장시킬 위치
		UGameplayStatics::FinishSpawningActor(equipment, transform);

		if (!!attachment)
		{
			// 연결해줌
			equipment->OnEquipmentDelegate.AddDynamic(attachment, &ACAttachment::OnEquip);
			equipment->OnUnequipmentDelegate.AddDynamic(attachment, &ACAttachment::OnUnequip);
		}
	}
	
	ACDoAction* doAction = NULL;

	if(!!DoActionClass)
	{
		doAction = InOwnerCharacter->GetWorld()->SpawnActorDeferred<ACDoAction>(DoActionClass, transform, InOwnerCharacter);
		doAction->AttachToComponent(InOwnerCharacter->GetMesh(), FAttachmentTransformRules(EAttachmentRule::KeepRelative, true));
		doAction->SetActorLabel(GetLableName(InOwnerCharacter, "DoAction"));
		doAction->SetDatas(DoActionDatas);
		UGameplayStatics::FinishSpawningActor(doAction, transform);

		if (!!equipment)
		{
			doAction->SetEquipped(equipment->GetEquipped());
		}

		if (!!attachment)
		{
			// 충돌 함수 연결해줌
			attachment->OnAttachmentBeginOverlap.AddDynamic(doAction, &ACDoAction::OnAttachmentBeginOverlap);
			attachment->OnAttachmentEndOverlap.AddDynamic(doAction, &ACDoAction::OnAttachmentEndOverlap);

			attachment->OnAttachmentCollision.AddDynamic(doAction, &ACDoAction::OnAttahmentCollision);
			attachment->OffAttachmentCollision.AddDynamic(doAction, &ACDoAction::OffAttahmentCollision);
		}
	}

	// 객체 생성 후 값을 초기화
	*OutAction = NewObject<UCAction>();
	(*OutAction)->Attachment = attachment;
	(*OutAction)->Equipment = equipment;
	(*OutAction)->DoAction = doAction;
	(*OutAction)->EquipmentColor = EquipmentColor;
}

...

 

 

 

 

 

 

CAction.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "CAction.generated.h"


// 데이터 에셋에서 데이터를 묶음으로 불러오기 위한 구조체를 정의
// 블루프린트랑 연동해서 불러올 것 이어서
// 장착 애니메이션 데이터 담당
USTRUCT(BlueprintType)
struct FEquipmentData
{
	GENERATED_BODY()

public:
	// 얘들은 따로 타입을 안써도 됨
	UPROPERTY(EditAnywhere)
		class UAnimMontage* AnimMontage;

	UPROPERTY(EditAnywhere)
		float PlayRatio = 1.0f;

	UPROPERTY(EditAnywhere)
		FName StartSection;

	UPROPERTY(EditAnywhere)
		bool bCanMove = true;

	// 시야를 바꿀 것인지
	UPROPERTY(EditAnywhere)
		bool bPawnControl = true;

};

// 액션 같은 경우 몽타주 데이터가 더 필요함
USTRUCT(BlueprintType)
struct FDoActionData : public FEquipmentData
{
	GENERATED_BODY()

public:
	UPROPERTY(EditAnywhere)
		float Power = 5.0f;

	// HitStop : 타격되었을 때 일시정지(경직)을 구현할 변수(타격감)
	UPROPERTY(EditAnywhere)
		float HitStop;

	// 타격시 플레이 시킬 효과
	UPROPERTY(EditAnywhere)
		class UParticleSystem* Effect;

	// 효과 위치, 크기를 추가적으로 보정하기 위한 값
	UPROPERTY(EditAnywhere)
		FTransform EffectTransform;

	// 타격시 카메라의 흔들림을 구현할 클래스
	UPROPERTY(EditAnywhere)
		TSubclassOf<class UCameraShake> ShakeClass;

	// 어떤 Throw 객체를 지정해서 던질건지
	UPROPERTY(EditAnywhere)
		TSubclassOf<class ACThrow> ThrowClass;
};

UCLASS()
class UONLINE_04_ACTION_API UCAction : public UObject
{
	GENERATED_BODY()

public:
	// UCActionData에서 여기 private 값을 세팅하도록 접근 허용
	friend class UCActionData;

public:
	FORCEINLINE class ACEquipment* GetEquipment() { return Equipment; }
	FORCEINLINE class ACDoAction* GetDoAction() { return DoAction; }
	FORCEINLINE class ACAttachment* GetAttachment() { return Attachment; }

	FORCEINLINE FLinearColor GetEquipmentColor() { return EquipmentColor; }	

private:
	// 사본을 만들 데이터들
	class ACEquipment* Equipment;
	class ACAttachment* Attachment;
	class ACDoAction* DoAction;

	FLinearColor EquipmentColor;
};

 

 

 

 

CBTTaskNode_Action.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "CBTTaskNode_Action.generated.h"

UCLASS()
class UONLINE_04_ACTION_API UCBTTaskNode_Action : public UBTTaskNode
{
	GENERATED_BODY()

private:
	// EditAnywhere 붙여줘야 밖에 테스트 할때 나타남
	UPROPERTY(EditAnywhere, Category = "AI")
		float Delay = 2.0f; // 공격 딜레이(공격하고 다시 공격 들어갈때 딜레이)
	
public:
	UCBTTaskNode_Action();

protected:
	// Task가 실행될때
	virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;

public:
	// Tick()이 있어야함
	// 공격이 완료된 상태에서 InProgress(Task 중인)상태를 Tick()에서 종료시켜줌
	virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;

private:
	float TotalTime = 0.0f; // 경과시간(일정 시간이 흐른 후 종료시키기 위함)
};

 

 

CBTTaskNode_Action.cpp


더보기
#include "CBTTaskNode_Action.h"
#include "Global.h"
#include "Characters/CAIController.h"
#include "Characters/CEnemy_AI.h"
#include "Components/CActionComponent.h"
#include "Components/CStateComponent.h"

UCBTTaskNode_Action::UCBTTaskNode_Action()
{
	// true로 해주어야 Tick이 실행될 수 있음
	bNotifyTick = true;

	NodeName = "Action";
}

EBTNodeResult::Type UCBTTaskNode_Action::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	Super::ExecuteTask(OwnerComp, NodeMemory);

	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
	UCActionComponent* action = CHelpers::GetComponent<UCActionComponent>(ai);

	TotalTime = 0.0f;
	// 공격 실행됨
	action->DoAction();

	// InProgress(대기) 상태는 현재 태스크가 종료되지 않은 상태로 현재 태스크를 계속 실행하고 있는 상태가 됨
	return EBTNodeResult::InProgress;
}

void UCBTTaskNode_Action::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);

	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
	UCStateComponent* state = CHelpers::GetComponent<UCStateComponent>(ai);


	// 경과한 시간이 됨
	TotalTime += DeltaSeconds;

	// idle 모드로 바뀌었다면(Action이 끝났다면 Idle 상태가 됨)
	// 경과시간(TotalTime)이 Delay보다 크다면 태스크가 완료된(Finish) 상태임
	if (state->IsIdleMode() && TotalTime > Delay)
	{
		// FinishLatetAbort() : 중단으로 종료
		// FinishLatentTask() : 성공이나, 실패로 태스크 종료
		// Abort : 중단된 걸로 종료, FinishLatetAbort()로 종료시키는 것이 좋음
		// InProgress(대기)
		// Succeeded(완료)
		FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
	}

}

 

CPatrolPath.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "CPatrolPath.generated.h"

UCLASS()
class UONLINE_04_ACTION_API ACPatrolPath : public AActor
{
	GENERATED_BODY()

private:
	// 컨스트럭션 스크립트에서 다루기 위해서(C에서 컨스트럭션 스크립트를 다루는 방법이 있음)
	UPROPERTY(EditAnywhere, Category = "Loop")
		bool bLoop;

private:
	UPROPERTY(VisibleDefaultsOnly)
		class USceneComponent* Scene;

	UPROPERTY(VisibleDefaultsOnly)
		class USplineComponent* Spline;

	UPROPERTY(VisibleDefaultsOnly)
		class UTextRenderComponent* Text;

public:
	FORCEINLINE class USplineComponent* GetSpline() { return Spline; }
	
public:	
	ACPatrolPath();

	// BP의 컨스트럭트 스크립트(Construction Script)와 동일한 함수
	// 월드에서 변수들 속성 수정하면 인스턴스 각각 적용해서 처리할 수 있음
	virtual void OnConstruction(const FTransform& Transform) override;

protected:
	virtual void BeginPlay() override;

};

 

 

CPatrolPath.cpp


더보기
#include "CPatrolPath.h"
#include "Global.h"
#include "Components/SplineComponent.h"
#include "Components/TextRenderComponent.h"

ACPatrolPath::ACPatrolPath()
{
	CHelpers::CreateComponent<USceneComponent>(this, &Scene, "Scene");
	CHelpers::CreateComponent<USplineComponent>(this, &Spline, "Spline", Scene);
	CHelpers::CreateComponent<UTextRenderComponent>(this, &Text, "Text", Scene);

	// 땅에 뭍히지 않도록 위로 좀 올림
	Spline->SetRelativeLocation(FVector(0, 0, 30));
	
	Text->SetRelativeLocation(FVector(0, 0, 120));
	Text->SetRelativeRotation(FRotator(0, 180, 0));
	Text->HorizontalAlignment = EHorizTextAligment::EHTA_Center;
	Text->TextRenderColor = FColor::Red;


}

void ACPatrolPath::OnConstruction(const FTransform& Transform)
{
	// 배치 될때 이름 할당하기 위해
	Text->Text = FText::FromString(GetActorLabel());

	// 스플라인이 닫힌 루프인지 여부를 지정합니다. 루프 위치는 마지막 포인트의 입력 키 이후 1.0이 됩니다.
	Spline->SetClosedLoop(bLoop);
}

void ACPatrolPath::BeginPlay()
{
	Super::BeginPlay();
	
	Text->SetVisibility(false);
}

 

CPatrolComponent.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "CPatrolComponent.generated.h"


UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UONLINE_04_ACTION_API UCPatrolComponent : public UActorComponent
{
	GENERATED_BODY()

private:
	UPROPERTY(EditAnywhere)
		class ACPatrolPath* Path;

	UPROPERTY(EditAnywhere)
		int32 Index; // 현재 몇번을 타고 있는지

	UPROPERTY(EditAnywhere)
		bool bReverse; // 뒤집어 졌는지(역으로 진행할건지)

	UPROPERTY(EditAnywhere)
		float AcceptanceRadius = 50; // 도달반경(얼마만큼 근방으로 갔을때 도달로 간주할지)
	
public:
	// 있는지
	FORCEINLINE bool IsValid() { return Path != NULL; }

	// 어디로 이동할지(어느위치로 이동할지, 거리가 얼마나 되는지) 
	bool GetMoveTo(FVector& OutLocation, float& OutAcceptanceRadius);

	// 도달했으면 다음으로 넘겨줘라
	void UpdateNextIndex();

public:	
	UCPatrolComponent();

protected:
	virtual void BeginPlay() override;
};

 

 

CPatrolComponent.cpp


더보기
#include "CPatrolComponent.h"
#include "Global.h"
#include "Actors/CPatrolPath.h"
#include "Components/SplineComponent.h"

UCPatrolComponent::UCPatrolComponent()
{
}


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

}

bool UCPatrolComponent::GetMoveTo(FVector& OutLocation, float& OutAcceptanceRadius)
{
	OutLocation = FVector::ZeroVector;
	OutAcceptanceRadius = AcceptanceRadius;
	CheckNullResult(Path, false);

	// GetLocationAtSplinePoint() : 해당 포인트의 위치를 반환
	// 1. 포인트 인덱스, 2 : 어떤 좌표 방식으로 위치를 받을지(우리는 월드)
	OutLocation = Path->GetSpline()->GetLocationAtSplinePoint(Index, ESplineCoordinateSpace::World);

	return true;
}

void UCPatrolComponent::UpdateNextIndex()
{
	CheckNull(Path);

	// 포인트가 몇개인지 가져옴
	int32 count = Path->GetSpline()->GetNumberOfSplinePoints();
	
	// 역으로 가는 것 계산
	if (bReverse)
	{
		if (Index > 0)
		{
			Index--;

			return;
		}

		// 닫힌 루프라면
		if (Path->GetSpline()->IsClosedLoop())
		{
			Index = count - 1;

			return;
		}

		// 다 도달 했다면
		Index = 1;
		bReverse = false;
		
		return;
	}

	// 정방향이라면
	if (Index < count - 1)
	{
		Index++;

		return;
	}

	if (Path->GetSpline()->IsClosedLoop())
	{
		Index = 0;

		return;
	}

	// 0, 1, 2, 3, 4 갔다가 다시 3으로 돌아와야 하니까
	Index = count - 2;
	// 다시 뒤집어줌
	bReverse = true;
}

 

 

CBTTaskNode_Patrol.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "CBTTaskNode_Patrol.generated.h"

UCLASS()
class UONLINE_04_ACTION_API UCBTTaskNode_Patrol : public UBTTaskNode
{
	GENERATED_BODY()

public:
	UCBTTaskNode_Patrol();

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

public:
	virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;

};

 

 

CBTTaskNode_Patrol.cpp


더보기
#include "CBTTaskNode_Patrol.h"
#include "Global.h"
#include "Global.h"
#include "Characters/CAIController.h"
#include "Characters/CEnemy_AI.h"
#include "Components/CPatrolComponent.h"


UCBTTaskNode_Patrol::UCBTTaskNode_Patrol()
{
	bNotifyTick = true;

	NodeName = "Patrol";
}

EBTNodeResult::Type UCBTTaskNode_Patrol::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	Super::ExecuteTask(OwnerComp, NodeMemory);

	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
	UCPatrolComponent* patrol = CHelpers::GetComponent<UCPatrolComponent>(ai);

	FVector location; // 어느 위치로 이동할건지
	float acceptance; // 도달반경

	// 갈 수 없는 곳이라면 Failed 상태
	if (patrol->GetMoveTo(location, acceptance) == false)
		return EBTNodeResult::Failed;

	// 도달할때 까지가 InProgress이다.(InProgress면 반드시 완료를 시켜줘야 함)
	return EBTNodeResult::InProgress;
}

void UCBTTaskNode_Patrol::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);

	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
	UCPatrolComponent* patrol = CHelpers::GetComponent<UCPatrolComponent>(ai);

	FVector location; // 어느 위치로 이동할건지
	float acceptance; // 도달반경

	patrol->GetMoveTo(location, acceptance);
	// MoveToLocation() : AI Controller를 해당 위치로 이동시키는 함수
	// AIMoveTo 함수가 내부적으로 이 함수를 호출해 이동하게 된다.
	// 이 함수가 도달할수 있는지, 가고 있는지를 리턴해줌
	// 1. 어디로 갈지, 2. 얼마만큼 갔을때 도달로 간주할지, 3 : 두개가 겹쳤을때 정지할 건지, 4 bUsePathFinding : 우리는 네비게이션 메쉬사용하니까 기본적으로 true로 준다.
	// 기타 인자 bCanStrafe : 옆걸음질 칠건지
	EPathFollowingRequestResult::Type type = controller->MoveToLocation(location, acceptance, false);

	/*
	UENUM(BlueprintType)
	namespace EPathFollowingRequestResult
	{
		enum Type
		{
			Failed, // 갈 수 없는 위치
			AlreadyAtGoal, // 현재 도달했다면
			RequestSuccessful // 현재 완전히 정상적으로 종료가 되었다(갈 수 있는 위치인지를 확인할때 사용)
		};
	}
	*/

	if (type == EPathFollowingRequestResult::Failed)
	{
		// 갈 수 없다면 Failed로 해줌
		FinishLatentTask(OwnerComp, EBTNodeResult::Failed);

		return;
	}
	
	// 현재 도달했다면
	if (type == EPathFollowingRequestResult::AlreadyAtGoal)
	{
		FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);

		patrol->UpdateNextIndex();
	}
}

 

 

 

결과