본문 바로가기

Unreal Engine 4/C++

<Unreal C++> 86 - Action RPG (Enemy Wizard AI)

 

필요한 개념


<원거리 공격 캐릭터 생성>
- 플레이어가 근방에 가면 랜덤한 주변의 위치를 구해서 회피함(Warp 무기로 장착해서 회피)

- 일정 반경안에 플레이어가 들어오면 공격을 시작
- EQS로 회피할 지점 선택, 무기 Change하는 Task를 만들어서 무기 Change를 실행, Warp를 해서 반경을 체크해서 공격에 들어가는 작업함
- Wizard는 따로 순찰 모드가 없음

* 먼저 해당 작업을 할 수 있도록 Service를 생성(BTService를 상속받는 CBTService_Wizard 클래스 생성)

public:
	UCBTService_Wizard();

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




UCBTService_Wizard::UCBTService_Wizard()
{
	NodeName = "Wizard";
}



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

	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	UCBehaviorComponent* behavior = CHelpers::GetComponent<UCBehaviorComponent>(controller);

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

	if (state->IsHittedMode())
	{
		behavior->SetHittedMode();

		return;
	}

	ACPlayer* target = behavior->GetTargetPlayer();
	if (target == NULL)
	{
		behavior->SetWaitMode();
		// ClearFocus() : 포커스 초기화
		// EAIFocusPriority::Default : 우선순위를 기본으로 주겠다.
		// SetFocus() : 해당 액터를 포커스로 설정
		// 즉, 포커스를 초기화할때 우선순위를 기본으로 주겠다는 의미
		// 밑에 적을 바라볼 수 있도록 처리하기 위해 초기화해줌
		controller->ClearFocus(EAIFocusPriority::Default);

		/*
		namespace EAIFocusPriority
		{
			typedef uint8 Type;

			const Type Default = 0; // 기본
			const Type Move = 1; // 이동모드 였는지
			const Type Gameplay = 2; // 게임모드에서 포커스를 초기화했는지

			const Type LastFocusPriority = Gameplay;
		}
		*/

		return;
	}
	else
	{
		UCStateComponent* targetState = CHelpers::GetComponent<UCStateComponent>(target);
		if (targetState->IsDeadMode())
		{
			behavior->SetWaitMode();

			return;
		}
	}

	// 적을 발견했다면 항상 적을 바라보도록 처리
	// 첫번째로 지정한 캐릭터를 포커스로 지정하고 바라보게됨
	controller->SetFocus(target);

	// 감지 범위
	float action = controller->GetSightRadius();

	// ai와 Target과의 거리
	float distance = ai->GetDistanceTo(target);

	if (distance < controller->GetBehaviorRange())
	{
		// 회피모드로
		behavior->SetAvoidMode();

		return;
	}

	// 감지범위보다 행동범위가 작다면 액션 실행
	if (distance < action)
	{
		behavior->SetActionMode();

		return;
	}
}



* CPlayer만이 아닌, Enemy도 사용할 수 있도록 CDoAction_Warp 수정함
-> 기존에는 마우스 위치에 Warp가 되도록 했었음, 그러나 Enemy는 마우스가 없음

private:
	// 커서 위치를 사용할지, 적이 이동할 위치를 사용할지 판단
	bool UseCursorLocation();



void ACDoAction_Warp::DoAction()
{
	// 장비가 장착되어있는 상태일때만 실행
	CheckFalse(*bEquipped);
	// Idle 모드 일때만 실행
	CheckFalse(State->IsIdleMode());

	// 플레이어라면
	if (UseCursorLocation())
	{
		FRotator rotator;
		CheckFalse(GetCursorLocationAndRotation(Location, rotator));

	}
	// AIController라면
	else
	{
		AAIController* controller = OwnerCharacter->GetController<AAIController>();
		UCBehaviorComponent* behavior = CHelpers::GetComponent<UCBehaviorComponent>(controller);

		// 이동할 위치를 가져옴
		Location = behavior->GetWarpLocation();
		Decal->SetVisibility(false);
	}

	State->SetActionMode();

	OwnerCharacter->PlayAnimMontage(Datas[0].AnimMontage, Datas[0].PlayRatio, Datas[0].StartSection);

	Datas[0].bCanMove ? Status->SetMove() : Status->SetStop();
}



