적이 코어를 향해 이동하다가 플레이어를 인식하면, 타겟을 플레이어로 변경해 추격하도록 구현했다.
그런데 플레이어가 Nav위에 있지 않으면 경로탐색에서 문제가 생겼다.
플레이어가 건물 위지만, NavMesh가 일부만 연결된 곳에 있을 경우는 괜찮다.
이 상황에서는 MoveToActor() 호출 시 Partial Path가 생성되었고, 적은 가능한 지점까지 이동했다. 이후 부분 경로 끝에 도착하면 Success가 반환되었고, 기존 로직대로 공격 State로 정상 전환되었다.
문제는 플레이어가 NavMesh가 존재하지 않는 위치에 있을 경우였다.

이 경우 MoveToActor()를 호출하자마자 즉시 Failed를 반환했다.
StateTree 로직에서는 이동 요청 결과가 Failed이면 Task 역시 Failed를 반환하도록 구현했었는데, 문제는 그 이후 흐름이었다.
Move State → Failed
→ Rotate State
→ 다시 플레이어 인식
→ Move State 재진입
→ MoveToActor 실패
→ 반복
결과적으로 실제 이동은 전혀 하지 않으면서 State 전환만 반복하며 CPU를 계속 사용하고 있었다.
다만 플레이어가 다시 NavMesh 영역 안으로 들어오면, 그 순간부터는 정상적으로 RequestSuccessful이 반환되어 추격이 가능했다.
또 다른 문제는 추적 도중 타겟이 NavMesh를 벗어나는 경우였다.
예를 들어:
중간에 이동이 중단되며 Move 완료 콜백에서 Aborted가 반환되었다.
문제는 당시 Aborted 상황에 대한 처리를 전혀 하지 않았다는 점이었다.
그래서 결과적으로:
상태로 AI가 멈춰버렸다.
결국 문제의 핵심은 동일했다.
두 경우 모두 “현재 타겟이 Nav 기반 이동이 불가능한 상태”라는 의미였다.
그래서 해결 방식도 통합했다.
OnTargetNavAborted() 함수를 호출하여 다음 작업을 수행하도록 했다.
Priority를 다시 Max로 설정한 이유는, 이후 새로운 플레이어나 건물을 다시 정상적으로 인식할 수 있도록 하기 위함이다.
EStateTreeRunStatus UMoveToLoc::EnterState(/**/)
{
StartMoveToTarget();
else // 요청이 거절당한 경우 (Nav가 없음)
{
CachedAIController->ReceiveMoveCompleted.RemoveDynamic(this, &UMoveToLoc::OnMoveCompleted);
ContextEnemy->OnTargetNavAborted();
return EStateTreeRunStatus::Running;
}
}
void UMoveToLoc::OnMoveCompleted(/**/)
{
// 이 Task가 요청한 움직일 때만 처리
if (RequestID != CurrentRequestID)
{
return;
}
// 경로 문제
else
{
ContextEnemy->OnTargetNavAborted();
}
}
// BaseEnemyCharacter.cpp
void ABaseEnemyCharacter::OnTargetNavAborted()
{
InitTarget();
TransitionToRotate();
}
Nav를 벗어난 대상을 계속 인식하면 문제가 생기기 때문에, 무시하기 위해 자료 구조에 담아 관리하기로 하였다.
처음엔 TSet을 고려했다.
이유는:
때문이었다.
하지만 실제 상황을 생각해보니:
이라 굳이 해시 테이블 관리 비용을 들 필요가 없었다.
오히려 TArray가:
면에서 더 유리하다고 판단했다.
삭제도 RemoveAtSwap()을 사용하면 상수 시간으로 처리 가능하다.
IgnoredTargets.RemoveAtSwap(Index);
뒤 원소를 앞으로 당기지 않고 마지막 원소와 스왑 후 Size만 줄이는 방식이라 매우 가볍다.
무한정 무시하면 안 되므로, 일정 시간이 지나면 다시 인식 가능하도록 했다.
각 요소에 ExpireTime을 저장하고:
하도록 구현했다.
반복문은 뒤에서부터 순회했다.
for (int32 i = AbortedTargets.Num() - 1; i >= 0; --i)
{
AbortedTargets[i].ExpireTime++;
if (AbortedTargets[i].ExpireTime >= IgnoreDuration)
{
AbortedTargets.RemoveAtSwap(i, 1, EAllowShrinking::No);
}
}
RemoveAtSwap() 사용 시 요소 위치가 바뀌더라도 누락 없이 검사하기 위함이다.
결과적으로 현재 구조는 다음처럼 동작한다.
MoveToActor() 실패 또는 이동 중 AbortedOnTargetNavAborted() 호출덕분에:
이렇게 해서 전투에 생동감을 줄 수 있는 지능적이고 전술이 요구되는 AI 로직을 구현할 수 있었다.
모든 적은 시작 시:
Success 처리이동 중 Nav가 바뀌면 경로도 다시 계산됨
이동 중 주변 대상 탐색:
각 적/종족마다 우선순위 테이블이 다르며,
현재보다 우선순위 높은 대상 발견 시:
다음 상황 발생 시:
Failed / Aborted처리:
다음 두 경우 모두 공격 상태 진입:
즉:
“완전 도착”보다
“공격 가능한 거리 접근”을 기준으로 판단
타겟이 파괴되거나 EndOverlap 발생 시: