포트폴리오

Fantap 포트폴리오

ksw8596 2024. 9. 19. 11:39

 

장르: 쿼터뷰 로그라이크

 

Enemy 담당

 

 

EnemyBase

EnemyAIController

BehaviorTree

EQS System

Projectile

 

EnemyBase

#include "Enemy/FEnemyCharacterBase.h"
#include "Enemy/FEnemyAIController.h"
#include "BrainComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/Character.h"
#include "Components/CapsuleComponent.h"
#include "Skill/Action/FActionComponent.h"
#include "Player/FAttributeComponent.h"
#include "Buff/FBuffComponent.h"
#include "UI/Alarm/FDamageUI.h"
#include "UI/Alarm/FTextRenderActor.h"
#include "Perception/AISense_Damage.h"
#include "Blueprint/UserWidget.h"
#include "Components/ProgressBar.h"
#include "FGameFunctionLibrary.h"
#include "NiagaraComponent.h"
#include "NiagaraFunctionLibrary.h"
#include "Sound/SoundBase.h"
#include "UI/FEnemyHealthUI.h"
#include "UI/FWorldUI.h"
#include "FGameInstance.h"


AFEnemyCharacterBase::AFEnemyCharacterBase()
{
	PrimaryActorTick.bCanEverTick = true;

	//Pawn
	AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
	GetCharacterMovement()->bUseControllerDesiredRotation = true;

	GetMesh()->SetRelativeLocation(FVector(0.0, 0.0, -90.0));
	GetMesh()->SetRelativeRotation(FRotator(0.0, -90.0, 0.0));
	//Collision
	GetMesh()->SetCollisionProfileName(TEXT("NoCollision"));

	ProjectileSpawnPoint = CreateDefaultSubobject<USceneComponent>(TEXT("ProjectileSpawnPoint"));
	ProjectileSpawnPoint->SetupAttachment(GetMesh());
	/*ProjectileSpawnPoint->SetRelativeLocation(FVector(0.0, 120.0, 70.0));
	ProjectileSpawnPoint->SetRelativeRotation(FRotator(0.0, 90.0, 0.0));*/

	//Component
	ActionComp = CreateDefaultSubobject<UFActionComponent>(TEXT("ActionComp"));
	AttributeComp = CreateDefaultSubobject<UFAttributeComponent>(TEXT("AttributeComp"));
	BuffComp = CreateDefaultSubobject<UFBuffComponent>(TEXT("BuffComp"));
	DamageUIComponent = CreateDefaultSubobject<UFDamageUI>(TEXT("UFDamageUI"));
}

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

	CombatType = AttributeComp->CharacterStatusInfo.CombatType;

	//ProjectileSpawnPoint를 Socket에 부착
	if (ProjectileSpawnPoint->AttachToComponent(GetMesh(), FAttachmentTransformRules::KeepRelativeTransform, TEXT("Attack_pos_head")))
	{
		ProjectileSpawnPoint->AttachToComponent(GetMesh(), FAttachmentTransformRules::KeepRelativeTransform, TEXT("Attack_pos_head"));
	}
	else
	{
		ProjectileSpawnPoint->AttachToComponent(GetMesh(), FAttachmentTransformRules::KeepRelativeTransform);
	}

	AttributeComp->OnHealthChanged.AddDynamic(this, &AFEnemyCharacterBase::OnHealthChanged);

	HealthRatio = AttributeComp->GetCurrentHealth() / AttributeComp->GetMaxHealth();

	if (DamageUIComponent) {
		DamageUIComponent->Initialize(GetWorld(), 6);
	}
}

// Called every frame
void AFEnemyCharacterBase::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	HealthRatio = AttributeComp->GetCurrentHealth() / AttributeComp->GetMaxHealth();
}

===============중략===============

//WalkSpeed
float AFEnemyCharacterBase::UpdateWalkSpeed(EEnemySpeed SpeedType)
{
	switch (SpeedType)
	{
	case EEnemySpeed::Idle:
		Speed = SpeedInfo.IdleSpeed;
		break;
	case EEnemySpeed::Walking:
		Speed = SpeedInfo.WalkingSpeed;
		break;
	case EEnemySpeed::Running:
		Speed = SpeedInfo.RunningSpeed;
		break;
	default:
		break;
	}

	return GetCharacterMovement()->MaxWalkSpeed = Speed;
}