void ACDoAction_Warp::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	// 장착된 상태에서만 진행
	CheckFalse(*bEquipped);

	// 적일 경우 Decal(마우스 위치)을 보여줄 필요가 없다.
	if (UseCursorLocation() == false)
	{
		Decal->SetVisibility(false);

		return;
	}

	FVector location;
	FRotator rotator;

	if (GetCursorLocationAndRotation(location, rotator))
	{
		Decal->SetVisibility(true);
		// 커서가 리턴해 주는 위치에 세팅
		Decal->SetWorldLocation(location);
		Decal->SetWorldRotation(rotator);

		return;
	}
	else
	{
		// 충돌하는 위치가 없다면 보이지 않도록 처리
		Decal->SetVisibility(false);
	}
}

 

bool ACDoAction_Warp::UseCursorLocation()
{
	// 판단은 간단함(AAIController를 가졌다면 Enemy이다.)
	// NULL인 경우 플레이어로 판단
	return OwnerCharacter->GetController<AAIController>() == NULL;
}


* Blackboard에 vector로 Warp 변수 추가해줌(AI가 Warp로 이동할 위치)

BB_Enemy(Blackboard)

 

* CBehaviorComponent에 Warp키 변수 추가해줌

UPROPERTY(EditAnywhere)
	FName WarpKey = "Warp"; // Warp 키



* CBehaviorComponent에 GetWarpLocation() 함수 구현
-> AI가 이동할 위치 구하는 함수


// Warp 이동할 위치 구해줌
FVector GetWarpLocation();




FVector UCBehaviorComponent::GetWarpLocation()
{
	// 블랙보드에 있는 Vector Warp 리턴
	return Blackboard->GetValueAsVector(WarpKey);
}



* BP_CAIController_Wizard, BP_CEnemy_AI_Wizard, BT_Wizard 생성

 

 


- BP_CEnemy_AI_Wizard에서 Behavior Tree 변수 BT_Wizard로 세팅
- BP_CEnemy_AI_Wizard에서 AI Controller Class를 BP_CAIController_Wizard로 세팅
- BP_CEnemy_AI_Wizard에서 ActionComponent에 DataAsset에 Unarmed, Warp, IceBall 세팅
- BP_CEnemy_AI_Wizard의 이벤트 그래프 BeginPlay()에서 기본 모드로 IceBall모드로 세팅


BehaviorTree에서는 타켓이 잡혀있으면 공격할 거고, 타켓이 일정 거리안에 들어오면 EQS로 위치구한다음 Warp할 것임


* BT_Wizard(Behavior Tree) Sequence추가, 데코레이션 Blackboard Avoid랑 상태가 같다면 실행 
- BlackBoard 데코레이션 추가 할때, 관찰자 중단을 self로 해준다.


여기서 우선 EQS Query를 수행하여(RunEQSQuery Task(기존에 있는)) 위치를 얻어오고, 들어왔다면 회피니까 Avoid를 수행(Warp)
BP때는 RunEQS를 왼쪽으로 갈건지 오른쪽으로 갈건지 상황에 따라 실행시켰다.

EQS 실행할 Query가 있어야하고, ResultBlackBoard Key가 있어야함
ResultBlackBoard Key는 우리가 값을 받을 위치값(Vector) Warp(Blackboard에 있는)로 설정
즉, RunEQS Query 디테일에서 세팅해줌
Blackboard에 있는 BlackboardKey : Warp(리턴받을 값)

 


!중요 : EQS를 사용하기 위해서는 에디터->편집->에디터 개인설정 -> 일반 -> 실험단계 기능 -> AI -> 인바이런먼트 쿼리 시스템을 체크해서 켜준다.

4.26버전에서는 EQS가 정식기능으로 포함되어 있어서 더 이상 체크를 하지 않아도 된다.

* 인바이런먼트 쿼리 EQS_Wizard BP 생성
우클릭 -> AI -> 인바이런먼트 쿼리 생성

 

* EQS_Wizard BP



