PART 7 - 강의 1/3

순찰 AI 패턴

Patrol 경로 설계, Waypoint 시스템, 경계 상태, 복귀 로직을 체계적으로 구현합니다

01

Waypoint 기반 순찰 시스템

순찰 경로를 정의하는 Waypoint 액터

Waypoint 시스템은 AI가 순서대로 방문하는 지점들을 정의합니다. Spline 기반 경로와 개별 Waypoint 액터 방식이 있으며, 각 Waypoint에 대기 시간, 주시 방향 등의 메타데이터를 추가할 수 있습니다.

C++// Waypoint 액터 UCLASS() class APatrolWaypoint : public AActor { GENERATED_BODY() public: // 이 Waypoint에서의 대기 시간 UPROPERTY(EditAnywhere, Category = "Patrol") float WaitTime = 2.0f; // 대기 중 주시 방향 (로컬) UPROPERTY(EditAnywhere, Category = "Patrol") FRotator LookDirection = FRotator::ZeroRotator; // 대기 중 수행할 행동 UPROPERTY(EditAnywhere, Category = "Patrol") EPatrolIdleBehavior IdleBehavior = EPatrolIdleBehavior::LookAround; // 다음 Waypoint (순환 링크) UPROPERTY(EditAnywhere, Category = "Patrol") APatrolWaypoint* NextWaypoint; }; UENUM() enum class EPatrolIdleBehavior : uint8 { None, // 그냥 대기 LookAround, // 좌우 둘러보기 PlayAnimation, // 특정 애니메이션 Investigate, // 주변 수색 }; // Patrol Path 컨테이너 UCLASS() class APatrolPath : public AActor { GENERATED_BODY() public: UPROPERTY(EditAnywhere) TArray<APatrolWaypoint*> Waypoints; UPROPERTY(EditAnywhere) bool bLooping = true; // 순환 경로 UPROPERTY(EditAnywhere) bool bReversible = false; // 핑퐁 경로 APatrolWaypoint* GetWaypoint(int32 Index) const { if (Waypoints.Num() == 0) return nullptr; return Waypoints[Index % Waypoints.Num()]; } int32 GetNextIndex(int32 Current, bool& bReversed) const { if (bReversible) { if (!bReversed) { if (Current >= Waypoints.Num()-1) bReversed = true; } else { if (Current <= 0) bReversed = false; } return bReversed ? Current - 1 : Current + 1; } return bLooping ? (Current + 1) % Waypoints.Num() : FMath::Min(Current + 1, Waypoints.Num() - 1); } };

bReversible(핑퐁) 모드는 AI가 경로 끝에 도달하면 역순으로 되돌아갑니다. 복도 경비 AI처럼 왕복 순찰이 필요한 경우에 유용합니다.

02

순찰 BT 구현

Behavior Tree로 순찰 로직 구성

순찰 AI의 BT는 Waypoint 이동 → 대기 → 다음 Waypoint의 루프로 구성됩니다. Service가 현재 Waypoint를 Blackboard에 업데이트하고, Task가 이동과 대기를 수행합니다.