//Rise시 Anim
void AFEnemyCharacterBase::Rise()
{
	if (bRise)
	{

		UAnimInstance* Anim = GetMesh()->GetAnimInstance();
		if (Anim)
		{
			UGameplayStatics::PlaySoundAtLocation(GetWorld(), RiseSound, GetActorLocation());
			Anim->Montage_Play(RiseAni);
		}
	}
	else
	{
		return;
	}
}

//Hit 당했을 때 Anim
void AFEnemyCharacterBase::Hit()
{
	UAnimInstance* Anim = GetMesh()->GetAnimInstance();
	if (Anim)
	{
		Anim->Montage_Play(HitAni);
	}
}

//Death 상태 시 Anim
void AFEnemyCharacterBase::Death()
{
	UAnimInstance* Anim = GetMesh()->GetAnimInstance();

	if (Anim)
	{
		bUseControllerRotationYaw = false;
		GetCharacterMovement()->bUseControllerDesiredRotation = false;
		//생명주기를 부여, 자폭형 Enemy인 경우
		if (bBoom)
		{
			//UFGameFunctionLibrary::ApplyDamage(GetInstigator(), OtherActor, Damage, buffArray);
			ActionComp->StartActionByName(this, "Death");
			SpawnDeathDrops();
		}
		else
			SetLifeSpan(Anim->Montage_Play(DeathAni) + 1.5f);
	}
}

void AFEnemyCharacterBase::HitLogic()
{
	//Behavior Tree를 Restart명령을 내림
	AFEnemyAIController* EnemyController = Cast<AFEnemyAIController>(GetController());
	if (EnemyController)
	{
		EnemyController->BrainComponent->RestartLogic();
	}
}

void AFEnemyCharacterBase::DeathLogic()
{
	AFEnemyAIController* EnemyController = Cast<AFEnemyAIController>(GetController());
	if (EnemyController)
	{
		GetMovementComponent()->StopMovementImmediately();
		//현재 BehaviorTree를 멈추고
		EnemyController->BrainComponent->StopLogic("");
		EnemyController->BrainComponent->PauseLogic("");
		//Controller에 할당된 Focus를 초기화
		EnemyController->ClearFocus(EAIFocusPriority::Gameplay);
	}
}

//Damaged
void AFEnemyCharacterBase::OnHealthChanged(AActor* InstigatorActor, UFAttributeComponent* OwningComp, float Delta, float NewHealth)
{
	//죽을 시 바로 빠져나감
	if (bDeath)
	{
		return;
	}

	AFEnemyAIController* EnemyController = Cast<AFEnemyAIController>(GetController());

	int32 RevivalCount = AttributeComp->CharacterStatusInfo.RevivalCount;
	//AISense에 데미지 받은 것을 알림
	UAISense_Damage::ReportDamageEvent(GetWorld(), InstigatorActor, UGameplayStatics::GetPlayerCharacter(GetWorld(), 0), Delta, InstigatorActor->GetActorLocation(), GetActorLocation());

	if (Delta < 0)
	{
		FVector MeshLocation = GetMesh()->GetComponentLocation();
		FVector Origin;
		FVector BoxExtent;
		GetMesh()->GetOwner()->GetActorBounds(true, Origin, BoxExtent);

		FVector TopOfMesh = Origin + FVector(0, 0, BoxExtent.Z);
		FVector DamageTextLocation = TopOfMesh + FVector(0, 0, 30);

		FString DamageText = FString::Printf(TEXT("%.0f"), -Delta);
		FColor TextColor = FColor::Red;
		float TextSize = 50.0f;
		

		if (DamageUIComponent)
		{
			UE_LOG(LogTemp, Warning, TEXT("Displaying damage text..."));
			DamageUIComponent->DisplayDamage(DamageTextLocation, DamageText, TextColor, TextSize);
		}
	}

	if (AttributeComp->IsAlive())
	{
		//Super Armor상태가 아니면
		if (!bSuperArmor)
		{
			//GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::Red, TEXT("SuperArmor Off"));
			if (EnemyController)
			{
				GetMovementComponent()->StopMovementImmediately();
				ActionComp->StopAllActions(this);
				EnemyController->BrainComponent->StopLogic("");
			}
      
			Hit();

			FTimerHandle TimerHandle;
			GetWorldTimerManager().SetTimer(TimerHandle, this, &AFEnemyCharacterBase::HitLogic, 1.0f, false);
		}

		if (ActivatedHealthUI == nullptr)
		{
			ActivatedHealthUI = CreateWidget<UFEnemyHealthUI>(GetWorld(), HealthUI);
			if (ActivatedHealthUI)
			{
				ActivatedHealthUI->AttachedActor = this;
				ActivatedHealthUI->AddToViewport();
			}
		}

		if (ActivatedHealthUI)
		{
			HealthRatio = AttributeComp->GetCurrentHealth() / AttributeComp->GetMaxHealth();
			ActivatedHealthUI->HealthBar->SetPercent(HealthRatio);
		}
	}
    //목숨이 존재한 경우
	else if (!AttributeComp->IsAlive() && RevivalCount > 0)
	{
		return;
	}
    //Death 상태 시
	else
	{
		DeathLogic();
		bDeath = true;
		ActionComp->StopAllActions(this);

		//공중사망 방지
		GetCharacterMovement()->SetMovementMode(MOVE_Walking);

		GetCapsuleComponent()->SetCollisionResponseToChannel(ECC_GameTraceChannel2, ECR_Ignore);

		//Death Animation
		if (bBoom)
		{
			//이팩트
			ActionComp->StartActionByName(this, "BoomTimer");

			FTimerHandle EnemyTimer;
			//죽음
			GetWorldTimerManager().SetTimer(EnemyTimer, this, &AFEnemyCharacterBase::Death, BoomTimer, false);
		}
		else
		{
			SpawnDeathDrops();
			Death();
		}

		if (ActivatedHealthUI)
		{
			ActivatedHealthUI->RemoveFromParent();
		}
	}
}