Root에서 끌어다가 보면
Points::Cirlcle로 생성함
Circle을 중심으로 할 것임
원형으로 반경안에 있는 애를 선택
CircleRadius(반경)은 1000 그대로 둔다.
DataBinding : Random Number를 설정해서 랜덤으로 지정해줘도 됨, 그러나 여기선 None으로 지정
Space Between(아이템이 생성되는 간격) : 100으로 설정
Number Of Points(아이템을 그릴 개수) : 8개로 설정
즉, 100에서 8개의 위치 아이템이 그려짐
1000에서 100의 간격으로 10개 그려짐

* 우클릭하면 테스트 추가해서 Dot으로 해줌
Dot : 2개의 방향가지고 우리가 수행함

 

 


LineA
Mode : Two Points
Line From : CEnvQueryContext_CPlayer(우리가 정의한 플레이어 구하는 클래스)
Line To : EnvQueryContext_Querier(질의자)

LineB
Mode : Two Points
Line From : CEnvQueryContext_CPlayer
Line To : EnvQueryContext_Item(아이템)

LineA는 플레이어에서 질의자(AI)를 향하는 방향
LineB는 플레이어에서 Item을 향하는 방향
이 두개의 내적을 구함(-1 ~ 1까지)


* LineFrom에 넣기 위한 EQS Context 생성(EnvQueryContext를 상속받는 CEnvQueryContext_Player 클래스 생성)
-> Player를 제공할 것임

재정의 할것
BP에서는 4가지(위치를 리턴할지, 액터를 리턴할지, 위치들을 리턴할지, 액터들을 리턴할지)가 있었음(여기서는 그 4가지로 나뉘지 않고 한가지로 통합되어 있음)

virtual void ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const override




private:
	// BP에서는 4가지(위치를 리턴할지, 액터를 리턴할지, 위치들을 리턴할지, 액터들을 리턴할지)가 있었음(여기서는 그 4가지로 나뉘지 않고 한가지로 통합되어 있음)
	virtual void ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const override;



void UCEnvQueryContext_Player::ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const
{
	Super::ProvideContext(QueryInstance, ContextData);

	// QueryInstance : 얘를 질의하는 Instance(즉, AIController)
	// Owner는 AIController의 Pawn이 됨
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(QueryInstance.Owner.Get());

	// QueryInstance에도 Controller가 있지만 꺼내기 까다로움
	// 이렇게 하는게 좋다.
	UCBehaviorComponent* behavior = CHelpers::GetComponent<UCBehaviorComponent>(ai->GetController());


	// UEnvQueryItemType_Actor은 리턴할 타입을 의미하며
	// Actors, Location, Locations를 포함해 BP에서처럼 총 4개의 종류가 있다.
	// SetContextHelper()에다가 Actor를 줘서 플레이어를 리턴시키기 위함
	if (!!behavior->GetTargetPlayer())
		UEnvQueryItemType_Actor::SetContextHelper(ContextData, behavior->GetTargetPlayer());
}


* BT_Wizard(Behavior Tree) RunEQSQuery Task 디테일에 EQS EQS_Request에 EQS_Wizard를 주고, RunMode에는 Single Random Item From Best 25%(랜덤 상위 25%만 사용, 뒤쪽에서 조금 넓게 이동가능하게)를 세팅

 


* BP_CAIController_Wizard에 BehaviorRange(행동반경) 500으로 세팅

 

 

* PerceptionComponent의 디테일


Perception의 디테일

AIPerception
Sense Config
0의 Sense
SightRadius(시야반경) : 1000
Lose Sight Radius(타겟을 놓치게 된 경우, 시야반경) : 1000
PeripheralVisionHalfAngleDegree(시야각) : 180도(AI가 주변을 얼마나 넓게 볼 수 있는지)
Max Age(이 센스에서 자극받은 것이 잊혀지는데 걸리는 최대시간): 1


