PART 7 - 강의 3/3

협동 AI와 Squad 시스템

Squad Manager, 역할 배분, AI 간 협동 전술, 대형 유지 시스템을 구현합니다

01

Squad Manager 설계

AI 그룹을 관리하는 중앙 매니저

Squad Manager는 여러 AI를 하나의 그룹으로 관리하며, 전술적 의사결정을 조율합니다. 각 AI가 독립적으로 판단하는 대신, Squad 레벨에서 역할 배분과 협동 행동을 지시합니다.

C++// Squad Manager 클래스 UCLASS() class ASquadManager : public AActor { GENERATED_BODY() public: // 스쿼드 멤버 관리 UPROPERTY() TArray<AAIController*> Members; // 스쿼드 리더 UPROPERTY() AAIController* Leader; // 현재 전술 UPROPERTY() ESquadTactic CurrentTactic = ESquadTactic::Patrol; // 멤버 추가/제거 void AddMember(AAIController* Member); void RemoveMember(AAIController* Member); // 전술 변경 - 모든 멤버에게 전파 void SetTactic(ESquadTactic NewTactic); // 역할 배분 void AssignRoles(); // 타겟 공유 void ShareTarget(AActor* Target); // 스쿼드 상태 평가 void EvaluateSquadState(); protected: virtual void Tick(float DeltaTime) override; }; UENUM() enum class ESquadTactic : uint8 { Patrol, // 순찰 대형 Advance, // 전진 HoldPosition, // 위치 유지 Assault, // 돌격 Surround, // 포위 Retreat, // 후퇴 }; // 역할 정의 UENUM() enum class ESquadRole : uint8 { Leader, // 리더 (전방) Pointman, // 선두 (수색) Flanker, // 측면 담당 Sniper, // 원거리 지원 Support, // 후방 지원 Medic, // 회복 담당 }; // Tick에서 스쿼드 상태 주기적 평가 void ASquadManager::Tick(float DeltaTime) { Super::Tick(DeltaTime); // 멤버 생존 확인 Members.RemoveAll([](AAIController* M) { return !M || !M->GetPawn() || M->GetPawn()->IsDead(); }); // 리더 전사 시 새 리더 선출 if (!Leader || !Leader->GetPawn()) ElectNewLeader(); // 전술 재평가 EvaluateSquadState(); }
설계

Squad Manager는 액터(AActor)로 구현하면 레벨에 배치하여 디버깅이 편리합니다. 월드 서브시스템으로 구현하면 레벨 간 유지가 쉽습니다. 게임 특성에 맞게 선택하세요.

02

역할 배분과 전술 조율

멤버별 역할과 협동 행동 패턴

Squad Manager는 각 멤버에게 역할(Role)을 배분하고, 역할별로 다른 BT 서브트리를 실행하게 합니다. 상황 변화에 따라 실시간으로 역할을 재배분합니다.

