BulletAnt 개발일지 (16) - 경로 탐색 트러블슈팅(ProjectPointToNavigation), Intrude

김펭귄·4일 전

Today What I Learned (TIL)

목록 보기
131/139

NavMesh 위 목적지 보정 문제

적 크기별 Recast Nav를 적용한 이후 새로운 문제가 발생했다.
플레이어 추적은 정상적으로 동작했지만 Core, 건물로는 이동하지 못하는 현상이 발생했다.

처음에는 경로 자체 문제라고 생각했는데, Visual Logger를 확인해보니 원인이 바로 보였다.

EndPosition is not on NavMesh

즉 목적지 자체가 NavMesh 위에 존재하지 않았다.
이전까진 문제없었지만, 여러 Recast Agent를 적용한 이후에는 상황이 달라졌다.
특히 대형몹이 사용하는 Big RecastNavMesh의 경우 NavMesh가 Core 외곽보다 더 멀리 생성되면서, 기존 목적지가 Nav 영역과 멀리 떨어지게 되었다.

투영으로 해결

그래서 목적지 좌표를 강제로 NavMesh 위로 투영시키는 방식으로 수정했다.
언리얼에서 제공하는 ProjectPointToNavigation() 함수를 사용했다.
이 함수는 가장 가까운 Nav 위치를 탐색한 뒤 해당 지점으로 투영시켜준다.

다만 여기서 중요한 점이 있었다.
적마다 자기 크기때문에 사용하는 Nav Agent가 다르기 때문에, 함수 호출 시 FNavAgentProperties를 함께 넘겨 해당 적의 Agent 정보를 사용하도록 구성했다.

추가로 탐색 범위도 중요했다.
ProjectPointToNavigation()은 내부적으로 주변 Nav Tile 탐색하기 때문에 탐색범위가 너무 크면 탐색비용이 증가한다.

그래서 각 적의 Capsule 크기 정도만 탐색 범위로 사용하도록 수정했다.

결과적으로:

  • 작은 적
  • 큰 적

모두 자신의 NavMesh 기준으로 올바른 목적지를 찾게 되었고,
Core와 건물 방향 이동도 정상적으로 동작하는 것을 확인할 수 있었다.

bool UMoveToLoc::CanTargetLocProject(const FVector& Point, FNavLocation& OutLocation)
{
	UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent<UNavigationSystemV1>(GetWorld());
	if (IsValid(NavSys))
	{
		float Radius = ContextEnemy->GetCapsuleComponent()->GetScaledCapsuleRadius();
		float HalfHeight = ContextEnemy->GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
		const FNavAgentProperties& AgentProps = ContextEnemy->GetNavAgentPropertiesRef();
		return NavSys->ProjectPointToNavigation(Point, 
        										OutLocation,
                                                FVector(Radius * 1.5f, 
                                                		Radius * 1.5f,
                                                        HalfHeight * 2.5), 
                                                &AgentProps);
	}
	return false;
}

건물 파괴 후 Intrude 상태

처음엔 적이 타겟 건물을 부쉈을 때 무조건 안으로 파고드는 Intrude 상태를 새로 만들었었다.
하지만 기존 로직과 다른 점은 일정 시간동안 타겟 우선순위만 High로 높여 다른 타겟을 인지하지 않는 것이었다.
즉 상태를 새로 만드는 건 오히려 과하다고 판단했다.

그래서 건물을 파괴하면

  • 일정 시간 동안 Priority를 High로 변경
  • 타겟을 Core로 강제 설정
  • 주변 감지 무시

하도록 구성했다.
그리고 일정 시간이 지나면 Priority를 다시 낮추고(Max), 다시 타겟을 인지할 수 있도록 하였다.

구현 방법

건물 파괴 여부는 별도 Delegate가 아니라 인지하고 있던 타겟이 EndOverlap되었을 때 처리했다.

void ABaseEnemyCharacter::OnDetectionSphereEndOverlap(/**/)
{
	if (TargetActor == OtherActor)	// EndOverlap 대상이 타겟인 경우
	{
		InitTarget();				// 타겟 초기화
		StartIntrudeAction();		// Intrude 활성화
		TransitionToRotate();		// 타겟을 향해 회전 시작(StateTree재시작)
	}
}

Intrude 상태 관리는 GE 기반으로 구현했다.
건물 파괴 시:

  • GE_BEIntrude을 적용하고
  • GE에서 State.Movement.Intrude Tag를 적에게 부여한다.

GE에는 Duration을 설정하여 일정시간동안만 적용될 수 있게 하였다.
값을 사용했다.

다만 처음에는 Duration이 제대로 적용되지 않았다.
원인을 보니 GE Duration 타입을 Scalable Float으로 설정해두었는데,
이 방식은 GE 내부 값만 사용한다.
즉 외부 코드에서 동적으로 시간을 넣을 수 없었다.

그래서 Duration 방식을 SetByCaller로 변경한 후, 코드로 Duration값을 넘겨주는 방식으로 변경하였다.

void ABaseEnemyCharacter::StartIntrudeAction()
{
	TargetActorPriority = ETargetPriorityType::High;		// Priority Max로 설정

	EffectContext.AddSourceObject(this);
	SpecHandle = AbilitySystemComponent->MakeOutgoingSpec(BaseEnemyDataAsset->IntrudeEffect,
    													  1.0f, 
                                                          EffectContext);

	if (SpecHandle.IsValid())
	{
		SpecHandle.Data.Get()->SetSetByCallerMagnitude(TAG_Intrude, 
        											   BaseEnemyDataAsset->IntrudeTime);
		GEIntrudeHandle = AbilitySystemComponent->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data.Get());
	}
}

추가로 GE 제거 시점도 중요했다.
일정 시간이 지나고 Intrude가 끝나면 다시 Priority를 원래 값으로 복구해야 했기 때문이다.
그래서 OnGameplayEffectRemoved_InfoDelegate()를 사용해 GE 제거 콜백을 바인딩했다.

// ABaseEnemyCharacter::StartIntrudeAction()
AbilitySystemComponent->OnGameplayEffectRemoved_InfoDelegate(GEIntrudeHandle)
						->AddUObject(this, &ABaseEnemyCharacter::FinishIntrudeAction);

void ABaseEnemyCharacter::FinishIntrudeAction(const FGameplayEffectRemovalInfo& InGERemovalInfo)
{
	TargetActorPriority = ETargetPriorityType::Max;
}

GE Stack

위 사진같이 설정해서 각 적 객체마다 GE_Intrude는 최대 한 개씩만 적용가능하게끔 하였고,
연속으로 건물을 부실경우 Intrude의 지속상태가 초기화되도록 하였다.

profile
반갑습니다

0개의 댓글