* 결과를 보면('키하고 3번(EQS) 옵션을 키면 보임)
- Warp를 할 구역의 원에 둥글게 쭉 보인다.



<필터 적용>
- 이 원들 중에서 필터링을 통해 Warp할 공간을 찾을 것이다.

(적의 옆이나 뒤가 아니라, 플레이어 뒤쪽으로 이동하는 방식을 쓸 것임)

 

- EQS_Wizard BP에 Dot의 필터

Filter : Maximum
Float Value Max : 0.5(0.5이하를 잘라버림)
-> Player가 있는 뒤쪽인 반 만을 자름

* 그런데 옆쪽이 선택되고 있다.(값을 Inverse해서 플레이어의 뒤쪽으로 선택되도록 한다.) 

-> Enemy 기준 플레이어를 바라보면 플레이어 뒤쪽이 선택되도록 해야함
Score의 Scoring Equation : Linear -> InverseLinear로 바꾸면(값이 뒤집어짐)
-> 그러면 플레이어의 뒤쪽이 선택이 됨


* 무기를 선택할 Task 작성(BTTaskNode를 상속받는 CBTTaskNode_Change 클래스 생성)

private:
	UPROPERTY(EditAnywhere)
		EActionType Type; // 어떤 무기로 바뀔지 지정

public:
	UCBTTaskNode_Change();

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

public:
	// 무기교체에 대한 시간이 필요하므로
	virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;




UCBTTaskNode_Change::UCBTTaskNode_Change()
{
	bNotifyTick = true;

	NodeName = "Change";
}



EBTNodeResult::Type UCBTTaskNode_Change::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); 

	if (Type == EActionType::Warp)
	{
		if (action->IsWarpMode() == false)
			action->SetWarpMode();
	}
	else if (Type == EActionType::IceBall)
	{
		if (action->IsIceBallMode() == false)
			action->SetIceBallMode();
	}

	return EBTNodeResult::InProgress;
}

 

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

	// OwnerComp : 이놈을 실행한 BehaviorTreeComponent
	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());

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

	// ExcuteTask()에서 모드를 바꾸어주었고, Idle 모드가 되었다면 모드가 정상적으로 바뀐것이다.
	if (state->IsIdleMode())
		FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}



요약하자면 LineA, LineB의 내적을 구해, 필터링을 통해 값을 걸러서 특정 반경의 구를 구하고, 

여기서 Inverse Linear로 값을 뒤집어 플레이어 뒤쪽의 구 지점을 선택한다.
Avoid 상태면 Warp 모드로, IceBall모드면 IceBall모드로 함

* BT_Wizard 세팅
Avoid(공격 범위 안에 들어오면 Avoid 상태임)시에는 RunEQS Query로 플레이어 뒤의 위치를 선택해서 Warp Vector로 값을 전달하고 Task Change로 Warp모드로 변경해서 무기 장착하고 Action으로 해당 무기의 액션을 수행한다.
Action시에는 Task Change로 IceBall모드로 변경해서 무기 장착하고 Action으로 해당 무기의 액션을 수행한다.

 


* 결과를 보면 적이 죽으면 Warp 커서(Decal)이 플레이어로 넘어오는 버그가 있음
-> 액터가 제거될 때 무기가 제거되지 않아서 발생하는 문제, 무기가 소속되어 있는 액터가 없어져서 다른 액터로 자동으로 소유권이 이전된 상황
가지고 있는 모든 무기 제거


<가지고 있는 모든 무기 제거>

- CActionComponent에 추가

void UCActionComponent::DestroyAllActions()
{
	for (UCAction* data : Datas)
	{
		if (!!data == false)
			continue;

		// 즉, 있다면 다 제거한다.
		if (!!data->GetAttachment())
			data->GetAttachment()->Destroy();

		if (!!data->GetEquipment())
			data->GetEquipment()->Destroy();

		if (!!data->GetDoAction())
			data->GetDoAction()->Destroy();

	}
}



- CEnemy에 추가

void ACEnemy::Begin_Dead()
{
	Action->OffAllCollision();

	// 모든 무기 제거
	Action->DestroyAllActions();

	GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}



* 결과를 보면 발사중에 다가가면 동작이 캔슬되서 바로 이동되는 것을 볼 수 있음
-> 따로 고치면 됨

 

 

<플레이어 피격, 사망 처리>

* CPlayer의 Hitted 상태와 Dead 상태 가능하도록 세팅

public:
	float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;

private:
	void Hitted();
	void Dead();


public:
	virtual void Begin_Dead() override;
	virtual void End_Dead() override;

private:
	// 공격한 놈의 컨트롤러
	class AController* DamageInstigator;
	// 데미지 양
	float DamageValue;




