협동 AI와 Squad 시스템
Squad Manager, 역할 배분, AI 간 협동 전술, 대형 유지 시스템을 구현합니다
Squad Manager 설계
AI 그룹을 관리하는 중앙 매니저
Squad Manager는 여러 AI를 하나의 그룹으로 관리하며, 전술적 의사결정을 조율합니다. 각 AI가 독립적으로 판단하는 대신, Squad 레벨에서 역할 배분과 협동 행동을 지시합니다.
// 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)로 구현하면 레벨에 배치하여 디버깅이 편리합니다. 월드 서브시스템으로 구현하면 레벨 간 유지가 쉽습니다. 게임 특성에 맞게 선택하세요.
역할 배분과 전술 조율
멤버별 역할과 협동 행동 패턴
Squad Manager는 각 멤버에게 역할(Role)을 배분하고, 역할별로 다른 BT 서브트리를 실행하게 합니다. 상황 변화에 따라 실시간으로 역할을 재배분합니다.
// 역할 배분 로직
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로 배분하면 무기 특성에 맞는 전술이 됩니다.
대형(Formation) 시스템
이동 시 대형 유지와 전환
Formation 시스템은 스쿼드가 이동할 때 멤버 간 상대 위치(대형)를 유지합니다. 리더 기준으로 각 멤버의 오프셋을 계산하여, 일렬/삼각/원형 등 다양한 대형을 구현합니다.
// 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 위의 가장 가까운 점으로 보정합니다.
협동 전술 구현
압박/측면, 포위, 구출 등 팀 전술
스쿼드 단위의 협동 전술은 개별 AI의 합보다 큰 효과를 만듭니다. Squad Manager가 전술을 지시하고, 각 멤버가 역할에 맞는 행동을 동기화하여 수행합니다.
// 전술 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을 추가하세요.
통신과 정보 공유
스쿼드 멤버 간 정보 전달 시스템
스쿼드 멤버 간 정보 공유는 협동 AI의 핵심입니다. 적 발견, 엄폐물 위치, 위험 지역 등의 정보를 멤버 간에 전파하여 팀 전체의 상황 인식을 향상시킵니다.
// 스쿼드 통신 시스템
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의 신뢰성을 높이면서 난이도를 적절히 조절합니다.
핵심 요약
- Squad Manager가 멤버 관리, 역할 배분, 전술 조율을 중앙에서 담당한다
- 역할(Role)을 BB에 설정하면 BT에서 역할별 서브트리로 자동 분기된다
- Formation 시스템으로 Wedge/Line/Circle 등 대형을 유지하며 이동한다
- 협동 전술(압박+측면, 포위, 동시돌격)은 Squad Manager가 지시하고 각 멤버가 동기화하여 수행한다
- 통신 시스템으로 적 위치, 지원 요청 등을 멤버 간 공유하되, 거리 기반 지연으로 현실감을 부여한다
도전 과제
배운 내용을 직접 실습해보세요
ASquadManager Actor를 만들고 소속 AI 목록을 관리하세요. 리더 AI의 명령(이동, 공격, 대기)을 Squad 멤버에게 전파하는 기본 시스템을 구현합니다.
Squad 내에서 Attacker(공격), Flanker(우회), Supporter(지원) 역할을 배분하세요. 각 역할에 맞는 EQS 쿼리를 할당하여 역할별 최적 위치를 계산합니다.
Squad가 핀서(Pincer) 전술을 실행하는 시스템을 구현하세요. 전방 억제 팀과 측면 우회 팀으로 나누어 동시에 진입하고, 팀 간 Blackboard 공유로 타이밍을 동기화합니다.