FEnemyCharacterBase.cpp

 

Enemy의 기본이 되는 코드이다.

BP_EnemyBase

 

(왼쪽) Enemy의 기본 설정값을 정할 수 있게 만들었다. 

(오른쪽) Player와 겹치는 부분이 있어 Hp, Power, Revival Count 등은 공유하기로 하였다.(Attaribute Comp)

Enemy Base의 자식객체들이다.

 

이런식으로 Enemy에 설정 값을 바꾸게끔 만들어 재사용성을 높였다.

 

[위로 이동하기]

EnemyAIController

#include "Enemy/FEnemyAIController.h"
#include "Enemy/FEnemyCharacterBase.h"
#include "Kismet/KismetMathLibrary.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Player/FAttributeComponent.h"

AFEnemyAIController::AFEnemyAIController()
{
	SetPerceptionSystem();
}

void AFEnemyAIController::OnPossess(APawn* InPawn)
{
	Super::OnPossess(InPawn);
	RunBehaviorTree(BehaviorTree);

	AFEnemyCharacterBase* Enemy = Cast<AFEnemyCharacterBase>(InPawn);
	Self = Enemy;
    
    auto CombatType = Enemy->CombatType;
    auto Building = Enemy->bBuilding;

    //Blackboard 변화하지않는 초기값 결정
    GetBlackboardComponent()->SetValueAsEnum(TEXT("CombatType"), static_cast<uint8>(CombatType));

    GetBlackboardComponent()->SetValueAsBool(TEXT("BuildingEnemy?"), Building);

	//Rise가 체크되었을 때 (대기하는 모션이 존재하는 경우)
	if (Enemy->bRise)
	{
		SetRising();
	}
	else
	{
		SetPassiving();
	}
	PerceptionComp->OnTargetPerceptionUpdated.AddDynamic(this, &AFEnemyAIController::OnTargetDetected);
}

//부드러운 회전을 위한 Smooth함수
void AFEnemyAIController::UpdateControlRotation(float DeltaTime, bool bUpdatePawn)
{
	Super::UpdateControlRotation(DeltaTime, false);

	if (bUpdatePawn)
	{
		APawn* const MyPawn = GetPawn();
		const FRotator CurrentPawnRotation = MyPawn->GetActorRotation();

		//Calculate smoothed rotation
		SmoothTargetRotation = UKismetMathLibrary::RInterpTo_Constant(MyPawn->GetActorRotation(), ControlRotation, DeltaTime, SmoothFocusInterpSpeed);
		//Check if we need to change
		if (CurrentPawnRotation.Equals(SmoothTargetRotation, 1e-3f) == false)
		{
			//Change rotation using the Smooth Target Rotation
			MyPawn->FaceRotation(SmoothTargetRotation, DeltaTime);
		}
	}
}