// 얘는 상태 바뀔때 마다 콜됨
void ACPlayer::OnStateTypeChanged(EStateType InPrevType, EStateType InNewType)
{
	// 상태에 따라 처리가능
	// Begin()은 여기서 처리해주고, End()함수들은 Notify에서 콜되도록 할 것이다.
	switch (InNewType)
	{
		case EStateType::Roll: Begin_Roll(); break;
		case EStateType::Backstep: Begin_Backstep(); break;
		case EStateType::Hitted: Hitted(); break;
		case EStateType::Dead: Dead(); break;
	}
}




float ACPlayer::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	DamageInstigator = EventInstigator;
	DamageValue = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

	State->SetHittedMode();

	return Status->GetHealth();
}



void ACPlayer::Hitted()
{
	Status->SubHealth(DamageValue);
	DamageValue = 0.0f;

	if (Status->GetHealth() <= 0.0f)
	{
		State->SetDeadMode();

		return;
	}
    Montages->PlayHitted();
}





void ACPlayer::Dead()
{
	// 재동작 방지
	CheckFalse(State->IsDeadMode());

	Montages->PlayDead();
}

 

void ACPlayer::Begin_Dead()
{
	Action->OffAllCollision();
	Action->DestroyAllActions();
}



void ACPlayer::End_Dead()
{
	/*
		namespace EQuitPreference
		{
			enum Type
			{
				Quit, // 종료시킬 건지
				Background, // Background로 돌릴건지
			};
		}
*/

	// 사망하면 게임을 종료시킴
	// UKismetSystemLibrary::QuitGame() : 게임 종료 함수
	// 4번째 인자, bool bIgnorePlatformRestrictions : 플랫폼 폼에 따라 다르게 처리할건지(우리는 false)
	UKismetSystemLibrary::QuitGame(GetWorld(), GetController<APlayerController>(), EQuitPreference::Quit, false);
}




* Player용 Dead, Hitted 몽타주 생성 및 세팅

- Dead Montage 세팅

 

 

FullBody로 세팅
Player 애니메이션도 Enemy와 마찬가지로 끝 프레임을 덧붙여서 엎어져 있는 상태가 지속되도록 함
-> 마우스 우클릭 -> 끝부분에 덧붙입니다 -> 100 프레임해주면 됨
Dead를 시작할때부터 줘도 상관없음, Begin_Dead는 Collisoin을 전부끄고, 무기들을 전부 Destory()하기 때문에
Dead끝부분은 죽었을때 애니메이션 비슷한 구간에 둔다. Player의 End_Dead는 게임을 종료시킨다.


- Hitted Montage 세팅

 

 

 

UpperBody로 세팅(플레이어는 움직이면서 맞을 것임)
애니메이션이 끝날 쯤에 Hitted Notify 추가해준다.(Hitted Notify는 IdleMode로 바꿔주는 역할만 수행)
HittedMode로 바꿔주는 것은 Player의 TakeDamage가 처리한다.


* Player의 DataTable 세팅해줌

- Player.csv에 Hitted, Dead 모드 추가하고 데이터테이블 Player는 리임포트 해준다.

*&nbsp;Player.csv

 

* Player DataTable


* Wizard 공격이 플레이어 한테 안맞는다.
-> 발사하는 시점을 조정함으로써 플레이어 방향으로 정확히 발사할 수 있도록 처리해야함

(일찍 공격하게 되면 IceBall이 소켓에 붙기 때문에 회전되어서 정확히 정면으로 안가게 된 것)
IceBall Montage에서 BeginAction의 시작 프레임을 손이 다 뻗었을때로 늦춘다.

 

 

-> 이제 정면으로 쏘니까 플레이어가 피격되게 됨

 

 

 

 

 

CBTService_Wizard.h


더보기
#pragma once

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

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

public:
	UCBTService_Wizard();
	
protected:
	virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};

 

 

 

 

 

 

 

CBTService_Wizard.cpp


더보기
#include "CBTService_Wizard.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"