C++// 순찰 Service: 다음 Waypoint 업데이트 UCLASS() class UBTService_UpdatePatrolPoint : public UBTService { GENERATED_BODY() protected: virtual void TickNode( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override { AAIController* Controller = OwnerComp.GetAIOwner(); UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent(); // PatrolPath가 설정되어 있는지 확인 APatrolPath* Path = Cast<APatrolPath>( BB->GetValueAsObject(TEXT("PatrolPath"))); if (!Path) return; int32 CurrentIndex = BB->GetValueAsInt(TEXT("PatrolIndex")); APatrolWaypoint* Waypoint = Path->GetWaypoint(CurrentIndex); if (Waypoint) { BB->SetValueAsVector(TEXT("PatrolLocation"), Waypoint->GetActorLocation()); BB->SetValueAsFloat(TEXT("WaitTime"), Waypoint->WaitTime); } } }; // 순찰 Task: Waypoint 도착 후 처리 UCLASS() class UBTTask_AdvancePatrol : public UBTTaskNode { GENERATED_BODY() protected: virtual EBTNodeResult::Type ExecuteTask( UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override { UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent(); APatrolPath* Path = Cast<APatrolPath>( BB->GetValueAsObject(TEXT("PatrolPath"))); int32 CurrentIndex = BB->GetValueAsInt(TEXT("PatrolIndex")); bool bReversed = BB->GetValueAsBool(TEXT("PatrolReversed")); // 다음 인덱스로 진행 int32 NextIndex = Path->GetNextIndex(CurrentIndex, bReversed); BB->SetValueAsInt(TEXT("PatrolIndex"), NextIndex); BB->SetValueAsBool(TEXT("PatrolReversed"), bReversed); return EBTNodeResult::Succeeded; } }; // BT 구조: // Sequence [순찰 루프] // ├─ Service: UpdatePatrolPoint (Interval: 0.5) // ├─ Task: MoveTo(BB: PatrolLocation) // ├─ Task: WaitBlackboardTime(BB: WaitTime) // └─ Task: AdvancePatrol (인덱스 증가)
참고

Sequence 아래에 Decorator: Loop를 추가하면 순찰이 무한 반복됩니다. Loop Decorator 없이도 상위 Selector에서 다른 브랜치가 실패하면 순찰 브랜치가 반복 실행됩니다.

03

경계(Alert) 상태 시스템

소음/적 감지 시 경계 모드 전환

순찰 AI는 적을 직접 발견하기 전에 경계(Alert) 상태를 거칩니다. 소음을 듣거나 의심스러운 징후를 감지하면 경계 모드로 전환하여 수색 행동을 수행합니다.

C++// AI 상태 열거형 UENUM() enum class EAIState : uint8 { Patrol, // 정상 순찰 Alert, // 경계 (수색) Chase, // 추적 Combat, // 전투 Return, // 순찰 복귀 }; // 경계 상태 전환 로직 void AMyAIController::OnTargetPerceptionUpdated( AActor* Actor, FAIStimulus Stimulus) { UBlackboardComponent* BB = GetBlackboardComponent(); if (Stimulus.Type == UAISense::GetSenseID<UAISense_Hearing>()) { // 소음 감지 → 경계 상태 BB->SetValueAsEnum(TEXT("AIState"), static_cast<uint8>(EAIState::Alert)); BB->SetValueAsVector(TEXT("InvestigateLocation"), Stimulus.StimulusLocation); } else if (Stimulus.Type == UAISense::GetSenseID<UAISense_Sight>()) { if (Stimulus.WasSuccessfullySensed()) { // 적 시각 감지 → 전투/추적 BB->SetValueAsEnum(TEXT("AIState"), static_cast<uint8>(EAIState::Combat)); BB->SetValueAsObject(TEXT("TargetActor"), Actor); } } } // BT 구조 (상태 기반): // Selector // ├─ Sequence [전투] // │ ├─ Decorator: AIState == Combat // │ │ (ObserverAborts: LowerPriority) // │ └─ SubTree: CombatBehavior // ├─ Sequence [추적] // │ ├─ Decorator: AIState == Chase // │ │ (ObserverAborts: LowerPriority) // │ └─ SubTree: ChaseBehavior // ├─ Sequence [경계 수색] // │ ├─ Decorator: AIState == Alert // │ │ (ObserverAborts: LowerPriority) // │ ├─ Task: MoveTo(InvestigateLocation) // │ ├─ Task: LookAround (3초) // │ └─ Task: SetBBValue(AIState = Return) // ├─ Sequence [순찰 복귀] // │ ├─ Decorator: AIState == Return // │ ├─ Task: MoveTo(PatrolLocation) // │ └─ Task: SetBBValue(AIState = Patrol) // └─ Sequence [순찰] // └─ SubTree: PatrolBehavior