C++// 역할 배분 로직 void ASquadManager::AssignRoles() { if (Members.Num() == 0) return; // 리더 선정 (체력 높고 경험치 높은 멤버) Leader = FindBestLeader(); SetRole(Leader, ESquadRole::Leader); // 나머지 역할 배분 for (AAIController* Member : Members) { if (Member == Leader) continue; // AI 특성에 따라 역할 결정 float ShootingSkill = GetSkill(Member, "Shooting"); float Speed = GetSpeed(Member); if (ShootingSkill > 0.8f) SetRole(Member, ESquadRole::Sniper); else if (Speed > 600.f) SetRole(Member, ESquadRole::Flanker); else SetRole(Member, ESquadRole::Support); } } void ASquadManager::SetRole(AAIController* Member, ESquadRole Role) { // Blackboard에 역할 설정 → BT가 역할별 행동 선택 UBlackboardComponent* BB = Member->GetBlackboardComponent(); BB->SetValueAsEnum(TEXT("SquadRole"), static_cast<uint8>(Role)); } // 타겟 공유 시스템 void ASquadManager::ShareTarget(AActor* Target) { // 모든 멤버의 BB에 타겟 전파 for (AAIController* Member : Members) { UBlackboardComponent* BB = Member->GetBlackboardComponent(); BB->SetValueAsObject(TEXT("SquadTarget"), Target); } // 전술 자동 전환 if (Target) SetTactic(ESquadTactic::Assault); else SetTactic(ESquadTactic::Patrol); } // BT에서 역할별 행동 분기 // Selector // ├─ Sequence [스나이퍼] // │ ├─ Decorator: SquadRole == Sniper // │ └─ SubTree: SniperBehavior // │ → 원거리 위치 유지, 정밀 사격 // ├─ Sequence [측면 담당] // │ ├─ Decorator: SquadRole == Flanker // │ └─ SubTree: FlankerBehavior // │ → 적 측면으로 이동, 기습 // ├─ Sequence [지원] // │ ├─ Decorator: SquadRole == Support // │ └─ SubTree: SupportBehavior // │ → 리더 따라가기, 엄폐 사격 // └─ Sequence [리더] // ├─ Decorator: SquadRole == Leader // └─ SubTree: LeaderBehavior // → 전방 이동, 명령 하달

역할 배분 시 AI의 장비/스킬 특성을 고려하면 더 자연스럽습니다. 저격총을 든 AI는 Sniper, 산탄총을 든 AI는 Flanker로 배분하면 무기 특성에 맞는 전술이 됩니다.

03

대형(Formation) 시스템

이동 시 대형 유지와 전환

Formation 시스템은 스쿼드가 이동할 때 멤버 간 상대 위치(대형)를 유지합니다. 리더 기준으로 각 멤버의 오프셋을 계산하여, 일렬/삼각/원형 등 다양한 대형을 구현합니다.

C++// Formation 정의 UENUM() enum class EFormationType : uint8 { Line, // 일렬 종대 Wedge, // V 대형 (삼각) Diamond, // 마름모 Circle, // 원형 (방어) Spread, // 넓게 산개 }; // Formation 오프셋 계산 struct FFormationSlot { FVector Offset; // 리더 기준 상대 위치 FRotator Rotation; // 기준 회전 }; TArray<FFormationSlot> GetFormationSlots( EFormationType Type, int32 MemberCount, float Spacing) { TArray<FFormationSlot> Slots; switch (Type) { case EFormationType::Wedge: // V 대형: 리더 전방, 좌우 뒤로 벌림 Slots.Add({FVector::ZeroVector, FRotator::ZeroRotator}); // 리더 for (int32 i = 1; i < MemberCount; ++i) { float Side = (i % 2 == 0) ? 1.f : -1.f; int32 Row = (i + 1) / 2; Slots.Add({ FVector(-Row * Spacing, Side * Row * Spacing * 0.7f, 0), FRotator::ZeroRotator }); } break; case EFormationType::Line: // 일렬: 리더 뒤로 일렬 for (int32 i = 0; i < MemberCount; ++i) { Slots.Add({ FVector(-i * Spacing, 0, 0), FRotator::ZeroRotator }); } break; case EFormationType::Circle: // 원형: 균등 분배 for (int32 i = 0; i < MemberCount; ++i) { float Angle = (360.f / MemberCount) * i; float Rad = FMath::DegreesToRadians(Angle); Slots.Add({ FVector(FMath::Cos(Rad), FMath::Sin(Rad), 0) * Spacing, FRotator(0, Angle + 180.f, 0) // 바깥을 향함 }); } break; } return Slots; } // 월드 좌표로 변환 FVector GetFormationWorldPosition( const AActor* Leader, const FFormationSlot& Slot) { FTransform LeaderTransform = Leader->GetActorTransform(); return LeaderTransform.TransformPosition(Slot.Offset); } // Service에서 대형 위치 업데이트 void UBTService_Formation::TickNode( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) { int32 SlotIndex = BB->GetValueAsInt(TEXT("FormationSlot")); FVector FormPos = SquadManager->GetFormationPosition(SlotIndex); BB->SetValueAsVector(TEXT("FormationTarget"), FormPos); }
주의