UCBTService_Wizard::UCBTService_Wizard()
{
	NodeName = "Wizard";
}

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

	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
	UCBehaviorComponent* behavior = CHelpers::GetComponent<UCBehaviorComponent>(controller);

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

	if (state->IsHittedMode())
	{
		behavior->SetHittedMode();

		return;
	}

	ACPlayer* target = behavior->GetTargetPlayer();
	if (target == NULL)
	{
		behavior->SetWaitMode();
		// ClearFocus() : 포커스 초기화
		// EAIFocusPriority::Default : 우선순위를 기본으로 주겠다.
		// SetFocus() : 해당 액터를 포커스로 설정
		// 즉, 포커스를 초기화할때 우선순위를 기본으로 주겠다는 의미
		// 밑에 적을 바라볼 수 있도록 처리하기 위해 초기화해줌
		controller->ClearFocus(EAIFocusPriority::Default);

		/*
		namespace EAIFocusPriority
		{
			typedef uint8 Type;

			const Type Default = 0; // 기본
			const Type Move = 1; // 이동모드 였는지
			const Type Gameplay = 2; // 게임모드에서 포커스를 초기화했는지

			const Type LastFocusPriority = Gameplay;
		}
		*/

		return;
	}
	else
	{
		UCStateComponent* targetState = CHelpers::GetComponent<UCStateComponent>(target);
		if (targetState->IsDeadMode())
		{
			behavior->SetWaitMode();

			return;
		}
	}

	// 적을 발견했다면 항상 적을 바라보도록 처리
	// 첫번째로 지정한 캐릭터를 포커스로 지정하고 바라보게됨
	controller->SetFocus(target);

	// 감지 범위
	float action = controller->GetSightRadius();
	
	// ai와 Target과의 거리
	float distance = ai->GetDistanceTo(target);

	if (distance < controller->GetBehaviorRange())
	{
		// 회피모드로
		behavior->SetAvoidMode();

		return;
	}

	// 감지범위보다 행동범위가 작다면 액션 실행
	if (distance < action)
	{
		behavior->SetActionMode();

		return;
	}
}

 

 

CDoAction_Warp.h 수정된 내용


더보기
...

UCLASS()
class UONLINE_04_ACTION_API ACDoAction_Warp : public ACDoAction
{
	GENERATED_BODY()

...

private:
	// 커서 위치를 사용할지, 적이 이동할 위치를 사용할지 판단
	bool UseCursorLocation();
};

 

 

 

 

 

 

 

CDoAction_Warp.cpp 수정된 내용


더보기
...

void ACDoAction_Warp::DoAction()
{
	// 장비가 장착되어있는 상태일때만 실행
	CheckFalse(*bEquipped);
	// Idle 모드 일때만 실행
	CheckFalse(State->IsIdleMode());

	// 플레이어라면
	if (UseCursorLocation())
	{
		FRotator rotator;
		CheckFalse(GetCursorLocationAndRotation(Location, rotator));
		
	}
	// AIController라면
	else
	{
		AAIController* controller = OwnerCharacter->GetController<AAIController>();
		UCBehaviorComponent* behavior = CHelpers::GetComponent<UCBehaviorComponent>(controller);
		
		// 이동할 위치를 가져옴
		Location = behavior->GetWarpLocation();
		Decal->SetVisibility(false);
	}

	State->SetActionMode();

	OwnerCharacter->PlayAnimMontage(Datas[0].AnimMontage, Datas[0].PlayRatio, Datas[0].StartSection);

	Datas[0].bCanMove ? Status->SetMove() : Status->SetStop();
}

void ACDoAction_Warp::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	// 장착된 상태에서만 진행
	CheckFalse(*bEquipped);

	// 적일 경우 Decal(마우스 위치)을 보여줄 필요가 없다.
	if (UseCursorLocation() == false)
	{
		Decal->SetVisibility(false);

		return;
	}

	FVector location;
	FRotator rotator;

	if (GetCursorLocationAndRotation(location, rotator))
	{
		Decal->SetVisibility(true);
		// 커서가 리턴해 주는 위치에 세팅
		Decal->SetWorldLocation(location);
		Decal->SetWorldRotation(rotator);

		return;
	}
	else
	{
		// 충돌하는 위치가 없다면 보이지 않도록 처리
		Decal->SetVisibility(false);
	}
}

bool ACDoAction_Warp::UseCursorLocation()
{
	// 판단은 간단함(AAIController를 가졌다면 Enemy이다.)
	// NULL인 경우 플레이어로 판단
	return OwnerCharacter->GetController<AAIController>() == NULL;
}

 

 

