Fantap 포트폴리오
장르: 쿼터뷰 로그라이크
Enemy 담당
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의 기본이 되는 코드이다.


(왼쪽) Enemy의 기본 설정값을 정할 수 있게 만들었다.
(오른쪽) Player와 겹치는 부분이 있어 Hp, Power, Revival Count 등은 공유하기로 하였다.(Attaribute Comp)


이런식으로 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의 상태를 관리한다.


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

기본몬스터 트리이다.

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

엘리트 몬스터 트리이다.

중간보스 트리이다.

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

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

보스몬스터 트리이다.
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

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

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

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

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

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

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

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

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

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

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




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

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

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

#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
몬스터에 쉴드를 구현해보았다.