EnemySpawner
- NavMesh 위에 적 랜덤 생성
EnemyAIController
- 플레이어를 0.2초마다 탐지
- 감지되면 Blackboard.TargetActor에 저장
Behavior Tree
- TargetActor가 있으면 추적
- 공격 범위 안이면 공격 Task 실행
BTDecorator_IsInAttackRange
- 플레이어가 공격 범위 안인지 검사
BTTask_AttackTarget
- 이동 정지
- 플레이어 방향으로 회전
- 공격 애니메이션 실행
EnemyCharacter
- HP, 공격력, 탐지/공격 범위 관리
- Anim Notify 시점에 실제 데미지 적용
- 사망 시 점수 지급, 이동/충돌 비활성화
- EnemyCharacter에서 MaxHP, CurrentHP, Defense 관리
- 피격 시 방어력을 반영해 최종 데미지 계산
- TakeDamage()로 데미지 수신
- HP가 0 이하가 되면 Die() 실행
- 사망 시 점수 지급, 이동 비활성화, 충돌 제거, 사망 애니메이션 재생
- EnemyAIController에서 플레이어를 0.2초마다 탐지
- 적과 플레이어 사이의 거리를 계산
- DetectionRange 안에 있으면 Blackboard의 TargetActor에 플레이어 저장
- 범위 밖이면 TargetActor 제거
- Behavior Tree에서 TargetActor가 존재하면 추적 로직 실행
- Blackboard에 저장된 플레이어를 기준으로 이동
- NavMesh를 사용해 이동 가능한 경로를 따라 추적
- BTDecorator_IsInAttackRange에서 플레이어가 공격 범위 안에 있는지 검사
- 범위 안이면 BTTask_AttackTarget 실행
- 공격 Task에서 이동 정지
- 플레이어 방향으로 회전
- 공격 애니메이션 실행
- 실제 데미지는 애니메이션 Notify 시점에 ApplyAttackDamage()로 적용
- EnemySpawner가 NavMesh 위의 랜덤 위치를 찾아 적 생성
- AI 이동도 NavMesh 기반으로 처리
- 벽이나 이동 불가능한 영역을 피해 플레이어에게 접근
- EnemyCharacter의 CharacterMovement에 RVO Avoidance 적용
- 여러 적이 동시에 이동할 때 서로 겹치는 현상 완화
- 회피 반경을 설정해 적들이 자연스럽게 퍼져 이동하도록 처리
- 스폰할 적 클래스 EnemyClass 지정
- 스폰 개수 EnemySpawnCount 설정
- 스폰 반경 SpawnRadius 설정
- 충돌 시 스폰 실패 처리
- 무한 반복 방지를 위한 최대 스폰 시도 횟수 적용
CombatComponent.FireGrenade()
- 스킬 사용 가능 여부 확인
- 손 소켓(hand_rSocket) 위치 계산
- 카메라 기준 조준 방향 보정
- GrenadeProjectile 생성
- 데미지 / 폭발 반경 전달
CollisionComp
- 충돌 판정용 SphereComponent
MeshComp
- 외형 표시용 StaticMeshComponent
ProjectileMovement
- 속도, 중력, 바운스, 마찰 적용
FuseTimer
- 일정 시간 후 Explode 실행
Explode()
- 현재 위치에 폭발 이펙트 생성
- 폭발 사운드 재생
- ApplyRadialDamage로 범위 데미지 적용
- 투사체 Destroy
문제 1. 공격 애니메이션과 데미지 타이밍이 안 맞음
// ========== 기존 코드 ========== //
// BTTask_AttackTarget.cpp
UGameplayStatics::ApplyDamage(
TargetActor,
Enemy->AttackDamage,
AIController,
Enemy,
nullptr
);
// ========== 수정 후 코드 ========== //
// BTTask_AttackTarget.cpp
Enemy->PlayAttackAnimation();
Enemy->MarkAttack();
// EnemyCharacter.h
UFUNCTION(BlueprintCallable, Category = "Enemy|Attack")
void ApplyAttackDamage();
// EnemyCharacter.cpp
void AEnemyCharacter::ApplyAttackDamage()
{
if (bIsDead)
{
return;
}
AAIController* AIController = Cast<AAIController>(GetController());
if (!AIController)
{
return;
}
UBlackboardComponent* BlackboardComp = AIController->GetBlackboardComponent();
if (!BlackboardComp)
{
return;
}
AActor* TargetActor = Cast<AActor>(
BlackboardComp->GetValueAsObject(TEXT("TargetActor"))
);
if (!TargetActor)
{
return;
}
UGameplayStatics::ApplyDamage(
TargetActor,
AttackDamage,
AIController,
this,
nullptr
);
}
- BTTask에서 바로 데미지를 주지 않고, Anim Notify에서 ApplyAttackDamage 호출로 해결
문제 2. 적이 죽은 뒤에도 움직이거나 공격하려고 함
// ========== 기존 코드 ========== //
// EnemyCharacter.cpp
void AEnemyCharacter::Die()
{
if (bIsDead)
{
return;
}
bIsDead = true;
AShooterGameMode* GM = Cast<AShooterGameMode>(UGameplayStatics::GetGameMode(this));
if (GM)
{
GM->AddScore(ScoreValue);
}
Destroy();
}
// ========== 수정후 코드 ========== //
// BTTask_AttackTarget.cpp
AEnemyCharacter* Enemy = Cast<AEnemyCharacter>(AIController->GetPawn());
if (!Enemy || Enemy->IsDead())
{
return EBTNodeResult::Failed;
}
// BTDecorator_IsInAttackRange.cpp
const AEnemyCharacter* Enemy = Cast<AEnemyCharacter>(AIController->GetPawn());
if (!Enemy || Enemy->IsDead())
{
return false;
}
// EnemyCharacter.cpp
void AEnemyCharacter::Die()
{
if (bIsDead)
{
return;
}
bIsDead = true;
AShooterGameMode* GM = Cast<AShooterGameMode>(UGameplayStatics::GetGameMode(this));
if (GM)
{
GM->AddScore(ScoreValue);
}
GetCharacterMovement()->DisableMovement();
GetCharacterMovement()->StopMovementImmediately();
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
PlayDeathAnimation();
SetLifeSpan(2.5f);
}
- IsDead 체크 추가, 사망 시 Movement/Collision 비활성화로 해결
문제 3. 플레이어가 공격 범위 밖으로 벗어나도 맞는 문제
// ========== 기존 코드 ========== //
// BTDecorator_IsInAttackRange.cpp
const float Distance = FVector::Dist2D(
Enemy->GetActorLocation(),
TargetActor->GetActorLocation()
);
return Distance <= Enemy->AttackRange;
// ========== 수정후 코드 ========== //
// EnemyCharacter.cpp
void AEnemyCharacter::ApplyAttackDamage()
{
if (bIsDead)
{
return;
}
AAIController* AIController = Cast<AAIController>(GetController());
if (!AIController)
{
return;
}
UBlackboardComponent* BlackboardComp = AIController->GetBlackboardComponent();
if (!BlackboardComp)
{
return;
}
AActor* TargetActor = Cast<AActor>(
BlackboardComp->GetValueAsObject(TEXT("TargetActor"))
);
if (!TargetActor)
{
return;
}
const float DistanceToTarget = FVector::Dist2D(
GetActorLocation(),
TargetActor->GetActorLocation()
);
if (DistanceToTarget > AttackRange + 50.f)
{
return;
}
UGameplayStatics::ApplyDamage(
TargetActor,
AttackDamage,
AIController,
this,
nullptr
);
}
- Notify 시점에 AttackRange + 보정값으로 거리 재검사로 해결
문제 4. 여러 적이 겹쳐서 이동이 어색함
// ========== 기존 코드 ========== //
// EnemyCharacter.cpp
AEnemyCharacter::AEnemyCharacter()
{
PrimaryActorTick.bCanEverTick = false;
MaxHP = 100.f;
CurrentHP = MaxHP;
Defense = 0.f;
ScoreValue = 1;
AttackDamage = 10.f;
DetectionRange = 1200.f;
AttackRange = 150.f;
AttackCooldown = 1.5f;
bIsDead = false;
}
// ========== 수정후 코드 ========== //
// EnemyCharacter.cpp
AEnemyCharacter::AEnemyCharacter()
{
PrimaryActorTick.bCanEverTick = false;
MaxHP = 100.f;
CurrentHP = MaxHP;
Defense = 0.f;
ScoreValue = 1;
AttackDamage = 10.f;
DetectionRange = 1200.f;
AttackRange = 150.f;
AttackCooldown = 1.5f;
LastAttackTime = -AttackCooldown;
bIsDead = false;
GetCharacterMovement()->bUseRVOAvoidance = true;
GetCharacterMovement()->AvoidanceConsiderationRadius = 300.f;
bUseControllerRotationYaw = false;
GetCharacterMovement()->bOrientRotationToMovement = true;
GetCharacterMovement()->RotationRate = FRotator(0.f, 720.f, 0.f);
}
- CharacterMovement에 RVO Avoidance 적용으로 해결
문제 1. 투사체로 변경 후 발사하자마자 플레이어와 충돌하는 문제
// ========== 기존 코드 ========== //
// CombatComponent.cpp
FVector ExplodeLocation = BaseStart + (ShootDirection * 400.f);
TArray<AActor*> IgnoreActors;
IgnoreActors.Add(Owner);
UGameplayStatics::ApplyRadialDamage(
GetWorld(),
GrenadeDamage,
ExplodeLocation,
GrenadeRadius,
UDamageType::StaticClass(),
IgnoreActors,
Owner,
Owner->GetInstigatorController(),
true
);
// ========== 수정후 코드 ========== //
// CombatComponent.cpp
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = Owner;
SpawnParams.Instigator = Cast<APawn>(Owner);
SpawnParams.SpawnCollisionHandlingOverride =
ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
AGrenadeProjectile* Grenade = GetWorld()->SpawnActor<AGrenadeProjectile>(
GrenadeProjectileClass,
SpawnLocation,
ShootDirection.Rotation(),
SpawnParams
);
// GrenadeProjectile.cpp
void AGrenadeProjectile::BeginPlay()
{
Super::BeginPlay();
if (AActor* OwnerActor = GetOwner())
{
CollisionComp->IgnoreActorWhenMoving(OwnerActor, true);
}
GetWorldTimerManager().SetTimer(
FuseTimerHandle,
this,
&AGrenadeProjectile::Explode,
FuseTime,
false
);
}
- GrenadeProjectile BeginPlay에서 OwnerActor를 충돌 무시 처리로 해결
문제 2. 조준 방향과 실제 발사 방향이 어긋남
// ========== 기존 코드 ========== //
// CombatComponent.cpp
FVector ShootDirection = Owner->GetActorForwardVector();
FVector BaseStart = Owner->GetActorLocation();
if (CharOwner)
{
if (APlayerController* PC = Cast<APlayerController>(CharOwner->GetController()))
{
FVector CameraLoc;
FRotator CameraRot;
PC->GetPlayerViewPoint(CameraLoc, CameraRot);
ShootDirection = CameraRot.Vector();
BaseStart = CameraLoc;
}
}
FVector ExplodeLocation = BaseStart + (ShootDirection * 400.f);
// ========== 수정후 코드 ========== //
// CombatComponent.cpp
ACharacter* CharOwner = Cast<ACharacter>(Owner);
FVector SpawnLocation = Owner->GetActorLocation();
FVector ShootDirection = Owner->GetActorForwardVector();
if (CharOwner)
{
if (CharOwner->GetMesh()->DoesSocketExist(TEXT("hand_rSocket")))
{
SpawnLocation = CharOwner->GetMesh()->GetSocketLocation(TEXT("hand_rSocket"));
}
if (APlayerController* PC = Cast<APlayerController>(CharOwner->GetController()))
{
FVector CameraLoc;
FRotator CameraRot;
PC->GetPlayerViewPoint(CameraLoc, CameraRot);
FVector TraceStart = CameraLoc;
FVector TraceEnd = TraceStart + CameraRot.Vector() * 3000.f;
FHitResult HitResult;
FCollisionQueryParams Params;
Params.AddIgnoredActor(Owner);
bool bHit = GetWorld()->LineTraceSingleByChannel(
HitResult,
TraceStart,
TraceEnd,
ECC_Visibility,
Params
);
FVector TargetPoint = bHit ? HitResult.ImpactPoint : TraceEnd;
ShootDirection = (TargetPoint - SpawnLocation).GetSafeNormal();
}
}
SpawnLocation += ShootDirection * 40.f;
SpawnLocation += FVector(0.f, 0.f, 10.f);
AGrenadeProjectile* Grenade = GetWorld()->SpawnActor<AGrenadeProjectile>(
GrenadeProjectileClass,
SpawnLocation,
ShootDirection.Rotation(),
SpawnParams
);
- 카메라 LineTrace 기준으로 목표 지점을 계산하고 손 소켓에서 그 방향으로 발사로 해결
문제 3. 폭발 연출과 데미지 처리가 분리되어 어색함
// ========== 기존 코드 ========== //
// CombatComponent.cpp
FVector ExplodeLocation = BaseStart + (ShootDirection * 400.f);
DrawDebugSphere(
GetWorld(),
ExplodeLocation,
GrenadeRadius,
16,
FColor::Purple,
false,
2.0f,
0,
1.5f
);
TArray<AActor*> IgnoreActors;
IgnoreActors.Add(Owner);
UGameplayStatics::ApplyRadialDamage(
GetWorld(),
GrenadeDamage,
ExplodeLocation,
GrenadeRadius,
UDamageType::StaticClass(),
IgnoreActors,
Owner,
Owner->GetInstigatorController(),
true
);
// ========== 수정후 코드 ========== //
// CombatComponent.cpp
AGrenadeProjectile* Grenade = GetWorld()->SpawnActor<AGrenadeProjectile>(
GrenadeProjectileClass,
SpawnLocation,
ShootDirection.Rotation(),
SpawnParams
);
if (Grenade)
{
Grenade->SetExplosionData(GrenadeDamage, GrenadeRadius);
}
// GrenadeProjectile.cpp
void AGrenadeProjectile::SetExplosionData(float InDamage, float InRadius)
{
ExplosionDamage = InDamage;
ExplosionRadius = InRadius;
}
void AGrenadeProjectile::Explode()
{
if (bHasExploded) return;
bHasExploded = true;
FVector ExplodeLocation = GetActorLocation();
if (GrenadeEffect)
{
UGameplayStatics::SpawnEmitterAtLocation(
GetWorld(),
GrenadeEffect,
ExplodeLocation
);
}
if (GrenadeSound)
{
UGameplayStatics::PlaySoundAtLocation(
GetWorld(),
GrenadeSound,
ExplodeLocation,
GrenadeSoundVolume
);
}
TArray<AActor*> IgnoreActors;
if (GetOwner())
{
IgnoreActors.Add(GetOwner());
}
UGameplayStatics::ApplyRadialDamage(
GetWorld(),
ExplosionDamage,
ExplodeLocation,
ExplosionRadius,
UDamageType::StaticClass(),
IgnoreActors,
this,
GetInstigatorController(),
true
);
Destroy();
}
- Explode 함수 안에서 이펙트, 사운드, 범위 데미지를 한 번에 처리로 해결
| Unreal C++ 싱글플레이 FPS 슈터 게임 팀 프로젝트 적 AI - 최종 (0) | 2026.05.28 |
|---|---|
| Unreal C++ 싱글플레이 FPS 슈터 게임 팀 프로젝트 적 AI - 4 (0) | 2026.05.23 |
| Unreal C++ 싱글플레이 FPS 슈터 게임 팀 프로젝트 적 AI - 3 (0) | 2026.05.21 |
| Unreal C++ 싱글플레이 FPS 슈터 게임 팀 프로젝트 적 AI - 2 (0) | 2026.05.20 |
| Unreal C++ 싱글플레이 FPS 슈터 게임 팀 프로젝트 적 AI - 1 (0) | 2026.05.19 |