CBehaviorComponent.h 추가된 내용


더보기
...

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class UONLINE_04_ACTION_API UCBehaviorComponent : public UActorComponent
{
	GENERATED_BODY()
		
private:
	...
    
	UPROPERTY(EditAnywhere)
		FName WarpKey = "Warp"; // Warp 키
        
public:
	...
	// Warp 이동할 위치 구해줌
	FVector GetWarpLocation();
};

 

 

 

 

 

 

 

CBehaviorComponent.cpp 추가된 내용


더보기
...

FVector UCBehaviorComponent::GetWarpLocation()
{
	// 블랙보드에 있는 Vector Warp 리턴
	return Blackboard->GetValueAsVector(WarpKey);
}

 

 

CEnvQueryContext_Player.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "EnvironmentQuery/EnvQueryContext.h"
#include "CEnvQueryContext_Player.generated.h"

UCLASS()
class UONLINE_04_ACTION_API UCEnvQueryContext_Player : public UEnvQueryContext
{
	GENERATED_BODY()

private:
	// BP에서는 4가지(위치를 리턴할지, 액터를 리턴할지, 위치들을 리턴할지, 액터들을 리턴할지)가 있었음(여기서는 그 4가지로 나뉘지 않고 한가지로 통합되어 있음)
	virtual void ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const override;

};

 

 

 

 

 

 

 

CEnvQueryContext_Player.cpp


더보기
#include "CEnvQueryContext_Player.h"
#include "Global.h"
#include "Characters/CEnemy_AI.h"
#include "Characters/CPlayer.h"
#include "EnvironmentQuery/EnvQueryTypes.h" // 얘를 이용하여 FEnvQueryInstance안에 객체(.Owner)를 얻어옴
#include "EnvironmentQuery/Items/EnvQueryItemType_Actor.h" // Actor 하나만 리턴할거여서(Actor로 헤더 추가, 다른타입으로 리턴한다면 ex)Location을 헤더에 붙임)
#include "Components/CBehaviorComponent.h"

void UCEnvQueryContext_Player::ProvideContext(FEnvQueryInstance& QueryInstance, FEnvQueryContextData& ContextData) const
{
	Super::ProvideContext(QueryInstance, ContextData);

	// QueryInstance : 얘를 질의하는 Instance(즉, AIController)
	// Owner는 AIController의 Pawn이 됨
	ACEnemy_AI* ai = Cast<ACEnemy_AI>(QueryInstance.Owner.Get());

	// QueryInstance에도 Controller가 있지만 꺼내기 까다로움
	// 이렇게 하는게 좋다.
	UCBehaviorComponent* behavior = CHelpers::GetComponent<UCBehaviorComponent>(ai->GetController());


	// UEnvQueryItemType_Actor은 리턴할 타입을 의미하며
	// Actors, Location, Locations를 포함해 BP에서처럼 총 4개의 종류가 있다.
	// SetContextHelper()에다가 Actor를 줘서 플레이어를 리턴시키기 위함
	if (!!behavior->GetTargetPlayer())
		UEnvQueryItemType_Actor::SetContextHelper(ContextData, behavior->GetTargetPlayer());
}

 

 

CBTTaskNode_Change.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "Components/CActionComponent.h"
#include "CBTTaskNode_Change.generated.h"

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

private:
	UPROPERTY(EditAnywhere)
		EActionType Type; // 어떤 무기로 바뀔지 지정
	
public:
	UCBTTaskNode_Change();

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

public:
	// 무기교체에 대한 시간이 필요하므로
	virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;

};

 

 

 

 

 

 

 

CBTTaskNode_Change.cpp


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


UCBTTaskNode_Change::UCBTTaskNode_Change()
{
	bNotifyTick = true;

	NodeName = "Change";
}

EBTNodeResult::Type UCBTTaskNode_Change::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); 

	if (Type == EActionType::Warp)
	{
		if (action->IsWarpMode() == false)
			action->SetWarpMode();
	}
	else if (Type == EActionType::IceBall)
	{
		if (action->IsIceBallMode() == false)
			action->SetIceBallMode();
	}

	return EBTNodeResult::InProgress;
}

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

	// OwnerComp : 이놈을 실행한 BehaviorTreeComponent
	ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());

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

	// ExcuteTask()에서 모드를 바꾸어주었고, Idle 모드가 되었다면 모드가 정상적으로 바뀐것이다.
	if (state->IsIdleMode())
		FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}

 

 

