https://hyunjunstar.tistory.com/101
위 링크에서는 적 AI의 TargetActor 갱신 문제와 공격 데미지 타이밍 문제를 수정했다.
TargetActor가 한 번만 설정되던 문제는 SetTimer()를 반복 실행하도록 변경하여 해결했고,
BTTask_AttackTarget에서 ApplyDamage()를 즉시 호출하던 구조는 Anim Notify 시점에 데미지를 적용하는 방식으로 수정했다.
또한 Montage, Anim Notify, Anim Graph를 활용해 공격, 사망, 이동 애니메이션이 더 자연스럽게 연결되도록 개선했다.
이번 글에서는 최종적으로 완성된 적 AI의 전체 흐름과 코드를 정리하려고 한다.

적 AI는 EnemySpawner에서 생성된 뒤 EnemyCharacter, EnemyAIController, Blackboard, Behavior Tree를 거쳐 추적과 공격을 수행합니다.
실제 데미지는 공격 Task에서 바로 적용하지 않고, 공격 Montage의 Anim Notify 시점에 ApplyAttackDamage()를 호출해 처리합니다.

EnemySpawner는 게임 시작 시 NavMesh 위의 랜덤 위치를 찾아 적을 생성합니다.
스폰 위치는 SpawnRadius 안에서 탐색하며, 충돌이 발생하면 가능한 위치로 조정하거나 스폰을 건너뛰도록 처리했습니다.

EnemyCharacter는 HP, 방어력, 공격력, 탐지 범위, 공격 범위 등 적의 기본 전투 데이터를 관리합니다.
또한 Behavior Tree 참조, RVO 회피, 이동 방향 회전, 공격/사망 애니메이션 이벤트를 담당합니다.

EnemyAIController는 적을 Possess한 뒤 Behavior Tree를 실행하고, 0.2초마다 플레이어를 탐지합니다.
플레이어가 DetectionRange 안에 있으면 Blackboard의 TargetActor에 저장하고, 범위 밖이면 TargetActor를 제거합니다.

Behavior Tree는 Blackboard의 TargetActor를 기준으로 현재 행동을 결정합니다.
TargetActor가 있고 공격 범위 안이면 공격 Sequence를 실행하고, 공격 범위 밖이면 Move To로 플레이어를 추적합니다.
TargetActor가 없으면 짧게 대기합니다.

BTDecorator_IsInAttackRange는 TargetActor와 적 사이의 거리를 계산해 공격 가능 여부를 판단합니다.
공격 범위 안이면 공격 Task가 실행되고, 범위 밖이면 추적 로직으로 넘어갑니다.

BTTask_AttackTarget은 공격이 가능한 상태인지 확인한 뒤, 적의 이동을 멈추고 플레이어 방향으로 회전시킵니다.
이후 PlayAttackAnimation()을 호출해 공격 Montage를 재생하고, MarkAttack()으로 공격 쿨타임을 기록합니다.

BP_EnemyCharacter에서는 C++에서 호출한 PlayAttackAnimation 이벤트를 받아 공격 Montage를 재생합니다.
공격 Montage 안에는 AttackHit Notify가 배치되어 있으며, 실제 타격 프레임에 데미지 함수가 호출되도록 연결했습니다.

ABP_ZombieEnemy는 적의 속도를 계산해 Idle / Walk / Run 애니메이션을 전환합니다.
또한 DefaultSlot을 통해 이동 애니메이션 위에 공격 및 사망 Montage가 재생될 수 있도록 구성했습니다.

