추적 AI
19강. 추적 AI
1. 이번 강의에서 해결할 문제
적이 플레이어를 한 번 발견하면 영원히 추적하거나, 벽 뒤로 숨는 순간 즉시 원래 위치로 돌아갑니다. 시야를 잃은 시간과 마지막 확인 위치를 관리합니다.
2. 학습 목표
3. 핵심 개념
추적은 매 프레임 플레이어를 찾는 기능이 아닙니다. Service가 제한된 주기로 거리·시야를 갱신하고 Blackboard Observer가 Branch를 바꿉니다.
플레이어가 잠깐 기둥 뒤로 숨었을 때 즉시 포기하면 부자연스럽습니다. 마지막으로 본 위치와 시간을 저장해 몇 초 동안 그 위치를 확인한 뒤 순찰로 돌아갑니다.
4. 단계별 실습
Blackboard 작업
LastKnownLocation Vector와 LastSeenTime Float Key를 추가합니다.
C++ · 파일 경로: Source/TopDownSurvival/AI/MathAIController.h · 공개 갱신 API
public:
void RefreshCombatContext();
protected:
UPROPERTY(EditDefaultsOnly, Category = "AI|Senses",
meta = (ClampMin = "0.0", Units = "s"))
float LoseTargetDelay = 2.0f;
C++ · 파일 경로: Source/TopDownSurvival/AI/Services/BTService_UpdateCombatContext.h
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTService.h"
#include "BTService_UpdateCombatContext.generated.h"
UCLASS()
class TOPDOWNSURVIVAL_API UBTService_UpdateCombatContext : public UBTService
{
GENERATED_BODY()
public:
UBTService_UpdateCombatContext();
protected:
virtual void TickNode(
UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory,
float DeltaSeconds) override;
};
C++ · 파일 경로: Source/TopDownSurvival/AI/Services/BTService_UpdateCombatContext.cpp
#include "AI/Services/BTService_UpdateCombatContext.h"
#include "AI/MathAIController.h"
UBTService_UpdateCombatContext::UBTService_UpdateCombatContext()
{
NodeName = TEXT("Update Combat Context");
Interval = 0.25f;
RandomDeviation = 0.05f;
}
void UBTService_UpdateCombatContext::TickNode(
UBehaviorTreeComponent& OwnerComp,
uint8* NodeMemory,
float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
if (AMathAIController* Controller =
Cast<AMathAIController>(OwnerComp.GetAIOwner()))
{
Controller->RefreshCombatContext();
}
}
C++ · MathAIController의 핵심 갱신
#include "Kismet/GameplayStatics.h"
void AMathAIController::RefreshCombatContext()
{
APawn* Player = UGameplayStatics::GetPlayerPawn(this, 0);
APawn* Enemy = GetPawn();
UBlackboardComponent* Blackboard = GetBlackboardComponent();
if (!Enemy || !Blackboard) return;
const bool bVisible = IsValid(Player) && HasClearSight(Player);
const float Now = GetWorld()->GetTimeSeconds();
if (bVisible)
{
Blackboard->SetValueAsObject(TEXT("TargetActor"), Player);
Blackboard->SetValueAsVector(
TEXT("LastKnownLocation"), Player->GetActorLocation());
Blackboard->SetValueAsFloat(TEXT("LastSeenTime"), Now);
}
const float LastSeen = Blackboard->GetValueAsFloat(TEXT("LastSeenTime"));
const bool bWithinMemory = Now - LastSeen <= LoseTargetDelay;
if (!bVisible && !bWithinMemory)
{
Blackboard->ClearValue(TEXT("TargetActor"));
}
const float Distance = IsValid(Player)
? FVector::Dist2D(Enemy->GetActorLocation(), Player->GetActorLocation())
: TNumericLimits<float>::Max();
Blackboard->SetValueAsFloat(TEXT("DistanceToTarget"), Distance);
Blackboard->SetValueAsBool(TEXT("HasLineOfSight"), bVisible);
if (bVisible)
{
SetAIState(DetermineState(Distance, true));
}
else
{
SetAIState(bWithinMemory
? EEnemyAIState::Chase
: EEnemyAIState::Patrol);
}
}
Service를 BT Root 아래 Selector에 연결합니다. Chase Branch는 TargetActor가 설정됐을 때 MoveTo TargetActor 또는 LastKnownLocation을 실행합니다.
5. 코드와 수학·AI가 동작하는 이유
Service Interval과 RandomDeviation은 여러 적의 Trace가 같은 프레임에 몰리는 것을 줄입니다. 보일 때만 마지막 위치와 시간을 갱신하고 일정 유예 뒤 목표를 지워 자연스러운 탐색 시간을 만듭니다.
6. 자주 하는 실수와 해결법
7. 직접 실습
8. 이해 점검 질문 3개
9. 핵심 요약
추적 AI 미니 퀴즈
선택 즉시 정답과 해설을 확인할 수 있습니다. 결과는 이 브라우저에만 저장됩니다.
학습을 마쳤나요?
직접 실습과 점검 질문까지 확인한 뒤 완료로 표시하세요.