CActionComponent.h 추가된 내용


더보기
...

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

public:	
	...
	void DestroyAllActions();
};

 

 

 

 

 

 

 

CActionComponent.cpp 추가된 내용


더보기
...

void UCActionComponent::DestroyAllActions()
{	
	for (UCAction* data : Datas)
	{
		if (!!data == false)
			continue;

		// 즉, 있다면 다 제거한다.
		if (!!data->GetAttachment())
			data->GetAttachment()->Destroy();

		if (!!data->GetEquipment())
			data->GetEquipment()->Destroy();

		if (!!data->GetDoAction())
			data->GetDoAction()->Destroy();
	}
}

 

 

 

 

 

 

CEnemy.cpp 수정된 내용


더보기
...

void ACEnemy::Begin_Dead()
{
	// 충돌체를 꺼줌(다시 맞으면 안됨, 무기도 꺼야해고, 자기의 캡슐도 꺼야함)
	// 즉, 캐릭터의 캡슐 충돌체와 함께, Enemy 무기의 모든 충돌체도 꺼줘야 한다.
	// 충돌체 전부 꺼줌
	Action->OffAllCollision();
	
	// 모든 무기 제거
	Action->DestroyAllActions();

	// 자신의 캐릭터 캡슐 컴포넌트(Collision을 꺼줌)
	GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}

 

 

CPlayer.h 추가된 내용


더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "Components/CStateComponent.h"
#include "Characters/ICharacter.h"
#include "GenericTeamAgentInterface.h"
#include "CPlayer.generated.h"

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

	...
    
public:	
	...
	float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;


private:
	void Hitted();
	void Dead();


public:
	virtual void Begin_Dead() override;
	virtual void End_Dead() override;
    
    ...


private:
	// 공격한 놈의 컨트롤러
	class AController* DamageInstigator;
	// 데미지 양
	float DamageValue;
};

 

 

 

 

 

 

 

CPlayer.cpp 수정된 내용


더보기
...

// 얘는 상태 바뀔때 마다 콜됨
void ACPlayer::OnStateTypeChanged(EStateType InPrevType, EStateType InNewType)
{	
	// 상태에 따라 처리가능
	// Begin()은 여기서 처리해주고, End()함수들은 Notify에서 콜되도록 할 것이다.
	switch (InNewType)
	{
	case EStateType::Roll: Begin_Roll(); break;
	case EStateType::Backstep: Begin_Backstep(); break;
	case EStateType::Hitted: Hitted(); break;
	case EStateType::Dead: Dead(); break;
	}
}

float ACPlayer::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	DamageInstigator = EventInstigator;
	DamageValue = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

	State->SetHittedMode();

	return Status->GetHealth();
}

void ACPlayer::Hitted()
{
	Status->SubHealth(DamageValue);
	DamageValue = 0.0f;

	if (Status->GetHealth() <= 0.0f)
	{
		State->SetDeadMode();

		return;
	}

	Montages->PlayHitted();
}

void ACPlayer::Dead()
{
	// 재동작 방지
	CheckFalse(State->IsDeadMode());

	Montages->PlayDead();
}

void ACPlayer::Begin_Dead()
{
	Action->OffAllCollision();
	Action->DestroyAllActions();
}


void ACPlayer::End_Dead()
{
	/*
	namespace EQuitPreference
	{
	enum Type
	{
		Quit, // 종료시킬 건지
		Background, // Background로 돌릴건지
	};
	}
	*/
	
	// 사망하면 게임을 종료시킴
	// UKismetSystemLibrary::QuitGame() : 게임 종료 함수
	// 4번째 인자, bool bIgnorePlatformRestrictions : 플랫폼 폼에 따라 다르게 처리할건지(우리는 false)
	UKismetSystemLibrary::QuitGame(GetWorld(), GetController<APlayerController>(), EQuitPreference::Quit, false);
}

 

 

 

 

결과