void AFEnemyAIController::OnTargetDetected(AActor* Actor, FAIStimulus const Stimulus)
{
	AFEnemyCharacterBase* Enemy = Cast<AFEnemyCharacterBase>(Self);
	int32 Revival = Enemy->AttributeComp->CharacterStatusInfo.RevivalCount;
	//Tag컴포넌트화되면 변경
	
	//Tag가 Player이고 Player를 봤을때
	if (Actor->ActorHasTag("Player"))
	{
		TargetActor = Actor;
		//Blackboard에 값을 넣어준다.
		if(Stimulus.WasSuccessfullySensed())
		{
			SetAttacking(Actor);
		}
	}

	//보스의 경우 페이즈를 바꿔주는 부분
	if (Enemy->bBoss && Revival > 0)
	{
		SetPhase1();
	}
	else if (Enemy->bBoss && Revival == 0)
	{
		SetPhase2();
	}
}

//PerceptionSystem
void AFEnemyAIController::SetPerceptionSystem()
{
	//AI Perception System
	PerceptionComp = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("PerceptionComp"));
	SetPerceptionComponent(*PerceptionComp);

	SetSightConfigInfo();
	SetDamageConfigInfo();

	PerceptionComp->ConfigureSense(*SightConfig);
	PerceptionComp->ConfigureSense(*DamageConfig);

	PerceptionComp->SetDominantSense(*SightConfig->GetSenseImplementation());
}

//시야에 대한 PerceptionSystem
void AFEnemyAIController::SetSightConfigInfo()
{
	SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("Sight Config"));

	SightConfig->SightRadius = AISightRadius;
	SightConfig->LoseSightRadius = AILoseSightRadius;
	SightConfig->PeripheralVisionAngleDegrees = AIFieldOfView;
	SightConfig->SetMaxAge(AISightAge);

	SightConfig->DetectionByAffiliation.bDetectEnemies = true;
	SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
	SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
}

//데미지 받았을 때에 대한 PerceptionSystem
void AFEnemyAIController::SetDamageConfigInfo()
{
	DamageConfig = CreateDefaultSubobject<UAISenseConfig_Damage>(TEXT("Damage Config"));
	
	DamageConfig->SetMaxAge(0.0f);
	DamageConfig->SetStartsEnabled(true);
}

//Blackboard에 상태를 전달하는 함수들
void AFEnemyAIController::SetRising()
{
	GetBlackboardComponent()->SetValueAsEnum(TEXT("StateType"), 0);
	GetBlackboardComponent()->SetValueAsBool(TEXT("Rise"), true);
}

void AFEnemyAIController::SetPassiving()
{
	GetBlackboardComponent()->SetValueAsEnum(TEXT("StateType"), 1);
	GetBlackboardComponent()->SetValueAsBool(TEXT("Stop"), false);
}

void AFEnemyAIController::SetAttacking(AActor* Actor)
{
	GetBlackboardComponent()->SetValueAsEnum(TEXT("StateType"), 2);
	GetBlackboardComponent()->SetValueAsObject(TEXT("TargetActor"), Actor);
}

void AFEnemyAIController::SetPhase1()
{
	GetBlackboardComponent()->SetValueAsEnum(TEXT("PhaseType"), 0);
}

void AFEnemyAIController::SetPhase2()
{
	GetBlackboardComponent()->SetValueAsEnum(TEXT("PhaseType"), 1);
}

EnemyAIController.cpp

 

AIController부분으로 Enemy의 상태를 관리한다.

BP_EnemyAIController

EnemyController마다 Behavior Tree와 AI Perception의 Sight Radius가 다르므로 Basic, Elite 등으로 나누었다.

 

[위로 이동하기]

Behavior Tree

BT_BasicEnemy

 

기본몬스터 트리이다.

SubTree_Passiving

기본, 엘리트 보스에 들어가는 Passiving 트리이다.

 

BT_EliteEnemy

 

엘리트 몬스터 트리이다.

 

BT_MidBossEnemy

 

중간보스 트리이다.

 

SubTree_MidBoss_Pattern

 

체력이 30%이하시 패턴을 강화시키는 트리이다.

 

SubTree_MidBoss_Attack

 

기본공격과 일반 패턴공격을 하는 트리이다.

 

BT_Boss

 

보스몬스터 트리이다.

 

BTTask, BTDecorator, BTService

#include "Enemy/BTTask/FBTD_CantSeeTarget.h"
#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Kismet/KismetSystemLibrary.h"