대형 이동 시 NavMesh 유효성 검사를 반드시 수행하세요. 대형 위치가 벽이나 절벽 위에 있으면 AI가 경로를 찾지 못합니다. 유효하지 않은 위치는 NavMesh 위의 가장 가까운 점으로 보정합니다.

04

협동 전술 구현

압박/측면, 포위, 구출 등 팀 전술

스쿼드 단위의 협동 전술은 개별 AI의 합보다 큰 효과를 만듭니다. Squad Manager가 전술을 지시하고, 각 멤버가 역할에 맞는 행동을 동기화하여 수행합니다.

C++// 전술 1: 압박 + 측면 공격 (Suppress & Flank) void ASquadManager::ExecuteSuppressAndFlank(AActor* Target) { // 절반은 전방에서 압박 // 나머지는 측면으로 이동 int32 SuppressCount = Members.Num() / 2; for (int32 i = 0; i < Members.Num(); ++i) { UBlackboardComponent* BB = Members[i]->GetBlackboardComponent(); if (i < SuppressCount) { BB->SetValueAsEnum(TEXT("TacticRole"), static_cast<uint8>(ETacticRole::Suppress)); // → 엄폐 위치에서 지속 사격 } else { BB->SetValueAsEnum(TEXT("TacticRole"), static_cast<uint8>(ETacticRole::Flank)); // → 측면 이동 후 공격 } } } // 전술 2: 포위 (Surround) void ASquadManager::ExecuteSurround(AActor* Target) { FVector TargetLoc = Target->GetActorLocation(); float AngleStep = 360.f / Members.Num(); for (int32 i = 0; i < Members.Num(); ++i) { float Angle = AngleStep * i; float Rad = FMath::DegreesToRadians(Angle); FVector SurroundPos = TargetLoc + FVector(FMath::Cos(Rad), FMath::Sin(Rad), 0) * 800.f; // NavMesh 유효 위치로 보정 FNavLocation NavLoc; UNavigationSystemV1::GetCurrent(GetWorld()) ->ProjectPointToNavigation( SurroundPos, NavLoc, FVector(200, 200, 200)); UBlackboardComponent* BB = Members[i]->GetBlackboardComponent(); BB->SetValueAsVector(TEXT("TacticPosition"), NavLoc.Location); BB->SetValueAsEnum(TEXT("TacticRole"), static_cast<uint8>(ETacticRole::Surround)); } } // 전술 3: 동시 돌격 (Coordinated Assault) void ASquadManager::ExecuteAssault(AActor* Target) { // 카운트다운 후 동시 돌격 AssaultCountdown = 3.0f; for (AAIController* Member : Members) { UBlackboardComponent* BB = Member->GetBlackboardComponent(); BB->SetValueAsBool(TEXT("WaitForAssault"), true); BB->SetValueAsObject(TEXT("AssaultTarget"), Target); } } void ASquadManager::Tick(float DeltaTime) { if (AssaultCountdown > 0) { AssaultCountdown -= DeltaTime; if (AssaultCountdown <= 0) { // 동시 돌격 신호 for (AAIController* Member : Members) { Member->GetBlackboardComponent() ->SetValueAsBool(TEXT("WaitForAssault"), false); } } } }

동시 돌격 시 약간의 시간차(0.2~0.5초)를 두면 더 자연스럽습니다. 모든 AI가 정확히 같은 타이밍에 움직이면 플레이어에게 "기계적"으로 느껴집니다. 멤버별로 RandomDeviation을 추가하세요.

05

통신과 정보 공유

스쿼드 멤버 간 정보 전달 시스템

