PART 7 - 강의 2/3

전투 AI 패턴

전투 상태 머신, 엄폐, 측면 공격, 후퇴 전략을 BT와 EQS로 구현합니다

01

전투 상태 머신

전투 AI의 핵심 상태와 전이 조건

전투 AI는 상태 머신 기반으로 설계합니다. 추적, 교전, 엄폐, 측면 공격, 후퇴의 5가지 핵심 상태와 체력/거리/탄약 기반의 전이 조건을 정의합니다.

전투 상태 전이
Chase (추적) Engage (교전) Cover (엄폐) Flank (측면) Retreat (후퇴)
C++// 전투 상태 정의 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가 불안정하게 보이고, 너무 느리면 반응이 둔해집니다.

02

엄폐(Cover) 시스템

EQS 기반 엄폐물 탐색과 사용

엄폐 시스템은 적 시야에서 차단되면서 가까운 위치를 EQS로 찾고, 엄폐 상태에서 간헐적으로 사격하는 패턴입니다. Cover Point 액터를 미리 배치하거나 EQS로 동적 생성할 수 있습니다.

C++// 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가 예측 불가능하게 됩니다. 항상 같은 방향으로 나오면 플레이어가 패턴을 쉽게 파악합니다.

03

측면 공격(Flanking) 전략

적의 측면/후방으로 이동하여 공격

Flanking은 적의 전방을 피해 측면이나 후방에서 공격하는 전술입니다. EQS의 OnCircle Generator와 Dot Test를 조합하여 최적의 측면 공격 위치를 찾습니다.

C++// 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가 측면으로 이동하면, 엄폐의 이점을 무력화할 수 있습니다.

04

후퇴(Retreat) 전략

위기 상황에서의 안전 지역 이동

후퇴는 체력이 낮거나 아군이 전멸했을 때 실행됩니다. 적에게서 멀고 시야가 차단된 안전 지역으로 이동하며, 이동 중에도 가능하면 적을 견제합니다.

C++// 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로 이동과 사격을 동시에 수행하세요.

SUMMARY

핵심 요약

  • 전투 상태 머신(Chase/Engage/Cover/Flank/Retreat)으로 전투 AI를 체계적으로 설계한다
  • 엄폐 시스템은 EQS Trace Test로 적 시야 차단 위치를 찾고, PeekAndShoot으로 간헐 사격한다
  • 측면 공격은 OnCircle Generator + Dot Test로 적의 측면/후방 위치를 선정한다
  • 후퇴는 체력/아군 상태를 종합 판단하며, SimpleParallel로 이동과 견제를 동시에 수행한다
PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: 기본 전투 AI 구현

적 감지 -> 사거리까지 접근 -> 공격 -> 쿨다운 대기의 기본 전투 루프를 BT로 구현하세요. 사거리(MeleeRange, RangedRange)를 Blackboard에서 관리합니다.

실습 2: 엄폐 전투 AI

EQS로 엄폐 위치를 찾고, 엄폐 -> 사격 -> 재장전 -> 엄폐 전환 패턴을 구현하세요. Line of Sight Test로 적에게 노출되지 않는 위치를 평가합니다.

심화 과제

다수의 적 중 가장 위험한 대상을 자동으로 선택하는 Threat Assessment 시스템을 구현하세요. 거리, 체력, 무기 타입, 마지막 공격 시각 등을 종합하여 위협도를 계산합니다.