BulletAnt 개발일지 (24) - Nav Aborted, AI 최종 로직

김펭귄·4일 전

Today What I Learned (TIL)

목록 보기
139/139

Nav Aborted 문제

적이 코어를 향해 이동하다가 플레이어를 인식하면, 타겟을 플레이어로 변경해 추격하도록 구현했다.
그런데 플레이어가 Nav위에 있지 않으면 경로탐색에서 문제가 생겼다.

1. 플레이어가 Nav위에 잘 있는 경우

플레이어가 건물 위지만, NavMesh가 일부만 연결된 곳에 있을 경우는 괜찮다.

이 상황에서는 MoveToActor() 호출 시 Partial Path가 생성되었고, 적은 가능한 지점까지 이동했다. 이후 부분 경로 끝에 도착하면 Success가 반환되었고, 기존 로직대로 공격 State로 정상 전환되었다.

2. NavMesh 밖 타겟 추적 시

문제는 플레이어가 NavMesh가 존재하지 않는 위치에 있을 경우였다.

이 경우 MoveToActor()를 호출하자마자 즉시 Failed를 반환했다.

StateTree 로직에서는 이동 요청 결과가 Failed이면 Task 역시 Failed를 반환하도록 구현했었는데, 문제는 그 이후 흐름이었다.

Move State → Failed
→ Rotate State
→ 다시 플레이어 인식
→ Move State 재진입
→ MoveToActor 실패
→ 반복

결과적으로 실제 이동은 전혀 하지 않으면서 State 전환만 반복하며 CPU를 계속 사용하고 있었다.

다만 플레이어가 다시 NavMesh 영역 안으로 들어오면, 그 순간부터는 정상적으로 RequestSuccessful이 반환되어 추격이 가능했다.

3. 추적 중 NavMesh를 벗어나면 Aborted 발생

또 다른 문제는 추적 도중 타겟이 NavMesh를 벗어나는 경우였다.

예를 들어:

  • 플레이어가 건물 위에서 Nav 영역 밖으로 이동하거나
  • 제트팩으로 공중으로 올라가는 경우

중간에 이동이 중단되며 Move 완료 콜백에서 Aborted가 반환되었다.

문제는 당시 Aborted 상황에 대한 처리를 전혀 하지 않았다는 점이었다.

그래서 결과적으로:

  • Move State는 Running 상태 유지
  • 이동은 멈춤
  • 새로운 행동도 하지 않음

상태로 AI가 멈춰버렸다.

해결 방법

결국 문제의 핵심은 동일했다.

  • 이동 요청 직후 Failed
  • 이동 도중 Aborted

두 경우 모두 “현재 타겟이 Nav 기반 이동이 불가능한 상태”라는 의미였다.

그래서 해결 방식도 통합했다.

OnTargetNavAborted() 함수를 호출하여 다음 작업을 수행하도록 했다.

  1. 현재 타겟 무시
  2. 타겟을 다시 Core로 초기화
  3. Priority를 Max로 복구

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 기반 재인식

무한정 무시하면 안 되므로, 일정 시간이 지나면 다시 인식 가능하도록 했다.

각 요소에 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() 사용 시 요소 위치가 바뀌더라도 누락 없이 검사하기 위함이다.

최종 흐름

결과적으로 현재 구조는 다음처럼 동작한다.

  1. MoveToActor() 실패 또는 이동 중 Aborted
  2. OnTargetNavAborted() 호출
  3. 현재 타겟을 Ignore 목록에 추가
  4. 코어를 기본 타겟으로 재설정
  5. StateTree 재시작
  6. 일정 시간 후 다시 타겟 인식 가능

덕분에:

  • Nav 밖 타겟을 무한 재탐색하지 않게 되었고
  • AI 멈춤 현상도 해결되었으며
  • CPU 낭비 역시 크게 줄일 수 있었다.

AI 행동 흐름 정리

이렇게 해서 전투에 생동감을 줄 수 있는 지능적이고 전술이 요구되는 AI 로직을 구현할 수 있었다.

1. 초기 상태

  • 모든 적은 시작 시:

    • 타겟 = Core
    • 우선순위 = Max(최하)

2. 코어 이동

경로 존재

  • NavMesh 따라 코어로 이동

Partial Path만 존재

  • 갈 수 있는 최대 지점까지 이동
  • Success 처리

경로 자체가 없음

  • 앞을 막는 건물/오브젝트를 타겟으로 변경
  • 공격하며 길 확보

이동 중 Nav가 바뀌면 경로도 다시 계산됨

3. 타겟 탐색

이동 중 주변 대상 탐색:

  • 플레이어
  • 건물
  • 포탑 등

각 적/종족마다 우선순위 테이블이 다르며,
현재보다 우선순위 높은 대상 발견 시:

  1. 이동 중단
  2. 타겟 변경
  3. StateTree 재추격

4. Nav 불가능 타겟 처리

다음 상황 발생 시:

  • Nav 밖 플레이어
  • 공중 타겟
  • Failed / Aborted

처리:

  • 타겟을 Core로 초기화
  • StateTree 재시작

5. 공격 진입

다음 두 경우 모두 공격 상태 진입:

  • 실제 타겟 도착
  • Partial Path 끝 도착

즉:

“완전 도착”보다
“공격 가능한 거리 접근”을 기준으로 판단

6. 타겟 제거 시

타겟이 파괴되거나 EndOverlap 발생 시:

  • Intrude 상태 진입
  • 일정 시간 다른 대상 무시
  • 코어 방향으로 강제 이동
profile
반갑습니다

0개의 댓글