최종적으로 BP_EnemyCharacter, ABP_ZombieEnemy, Blend Space, Attack Montage, Death Montage를 연결해 실제 게임에서 AI 캐릭터가 자연스럽게 움직이고 공격하도록 구성했습니다.
1. EnemyAIController - 0.2초마다 TargetActor 갱신
// EnemyAIController.cpp
// 플레이어 감지 상태를 주기적으로 갱신
GetWorldTimerManager().SetTimer(
FindPlayerTimerHandle,
this,
&AEnemyAIController::SetTargetPlayer,
0.2f,
true
);
2. EnemyAIController - 감지 범위에 따라 TargetActor 설정/제거
// EnemyAIController.cpp
// 적과 플레이어 사이의 거리를 계산
const float DistanceToPlayer = FVector::Dist2D(
ControlledEnemy->GetActorLocation(),
TargetPlayer->GetActorLocation()
);
// 감지 범위 안이면 플레이어를 TargetActor로 등록하고, 벗어나면 제거
if (DistanceToPlayer <= ControlledEnemy->DetectionRange)
{
BB->SetValueAsObject(TEXT("TargetActor"), TargetPlayer);
}
else
{
BB->ClearValue(TEXT("TargetActor"));
}
3. BTDecorator_IsInAttackRange - 공격 범위 판단
// BTDecorator_IsInAttackRange.cpp
// 적과 타겟 사이의 거리를 계산
const float Distance = FVector::Dist2D(
Enemy->GetActorLocation(),
TargetActor->GetActorLocation()
);
// 공격 범위 안이면 true, 아니면 false
return Distance <= Enemy->AttackRange;
4. BTTask_AttackTarget - 이동 정지, 회전, 공격 몽타주 실행
// BTTask_AttackTarget.cpp
// 공격 시작 전에 이동과 AI Focus를 정리
AIController->StopMovement();
AIController->ClearFocus(EAIFocusPriority::Gameplay);
// 이전 이동 입력이 남아 공격 방향을 방해하지 않도록 즉시 정지
Enemy->GetCharacterMovement()->StopMovementImmediately();
// 플레이어 방향으로 몸 돌리기
FVector Direction = TargetActor->GetActorLocation() - Enemy->GetActorLocation();
Direction.Z = 0.f;
if (!Direction.IsNearlyZero())
{
// Z축을 제외한 방향으로 회전하여 적이 플레이어를 바라보게 함
FRotator LookRotation = Direction.Rotation();
Enemy->SetActorRotation(LookRotation);
AIController->SetControlRotation(LookRotation);
}
// 공격 애니메이션 재생
Enemy->PlayAttackAnimation();
// 공격을 시작한 시점에 쿨타임 기록
Enemy->MarkAttack();
5. EnemyCharacter - Anim Notify 시점 실제 데미지 적용
// EnemyCharacter.cpp
// Notify가 늦게 호출되는 동안 플레이어가 멀리 도망간 경우 데미지를 주지 않음
const float DistanceToTarget = FVector::Dist2D(
GetActorLocation(),
TargetActor->GetActorLocation()
);
if (DistanceToTarget > AttackRange + 50.f)
{
return;
}
// TargetActor는 플레이어이므로 플레이어의 TakeDamage가 호출됨
UGameplayStatics::ApplyDamage(
TargetActor,
AttackDamage,
AIController,
this,
nullptr
);
6. EnemyCharacter - 사망 처리
// EnemyCharacter.cpp
// 사망 후 이동과 충돌을 막아 AI가 더 이상 움직이거나 막히지 않게 함
GetCharacterMovement()->DisableMovement();
GetCharacterMovement()->StopMovementImmediately();
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
// BP_EnemyCharacter에서 연결한 사망 몽타주 재생
PlayDeathAnimation();
// 사망 애니메이션이 보일 시간을 준 뒤 액터 제거
SetLifeSpan(2.5f);
이번 적 AI 구현은 수업에서 아직 자세히 다루지 않은 내용이라 처음에는 구조를 이해하는 데 어려움이 있었다.
그래서 Unreal Engine의 AIController, Blackboard, Behavior Tree, Anim Notify 관련 자료와 영상을 참고하면서 전체 흐름을 먼저 공부했다.
처음에는 적이 플레이어를 따라오고 공격하는 기능이 단순할 것이라고 생각했는데,
실제로 진행해보니 플레이어 탐지, TargetActor 갱신, 추적 조건, 공격 범위 판단, 공격 애니메이션, 데미지 적용 타이밍, 사망 처리까지 여러 요소가 연결되어야 자연스럽게 동작한다는 것을 알게 되었다.
특히 BTTask_AttackTarget에서 바로 데미지를 적용하는 방식과 Anim Notify 시점에 데미지를 적용하는 방식의 차이를 보면서,
게임 개발에서는 코드가 실행되는 시점뿐만 아니라 플레이어가 체감하는 애니메이션 타이밍도 중요하다는 점을 배웠다.
또한 EnemyAIController, Blackboard, Behavior Tree, EnemyCharacter가
각각 어떤 역할을 맡는지 정리하면서 Unreal Engine의 AI 구조를 더 명확하게 이해할 수 있었다.
이번 구현을 통해 아직 익숙하지 않은 기능도 자료를 찾아보고 흐름을 분석하면 프로젝트에 적용할 수 있다는 자신감을 얻었다.
다음 프로젝트 부터는 이 구조를 더 공부해서 여러 공격 패턴, 피격 애니메이션, 난이도별 AI 반응 속도 같은 기능도 직접 확장해보고 싶다.
| Unreal C++ 싱글플레이 FPS 슈터 게임 팀 프로젝트 구조 정리 및 트러블슈팅 (0) | 2026.05.26 |
|---|---|
| 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 |