UFBTD_CantSeeTarget::UFBTD_CantSeeTarget()
{
	NodeName = "BTD_CantSeeTarget";
}

bool UFBTD_CantSeeTarget::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
	AAIController* Owner = OwnerComp.GetAIOwner();
	APawn* ControllPawn = Owner->GetPawn();
	UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
	AActor* TargetActor = Cast<AActor>(BlackboardComp->GetValueAsObject(TEXT("TargetActor")));
	
	FVector Start = ControllPawn->GetActorLocation();
	FVector End = TargetActor->GetActorLocation();
	TArray<AActor*> IgnorActions;
	FHitResult HitResult;

	//LineTrace를 쏴 Target이 맞는지 체크
	bool isHit = UKismetSystemLibrary::LineTraceSingle(GetWorld(), Start, End, ETraceTypeQuery::TraceTypeQuery1, false, IgnorActions, EDrawDebugTrace::None, HitResult, true, FLinearColor::Red, FLinearColor::Green, 5.0f);

	if (bTarget)
	{
		return !isHit;
	}
	else
	{
		return isHit;
	}
}

BTD_CantSeeTarget.cpp

 

타겟이 Trace에 있는지를 체크하는 BTD로 공격시 Player가 벽에 가려져있는지를 확인하기위해 만들었다.

 

#include "Enemy/BTTask/FBTS_RandomFloat.h"
#include "BehaviorTree/BlackboardComponent.h"

UFBTS_RandomFloat::UFBTS_RandomFloat()
{
	NodeName = "BTS_RandomFloat";

	bNotifyBecomeRelevant = true;
	Interval = 1.0f; // 1초마다 실행
	RandomDeviation = 0.1f; // 0.1초의 랜덤 오차
}

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

	UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();

	if (BlackboardComp)
	{
		float RandomValue = FMath::RandRange(0.0f, 1.0f);
		BlackboardComp->SetValueAsFloat(RandomKey, RandomValue);
	}
}

BTS_RandomFloat.cpp

 

랜덤으로 Float값을 받아오는 BTS로 몇몇 공격을 확률로 실행하게 만들었다.

 

#include "Enemy/BTTask/FBTS_UpdateDistance.h"
#include "AIController.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Skill/Action/FActionComponent.h"

UFBTS_UpdateDistance::UFBTS_UpdateDistance()
{
	NodeName = "BTS_UpdateDistance";

	bNotifyBecomeRelevant = true;
	Interval = 0.1f; // 0.1초마다 실행
	RandomDeviation = 0;
}

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

	AAIController* Owner = OwnerComp.GetAIOwner();
	APawn* ControllPawn = Owner->GetPawn();
	UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();

	AActor* TargetActor = Cast<AActor>(BlackboardComp->GetValueAsObject(TEXT("TargetActor")));

	FVector V1 = ControllPawn->GetActorLocation();
	FVector V2 = TargetActor->GetActorLocation();
	
	float Distance = FVector::Distance(V1, V2);

	//Target과의 거리를 계산
	if (BlackboardComp)
	{
		BlackboardComp->SetValueAsFloat(DistanceName, Distance);
	}
}

BTS_UpdateDistance.cpp

 

Player와의 거리값을 계산하기위해 만든 BTS이다.

 

#include "Enemy/BTTask/FBTT_ChasePlayer.h"
#include "Enemy/FEnemyCharacterBase.h"
#include "Enemy/FEnemyAIController.h"

UFBTT_ChasePlayer::UFBTT_ChasePlayer()
{
	NodeName = "BTT_ChasePlayer";
}

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

	AAIController* Owner = OwnerComp.GetAIOwner();
	AFEnemyCharacterBase* Enemy = Cast<AFEnemyCharacterBase>(Owner->GetPawn());

	if (Enemy)
	{
		Enemy->UpdateWalkSpeed(SpeedType);
		return EBTNodeResult::Succeeded;
	}
	else
	{
		return EBTNodeResult::Failed;
	}
}

BTT_ChasePlayer.cpp

 

Enemy의 Speed값을 변경하기위한 BTT로 Player를 발견하면 이동속도를 바꾸기위한 함수이다.

 

#include "Enemy/BTTask/FBTT_SuperArmor.h"
#include "Enemy/FEnemyCharacterBase.h"
#include "AIController.h"

UFBTT_SuperArmor::UFBTT_SuperArmor()
{
	NodeName = "BTT_SuperArmor";
}