경계 수색 후 적을 발견하지 못하면 Return 상태를 거쳐 마지막 순찰 위치로 복귀합니다. 즉시 순찰로 전환하면 AI가 순찰 경로의 중간부터 다시 시작하여 부자연스럽습니다.

04

EQS 기반 랜덤 순찰

Waypoint 없이 EQS로 동적 순찰

사전 배치된 Waypoint 대신 EQS를 사용하여 매번 새로운 순찰 위치를 동적으로 선택할 수 있습니다. 오픈 월드처럼 넓은 맵에서 미리 Waypoint를 배치하기 어려울 때 유용합니다.

C++// EQS 기반 순찰 포인트 생성 // EQS 쿼리: FindPatrolPoint // Generator: SimpleGrid (AI 주변 격자) // GridSize: 1500 // SpaceBetween: 300 // ProjectToNavigation: true // Test 1: Distance to Self // Score: Linear (Weight: -1) // → 적당히 먼 곳 선호 (너무 가까운 곳 회피) // Filter: Min 500, Max 2000 // Test 2: Trace to Self // Score: InverseLinear (Weight: 0.3) // → 시야가 다양한 곳 약간 선호 // Test 3: Distance to LastPatrolPoint // Score: Linear (Weight: 0.5) // → 이전 순찰 지점에서 먼 곳 선호 (새로운 곳) // RunMode: RandomBest25Pct // → 상위 25% 중 랜덤 → 매번 다른 위치 // BT 구조 (EQS 순찰): // Sequence // ├─ Task: RunEQSQuery(FindPatrolPoint) // │ → BB: PatrolLocation // ├─ Task: MoveTo(BB: PatrolLocation) // ├─ Task: Wait(Random 2~5초) // └─ Task: SaveLastPatrolPoint // → BB: LastPatrolPoint = PatrolLocation // 추가 최적화: AI 밀집 방지 // EQS Test: Distance to AllAllies (Context) // Score: Linear (Weight: 0.3) // → 다른 AI에게서 먼 위치 선호 // → AI끼리 같은 위치에 모이지 않음 // 영역 제한: SmartObject 태그 활용 // 순찰 영역을 NavModifierVolume이나 // SmartObject로 정의하면 // AI별로 다른 순찰 영역 할당 가능
핵심

EQS 순찰의 핵심은 RandomBest25Pct RunMode입니다. SingleBestItem을 사용하면 AI가 항상 같은 최적 위치로만 가지만, 상위 25% 중 랜덤 선택하면 자연스럽고 다양한 순찰 경로가 만들어집니다.

SUMMARY

핵심 요약

  • Waypoint 시스템으로 순환/핑퐁/단방향 순찰 경로를 정의하고, 각 Waypoint에 대기 시간과 행동을 설정한다
  • BT에서 Service가 Waypoint를 업데이트하고, MoveTo/Wait/AdvancePatrol Task가 순찰 루프를 구성한다
  • 경계(Alert) 상태를 도입하여 소음 감지 시 수색 → 복귀의 자연스러운 전환을 구현한다
  • EQS 기반 랜덤 순찰은 Waypoint 없이 동적 순찰을 구현하며, RandomBest25Pct로 다양성을 확보한다
PRACTICE

도전 과제

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

실습 1: 웨이포인트 순찰 구현

Spline 또는 Actor 배열 기반의 순찰 경로를 만들고, BT에서 MoveTo 태스크로 순서대로 이동하는 순찰 AI를 구현하세요. 마지막 웨이포인트 도달 시 처음으로 루프합니다.

실습 2: 랜덤 순찰 패턴

EQS를 활용하여 현재 위치에서 일정 반경 내 랜덤한 NavMesh 위치로 이동하는 순찰 패턴을 구현하세요. GetRandomReachablePointInRadius를 활용합니다.

심화 과제

플레이어가 자주 출현하는 지역을 학습하여 순찰 빈도를 높이는 적응형 순찰 시스템을 구현하세요. Blackboard에 위협 히트맵을 저장하고, EQS에서 위협이 높은 지역을 우선 순찰하도록 합니다.