전투 AI 패턴
전투 상태 머신, 엄폐, 측면 공격, 후퇴 전략을 BT와 EQS로 구현합니다
전투 상태 머신
전투 AI의 핵심 상태와 전이 조건
전투 AI는 상태 머신 기반으로 설계합니다. 추적, 교전, 엄폐, 측면 공격, 후퇴의 5가지 핵심 상태와 체력/거리/탄약 기반의 전이 조건을 정의합니다.
// 전투 상태 정의
UENUM(BlueprintType)
enum class ECombatState : uint8
{
Chase, // 적 추적 (교전 거리 밖)
Engage, // 직접 교전 (사격/공격)
Cover, // 엄폐물 뒤 대기
Flank, // 측면 이동 후 공격
Retreat, // 체력 낮음 → 후퇴
};
// 상태 전이 로직 (Service에서 평가)
UCLASS()
class UBTService_EvaluateCombatState : public UBTService
{
virtual void TickNode(
UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory, float DeltaSeconds) override
{
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
AAIController* Controller = OwnerComp.GetAIOwner();
float Health = GetHealthRatio(Controller);
float DistToEnemy = GetDistanceToEnemy(Controller);
bool bHasLOS = HasLineOfSightToEnemy(Controller);
bool bInCover = IsInCover(Controller);
ECombatState NewState;
// 전이 규칙
if (Health < 0.2f)
NewState = ECombatState::Retreat;
else if (DistToEnemy > EngageRange)
NewState = ECombatState::Chase;
else if (Health < 0.5f && !bInCover)
NewState = ECombatState::Cover;
else if (bInCover && FMath::RandBool())
NewState = ECombatState::Flank; // 가끔 측면 공격
else
NewState = ECombatState::Engage;
BB->SetValueAsEnum(TEXT("CombatState"),
static_cast<uint8>(NewState));
}
};
// BT 구조:
// Selector
// ├─ Sequence [후퇴]
// │ ├─ Decorator: CombatState == Retreat
// │ │ (ObserverAborts: LowerPriority)
// │ └─ SubTree: RetreatBehavior
// ├─ Sequence [엄폐]
// │ ├─ Decorator: CombatState == Cover
// │ └─ SubTree: CoverBehavior
// ├─ Sequence [측면 공격]
// │ ├─ Decorator: CombatState == Flank
// │ └─ SubTree: FlankBehavior
// ├─ Sequence [교전]
// │ ├─ Decorator: CombatState == Engage
// │ └─ SubTree: EngageBehavior
// └─ Sequence [추적]
// └─ SubTree: ChaseBehavior전투 상태 Service의 Interval은 0.3~0.5초가 적당합니다. 너무 빠르면 상태가 빈번히 전환되어 AI가 불안정하게 보이고, 너무 느리면 반응이 둔해집니다.
엄폐(Cover) 시스템
EQS 기반 엄폐물 탐색과 사용
엄폐 시스템은 적 시야에서 차단되면서 가까운 위치를 EQS로 찾고, 엄폐 상태에서 간헐적으로 사격하는 패턴입니다. Cover Point 액터를 미리 배치하거나 EQS로 동적 생성할 수 있습니다.
// EQS: FindCoverPosition
// Generator: SimpleGrid (AI 주변)
// GridSize: 1500, SpaceBetween: 200
// Test 1: Trace to EnemyActor → Filter(실패만 통과)
// → 적 시야에서 차단된 위치만
// Test 2: Distance to Self → Score(InverseLinear, W=1.0)
// → 가까운 엄폐물 선호
// Test 3: Distance to EnemyActor → Filter(Min 300)
// → 적과 최소 거리 유지
// Test 4: Pathfinding to Self → Filter(PathExist)
// → 도달 가능한 위치만
// 엄폐 BT (SubTree: CoverBehavior):
// Sequence
// ├─ Task: RunEQSQuery(FindCoverPosition)
// │ → BB: CoverLocation
// ├─ Task: MoveTo(CoverLocation)
// ├─ Task: Crouch (웅크리기)
// ├─ Loop (3~5회)
// │ ├─ Task: Wait(1~2초) (엄폐 대기)
// │ ├─ Task: PeekAndShoot (들여다보고 사격)
// │ └─ Task: ReturnToCover (엄폐 복귀)
// └─ Task: StandUp (일어서기)
// PeekAndShoot 커스텀 Task
UCLASS()
class UBTTask_PeekAndShoot : public UBTTaskNode
{
virtual EBTNodeResult::Type ExecuteTask(
UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory) override
{
AAIController* Controller = OwnerComp.GetAIOwner();
APawn* Pawn = Controller->GetPawn();
AActor* Target = Cast<AActor>(
OwnerComp.GetBlackboardComponent()
->GetValueAsObject(TEXT("TargetActor")));
// 엄폐물에서 살짝 나와서
FVector PeekOffset = GetPeekDirection(Pawn) * 100.f;
Pawn->AddActorWorldOffset(PeekOffset);
// 사격
if (HasLineOfSight(Pawn, Target))
{
FireWeapon(Controller);
}
// 돌아가기는 다음 Task에서 처리
return EBTNodeResult::Succeeded;
}
};Peek 방향을 랜덤(좌/우)으로 선택하면 AI가 예측 불가능하게 됩니다. 항상 같은 방향으로 나오면 플레이어가 패턴을 쉽게 파악합니다.
측면 공격(Flanking) 전략
적의 측면/후방으로 이동하여 공격
Flanking은 적의 전방을 피해 측면이나 후방에서 공격하는 전술입니다. EQS의 OnCircle Generator와 Dot Test를 조합하여 최적의 측면 공격 위치를 찾습니다.
// EQS: FindFlankPosition
// Generator: OnCircle (EnemyActor 중심)
// CircleRadius: 800
// SpaceBetween: 100
// ArcDirection: FrontOfContext (적 전방 기준)
// ArcAngle: 300 (적 전방 60도 제외)
// → 적의 측면/후방에만 후보 생성
// Test 1: Trace to EnemyActor → Score(Linear, W=1.0)
// → 적을 볼 수 있는 위치 선호 (사격 가능)
// Test 2: Dot(Enemy전방, Enemy→Item) → Score(InverseLinear, W=1.5)
// → 적 후방일수록 점수 ↑ (Dot -1에 가까울수록)
// Test 3: Distance to Self → Score(InverseLinear, W=0.5)
// → 가까운 위치 약간 선호
// Test 4: Trace from Self to Item → Filter(성공)
// → 이동 경로에 장애물 없음
// Flanking BT (SubTree: FlankBehavior):
// Sequence
// ├─ Task: RunEQSQuery(FindFlankPosition)
// │ → BB: FlankLocation
// ├─ SimpleParallel
// │ ├─ [Main] Task: MoveTo(FlankLocation)
// │ └─ [BG] Service: KeepTrackingEnemy
// ├─ Task: RotateToFaceEnemy
// └─ Task: AttackFromFlank
// Flanking 판별 함수
bool IsFlankingPosition(const FVector& Position,
const AActor* Enemy)
{
FVector EnemyForward = Enemy->GetActorForwardVector();
FVector EnemyToPosition = (Position -
Enemy->GetActorLocation()).GetSafeNormal();
// Dot Product: 1=전방, -1=후방
float Dot = FVector::DotProduct(EnemyForward, EnemyToPosition);
// Dot < 0.3 이면 측면~후방
return Dot < 0.3f;
}
// 측면 공격 성공 시 추가 데미지
float AMyCharacter::CalculateDamageMultiplier(
const AActor* Attacker) const
{
float Dot = FVector::DotProduct(
GetActorForwardVector(),
(Attacker->GetActorLocation() -
GetActorLocation()).GetSafeNormal());
if (Dot < -0.5f) return 2.0f; // 후방: 2배
if (Dot < 0.3f) return 1.5f; // 측면: 1.5배
return 1.0f; // 전방: 1배
}Flanking은 엄폐 상태인 적에게 매우 효과적입니다. 한 AI가 전방에서 압박(Suppression)하는 동안 다른 AI가 측면으로 이동하면, 엄폐의 이점을 무력화할 수 있습니다.
후퇴(Retreat) 전략
위기 상황에서의 안전 지역 이동
후퇴는 체력이 낮거나 아군이 전멸했을 때 실행됩니다. 적에게서 멀고 시야가 차단된 안전 지역으로 이동하며, 이동 중에도 가능하면 적을 견제합니다.
// EQS: FindRetreatPosition
// Generator: SimpleGrid (AI 주변, 적 반대편 가중)
// GridSize: 2000, SpaceBetween: 300
// Test 1: Distance to EnemyActor → Score(Linear, W=2.0)
// → 적에게서 멀수록 점수 ↑
// Test 2: Dot(Self→Item, Self→Enemy) → Score(InverseLinear, W=1.5)
// → 적 반대 방향일수록 점수 ↑
// Test 3: Trace to EnemyActor → Score(InverseLinear, W=1.0)
// → 시야 차단 위치 선호
// Test 4: Pathfinding → Filter(PathExist)
// → 도달 가능한 위치만
// Test 5: Distance to AllySpawnPoint → Score(InverseLinear, W=0.5)
// → 아군 거점 방향 약간 선호
// 후퇴 BT (SubTree: RetreatBehavior):
// Sequence
// ├─ Task: PlayVoice("Retreating!")
// ├─ Task: RunEQSQuery(FindRetreatPosition)
// │ → BB: RetreatLocation
// ├─ SimpleParallel
// │ ├─ [Main] Task: MoveTo(RetreatLocation, Sprint)
// │ └─ [BG] Sequence
// │ ├─ Task: SuppressiveFire (뒤돌아 견제 사격)
// │ └─ Service: CheckSafety
// ├─ Task: UseHealthItem (회복 아이템 사용)
// └─ Task: SetState(Cover) (엄폐로 전환)
// 후퇴 판단 조건
bool ShouldRetreat(AAIController* Controller)
{
float Health = GetHealthRatio(Controller);
int32 AliveAllies = GetAliveAllyCount(Controller);
bool bOutnumbered = GetNearbyEnemyCount(Controller) >
AliveAllies + 1;
return Health < 0.2f // 체력 20% 미만
|| (Health < 0.4f && bOutnumbered) // 체력 낮고 수적 열세
|| AliveAllies == 0; // 아군 전멸
}
// 후퇴 속도 보너스
void AMyCharacter::ApplyRetreatSpeedBoost()
{
GetCharacterMovement()->MaxWalkSpeed *= 1.3f;
// 일정 시간 후 복원
GetWorldTimerManager().SetTimer(SpeedTimer,
[this]() {
GetCharacterMovement()->MaxWalkSpeed /= 1.3f;
}, 5.0f, false);
}후퇴 중에도 견제 사격(Suppressive Fire)을 수행하면 AI가 훨씬 자연스럽게 보입니다. SimpleParallel로 이동과 사격을 동시에 수행하세요.
핵심 요약
- 전투 상태 머신(Chase/Engage/Cover/Flank/Retreat)으로 전투 AI를 체계적으로 설계한다
- 엄폐 시스템은 EQS Trace Test로 적 시야 차단 위치를 찾고, PeekAndShoot으로 간헐 사격한다
- 측면 공격은 OnCircle Generator + Dot Test로 적의 측면/후방 위치를 선정한다
- 후퇴는 체력/아군 상태를 종합 판단하며, SimpleParallel로 이동과 견제를 동시에 수행한다
도전 과제
배운 내용을 직접 실습해보세요
적 감지 -> 사거리까지 접근 -> 공격 -> 쿨다운 대기의 기본 전투 루프를 BT로 구현하세요. 사거리(MeleeRange, RangedRange)를 Blackboard에서 관리합니다.
EQS로 엄폐 위치를 찾고, 엄폐 -> 사격 -> 재장전 -> 엄폐 전환 패턴을 구현하세요. Line of Sight Test로 적에게 노출되지 않는 위치를 평가합니다.
다수의 적 중 가장 위험한 대상을 자동으로 선택하는 Threat Assessment 시스템을 구현하세요. 거리, 체력, 무기 타입, 마지막 공격 시각 등을 종합하여 위협도를 계산합니다.