EBTNodeResult::Type UFBTT_SuperArmor::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	AAIController* Owner = OwnerComp.GetAIOwner();
	AFEnemyCharacterBase* Enemy = Cast<AFEnemyCharacterBase>(Owner->GetPawn());

	if (Enemy)
	{
		Enemy->bSuperArmor = bSuperArmor;
		return EBTNodeResult::Succeeded;
	}
	else
	{
		return EBTNodeResult::Failed;
	}
}

BTT_SuperArmor.cpp

 

   Enemy에 SuperArmor가 활성화시키는 함수로 공격 시에 SuperArmor를 실행하여 캔슬되지않게 만들었다.(Elite이상 몬스  터 한하여)

 

  그밖에 BTT함수들이 존재하고 Focus를 주고 해제, SuperArmor 해제, 현재 실행중인 애니메이션을 실행 종료시키는 BTT가 존재한다.

 

Blueprint

BTD_AllowToRun

1번만 실행하는 BTD를 만들었다.

 

BTS_HealthUpdate

 

체력을 업그레이드 하는 BTS이다.

 

BTS_HealthHeal

 

보스에 사용하는 BTS로 2페이즈를 들어갈 때 체력을 100%를 유지한다.

 

BTT_Action_Skill

 

Action을 실행하는 BTT이다. (Action은 Animation을 관리하는 Component이다.)

 

BTT_CombatType

 

기본 몬스터의 근거리와 원거리를 표시하기위한 BTT이다.

 

BTT_FindRandomPatrol

 

Nav안에서 랜덤한 값을 가지고 움직이게 하는 함수이다.

 

BTT_isBuilding

 

고정형 몬스터인지를 판별하는 BTT이다.

 

[위로 이동하기]

EQS System

EQS_Context_AttackTarget

EQS System을 Target에 부착한다.

(위 Print함수는 Test용으로 EQS의 범위를 레벨에 표시하여 원할한 작업을 위해 만들었다.)

 

EQS_MinDistance

Target과의 거리가 가까운경우 멀어지게 하는 EQS이다.

 

EQS_FindCover

 

Target의 주변을 제외하고 거리가 100~ 300사이를 움직이게 하는 EQS이다.

[위로 이동하기]

Projectile

AC_ObjectPool

 

BP_PooledActor

 

Object Pooling 기술을 사용하였다. (Projectile을 미리 생성)

 

BP_WaterTornado

 

   Object Pooling을 적용한 Projectile로 여러개의 물폭풍을 생성하여 중간보스에서 회오리가 360방향으로 나아가는 공격이다.

 

BP_WaterCannon

 

중간보스가 물대포를 발사할 때 쓰는 Actor로 시간에 따라 벽을 계산, 그 값에 따라 대포의 거리를 나타냈다.

 

BP_FireBreath

 

#include "Enemy/FEnemyAOE.h"
#include "Components/SphereComponent.h"
#include "FGameFunctionLibrary.h"

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

	SphereComp = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComponent"));
	SetRootComponent(SphereComp);

	SphereComp->OnComponentBeginOverlap.AddDynamic(this, &AFEnemyAOE::OnComponentBeginOverlap);
	SphereComp->OnComponentEndOverlap.AddDynamic(this, &AFEnemyAOE::OnComponentEndOverlap);
}

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

	FTimerHandle TimerHandle;
    //0.5초마다 TickDamage를 무한히 실행한다.
	GetWorldTimerManager().SetTimer(TimerHandle, this, &AFEnemyAOE::TickDamage, 0.5f, true);
	//5초뒤 삭제
    SetLifeSpan(5.0f);
}

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

}

//Player가 Overlap했는지 아닌지를 판별
void AFEnemyAOE::OnComponentBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	if (OtherActor->ActorHasTag(TEXT("Player")))
	{
		bPlayerCome = true;
		TargetActor = OtherActor;
	}
}

void AFEnemyAOE::OnComponentEndOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	if (OtherActor->ActorHasTag(TEXT("Player")))
	{
		bPlayerCome = false;
	}
}

//TickDamage
void AFEnemyAOE::TickDamage()
{
	if (bPlayerCome)
	{
		TArray<TSubclassOf<class UFBuff>> BuffClassesToApply;
		UFGameFunctionLibrary::ApplyDamage(this, TargetActor, 2.0f, BuffClassesToApply);
	}
}