스쿼드 멤버 간 정보 공유는 협동 AI의 핵심입니다. 적 발견, 엄폐물 위치, 위험 지역 등의 정보를 멤버 간에 전파하여 팀 전체의 상황 인식을 향상시킵니다.

C++// 스쿼드 통신 시스템 UENUM() enum class ESquadMessage : uint8 { EnemySpotted, // 적 발견 EnemyLost, // 적 상실 TakingFire, // 피격 중 NeedBackup, // 지원 요청 CoverFound, // 엄폐물 발견 AreaClear, // 지역 클리어 Retreating, // 후퇴 중 InPosition, // 위치 도달 }; void ASquadManager::BroadcastMessage( AAIController* Sender, ESquadMessage Message, const FVector& Location, AActor* RelevantActor) { for (AAIController* Member : Members) { if (Member == Sender) continue; // 거리에 따른 전달 지연 시뮬레이션 float Distance = FVector::Dist( Sender->GetPawn()->GetActorLocation(), Member->GetPawn()->GetActorLocation()); float Delay = Distance / 10000.f; // 거리비례 딜레이 FTimerHandle TimerHandle; GetWorldTimerManager().SetTimer(TimerHandle, [Member, Message, Location, RelevantActor]() { HandleMessage(Member, Message, Location, RelevantActor); }, Delay, false); } } void ASquadManager::HandleMessage( AAIController* Receiver, ESquadMessage Message, const FVector& Location, AActor* RelevantActor) { UBlackboardComponent* BB = Receiver->GetBlackboardComponent(); switch (Message) { case ESquadMessage::EnemySpotted: // 아군이 적 발견 → 내 BB에도 등록 BB->SetValueAsObject(TEXT("SharedTarget"), RelevantActor); BB->SetValueAsVector(TEXT("SharedEnemyLocation"), Location); break; case ESquadMessage::NeedBackup: // 지원 요청 → 해당 위치로 이동 BB->SetValueAsVector(TEXT("BackupLocation"), Location); BB->SetValueAsBool(TEXT("BackupRequested"), true); break; case ESquadMessage::Retreating: // 아군 후퇴 → 엄호 사격 고려 BB->SetValueAsBool(TEXT("AllyRetreating"), true); break; } }
설계

거리 기반 전달 지연은 "무전 전달 시간"을 시뮬레이션합니다. 먼 아군일수록 정보를 늦게 받아 즉각적인 동기화 대신 현실적인 지연이 발생합니다. 이는 AI의 신뢰성을 높이면서 난이도를 적절히 조절합니다.

SUMMARY

핵심 요약

  • Squad Manager가 멤버 관리, 역할 배분, 전술 조율을 중앙에서 담당한다
  • 역할(Role)을 BB에 설정하면 BT에서 역할별 서브트리로 자동 분기된다
  • Formation 시스템으로 Wedge/Line/Circle 등 대형을 유지하며 이동한다
  • 협동 전술(압박+측면, 포위, 동시돌격)은 Squad Manager가 지시하고 각 멤버가 동기화하여 수행한다
  • 통신 시스템으로 적 위치, 지원 요청 등을 멤버 간 공유하되, 거리 기반 지연으로 현실감을 부여한다
PRACTICE

도전 과제

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

실습 1: 기본 Squad 시스템 구현

ASquadManager Actor를 만들고 소속 AI 목록을 관리하세요. 리더 AI의 명령(이동, 공격, 대기)을 Squad 멤버에게 전파하는 기본 시스템을 구현합니다.

실습 2: 역할 분담 시스템

Squad 내에서 Attacker(공격), Flanker(우회), Supporter(지원) 역할을 배분하세요. 각 역할에 맞는 EQS 쿼리를 할당하여 역할별 최적 위치를 계산합니다.

심화 과제

Squad가 핀서(Pincer) 전술을 실행하는 시스템을 구현하세요. 전방 억제 팀과 측면 우회 팀으로 나누어 동시에 진입하고, 팀 간 Blackboard 공유로 타이밍을 동기화합니다.