EnemyAOE.cpp


현재는 화염장판에 쓰이고 있으나 나중에는 다른곳에도 쓸 수 있게 변경할 예정이다.

 

기타 기능

#include "Enemy/FShield.h"
#include "GameFramework/Character.h"
#include "GameFramework/PlayerController.h"
#include "Kismet/KismetSystemLibrary.h"
#include "FGameFunctionLibrary.h"
#include "Player/FAttributeComponent.h"
#include "Buff/FBuffComponent.h"

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

	StaticMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ShiledMesh"));
	StaticMesh->OnComponentBeginOverlap.AddDynamic(this, &AFShield::OnComponentBeginOverlap);

	AttributeComp = CreateDefaultSubobject<UFAttributeComponent>(TEXT("AttributeComp"));
	BuffComp = CreateDefaultSubobject<UFBuffComponent>(TEXT("BuffComp"));
}

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

	SetLifeSpan(LifeTime);

	FTimerHandle TimerHandle;
    //Overlap상태를 Black상태로 변환시킨다.
	GetWorldTimerManager().SetTimer(TimerHandle, this, &AFShield::ChangeBlack, 0.2f, false);

	AttributeComp->OnHealthChanged.AddDynamic(this, &AFShield::OnHealthChanged);

	//데미지형 쉴드인지를 판별한다.
	if (bDamageShield)
	{
		GetWorldTimerManager().SetTimer(TimerHandle, this, &AFShield::OnDamage, BoomTimer, false);
	}
}

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

}

void AFShield::OnHealthChanged(AActor* InstigatorActor, UFAttributeComponent* OwningComp, float Delta, float NewHealth)
{
	if (!AttributeComp->IsAlive())
	{
		Destroy();
	}
}

//쉴드가 터질때 데미지 주는 함수
void AFShield::OnDamage()
{
	if (AttributeComp->IsAlive())
	{
		FVector Start = GetActorLocation();
		FVector ForwardVector = GetActorForwardVector();
		FVector End = Start + ForwardVector; 

		TArray<FHitResult> OutHits;

		float Radius = 1000.0f;

		TArray<AActor*> ActorsToIgnore;
		ActorsToIgnore.Add(this);
		TArray<TEnumAsByte<EObjectTypeQuery>> ObjectTypes;
		ObjectTypes.Add(UEngineTypes::ConvertToObjectType(ECollisionChannel::ECC_Pawn));

		bool isHit = UKismetSystemLibrary::SphereTraceMultiForObjects(GetWorld(),Start, End, Radius, ObjectTypes, false, ActorsToIgnore, EDrawDebugTrace::None, OutHits, true);
		if (isHit)
		{
			for(auto&Hit:OutHits)
			{
				AActor* HitActor = Hit.GetActor();
				if (HitActor)
				{
					UActorComponent* TargetComponent = HitActor->GetComponentByClass(UFAttributeComponent::StaticClass());
					if (IsValid(TargetComponent))
					{
						TArray<TSubclassOf<UFBuff>> buffArray;
						UFGameFunctionLibrary::ApplyDamage(GetInstigator(), HitActor, AttributeComp->GetMagicalPower()*DamageRate, buffArray);
					}
				}
			}
		}
	}
}

//Actor와 Shield와 겹쳤을때 밀어주는 로직
void AFShield::OnComponentBeginOverlap(UPrimitiveComponent* OverlappedComp, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	GEngine->AddOnScreenDebugMessage(-1, 3.0f, FColor::Blue, TEXT("Screen"));
	if(OtherActor && OtherActor != this)
	{
		PushActor(OtherActor);
	}
}

//쉴드생성시 Actor와 부딪힌경우 밀어내는 함수
void AFShield::PushActor(AActor* ActorPush)
{
	if(ActorPush)
	{
		ACharacter* Character = Cast<ACharacter>(ActorPush);

		if(Character)
		{
			FVector ActorLocation = Character->GetActorLocation();
			FVector ShiledLocation = GetActorLocation();

			FVector Distance = (ActorLocation - ShiledLocation);
			Distance.Normalize();

			if (Distance.Size() < 300)
			{
				Character->LaunchCharacter(Distance * 3000, true, true);
			}
		}
	}
}

void AFShield::ChangeBlack()
{
	StaticMesh->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Block);
}

Shiled.cpp

 

몬스터에 쉴드를 구현해보았다.

 

 

 

[위로 이동하기]