UDN
Search public documentation:

NavMeshDynamicObstacleSplittingKR
English Translation
日本語訳
中国翻译

Interested in the Unreal Engine?
Visit the Unreal Technology site.

Looking for jobs and company info?
Check out the Epic games site.

Questions about support via UDN?
Contact the UDN Staff

UE3 홈 > AI와 내비게이션 > 내비게이션 메시 동적 장애물 분할 기술적 개요

내비게이션 메시 동적 장애물 분할 기술적 개요


문서 변경내역: Matt Tonks 작성. James Tan 수정. 홍성진 번역.

개요


동적으로 변경되는 상황에 따라 실행시간에 내비게이션 메시(와 AI 내비게이션 상태)에 영향을 끼칠 수 있었으면 하는 상황이 종종 있습니다. 여기서는 내비게이션 메시 시스템으로 그 작업이 어떻게 이루어지나 알아보겠습니다.

하이 레벨 개요


기본 개념은 Interface_NavMeshPathObstacle (인터페이스_내비 메시 패쓰 장애물)은 내비게이션 메시를 어떤 모양으로 분할할지를 나타내는 인터페이스를 사용자에게 제공합니다. AI 가 돌아다닐 때 주의를 해야 하는 장애물이 게임 월드에 새로 들어설 때마다, 사용자는 RegisterObstacleWithNavMesh() (장애물을 내비 메시로 등록 함수)를 호출하고, 그러면 기존 메시의 폴리곤 중 어느 것이 이 장애물에 영향을 받는지 판단하여, 필요하다면 제공된 모양으로 메시를 분할합니다.

위 이미지에서는 폴리곤 넷으로 구성된 기본 메시가 큐브 모양을 한 장애물 모양 주변으로 나뉩니다. 장애물 모양은 이보다 복잡해도 되며, 축을 맞추지 않아도 됩니다. 단지 볼록한 모양이기만 하면 됩니다.

계층구조

메시가 장애물 주변으로 나뉠 때 실제로 벌어지는 작업은, 기존 폴리곤 자리에 새로운 메시를 만드는 것입니다. 이런 식으로 AI 가 영향받은 폴리곤을 사용할 때마다 찾아보는 일종의 계층구조를 형성합니다. 중간 (또는 사칭) 메시를 갖는 폴리곤을 통해 길찾기를 할 때는, 그냥 그 폴리곤의 에지에서 길찾기를 하기 보단, 해당 서브메시를 찾아보게 됩니다.

요청시 빌드

AI 가 장애물에 영향받은 것으로 태깅된 폴리곤을 통해 길찾기 요청을 할 때, 해당 장애물 주변으로 폴리곤이 분할되지 않은 상태라면 바로 그 때 분할시킵니다. 즉 장애물을 등록하자마자 분할하는 것이 아닌데, 그 이유는 장애물 등록 비용을 어떻게든 경감시키기 위함입니다. 예를 들어 등록과 해제가 매우 자주 일어나는 장애물이 있는 경우라면, 이런 비싼 장애물 처리는 AI 가 실제로 영향받은 메시 데이터에 있을 때만 해 주면 될 것입니다.

추가적으로 폴리곤이 분할되고 나면, 해당 지오메트리를 통하는 길찾기에 관련된 추가 비용은 발생하지 않습니다. 즉 장애물 등록에 있어 계산 비용은 딱 한 번만 일어난다는 뜻입니다. 또한 메시의 부모 구조가 바뀌고 있지는 않으므로, 장애물이 제거됐을 때 원래 상태로 돌리는 비용은 거의 공짜입니다.

구현 세부사항


패쓰 장애물의 매우 간단한 예제로 NavMeshObstacle 를 봅시다.

여기서 보면 장애물을 켜고/끄는 키즈멧 로직을 몇 가지 볼 수 있습니다만, 재밌는 부분은 여깁니다:

NavMeshObstacle.uc
cpptext
{
  /**
   * 이 함수는 out_polyshape 를 이 오브젝트의 볼록 경계 모양을 나타내는
   * 버텍스 리스트로 채웁니다.
   * @param out_PolyShape - 이 장애물의 경계 폴리모양에 대한 버텍스 버퍼를 담는 출력 배열입니다.
   * @return 참이면 이 오브젝트는 바로 장애물 역할을 합니다. (거짓이면 이 장애물은 메시에 영향을 끼치지 않습니다.)
   */
  virtual UBOOL GetBoundingShape(TArray<FVector>& out_PolyShape);

  virtual UBOOL PreserveInternalPolys() { return TRUE; }
}

매우 기본적인 패쓰 장애물입니다. 현재 이 모든 장애물이 그 모양을 따라 메시를 분할하려 하고 있습니다.

이 스크린 샷에서는 예제 패쓰 오브젝트를 껐다(unregister)가 켠다(register) 간주해 봅시다.
ALERT! 주: 아래 스크린 샷에는 PreserveInternalGeo()켜져 있는데, 이에 대해서는 나중에 자세히 알아 봅시다.

아직 메시와 등록되지 않은 월드의 장애물입니다. 메시는 안에 장애물이 놓여 있는 커다란 폴리곤 하나로 구성되어 있습니다.

(실행시간에) 장애물이 등록되고 메시가 분할된 이후의 메시입니다:

이 예제 오브젝트가 정의한 함수를 둘 다 살펴 봅시다.

GetBoundingShape()

바운딩 모양 구하기 함수, 이 오브젝트의 구현은 이렇습니다.

UBOOL ANavMeshObstacle::GetBoundingShape(TArray<FVector>& out_PolyShape)
{
  out_PolyShape.AddItem(Location + FRotationMatrix(Rotation).TransformFVector(FVector(200.f,200.f,0.f)));
  out_PolyShape.AddItem(Location + FRotationMatrix(Rotation).TransformFVector(FVector(-200.f,200.f,0.f)));
  out_PolyShape.AddItem(Location + FRotationMatrix(Rotation).TransformFVector(FVector(-200.f,-200.f,0.f)));
  out_PolyShape.AddItem(Location + FRotationMatrix(Rotation).TransformFVector(FVector(200.f,-200.f,0.f)));
  return TRUE;
}

보시다시피 그저 단순한 사각형을 제공합니다. 이 함수가 하는 일이라곤, 장애물의 경계를 나타내는 폴리곤을 제공하는 것 뿐입니다. 장애물이 스태틱 메시인 경우, 메시의 경계를 사용하여 위와 비슷한 작업을 해 주면 됩니다.

한 가지 기억할 것은, 이 함수는 장애물 등록 이후 메시가 처음 분할될 때 딱 한번만 호출된다는 것입니다. 장애물이 등록되고 그 주위로 메시가 분할되고 나면, 장애물은 등록을 해제한 후 재등록할 때까지 고려되지 않습니다.

PreserveInternalPolys()

내부 폴리곤 보존 함수, 이 함수가 하는 일이라곤, 분할 프로세스더러 장애물 안에 있는 지오메트리를 그 자리에 유지시킬지 일러주는 것이 전부입니다. 위의 스크린 샷에서 보면, 장애물 안에 지오메트리가 유지되고, 나머지 메시와 연결된 것을 볼 수 있습니다.

패쓰 장애물에 있어 마지막 한가지 중요한 부분은, 장애물 내부의 폴리곤과 (내부 지오메트리만 보존하는 장애물의) 외부 사이에 추가할 에지 종류를 지시하는 기능입니다. 장애물 내부의 폴리곤에서 외부의 폴리곤으로 링크(에지)가 만들려 할 때마다, 그 장애물의 AddObstacleEdge() (장애물 에지 추가 함수)를 호출하여 그 작업을 합니다.

이렇게 하면 사용자가 행위를 덮어쓰고, 장애물에 적합한 에지 종류를 무엇이든 추가할 수 있습니다. 기본 행위는 단지 모든 AI 가 패쓰 장애물의 내외부를 오갈 수 있다 일러주는 보통 에지를 추가하는 것입니다. 그러나 패쓰 장애물을 들어가거나 나갈 때 독특한 행위를 하도록 하려는 경우엔, Inteface_NavMeshPathObject 도 구현하는 것이 좋을 수 있습니다.

Interface_NavMeshPathObject 와 함께 사용


흔히 AddObstacleEdge() 를 덮어쓰게 되는 경우는, Path 오브젝트 에 링크되는 에지를 추가할 때입니다. 예를 들면 Interface_NavMeshPathObstacleInterface_NavMeshPathObject 둘 다 구현하는 장애물을 만들 수 있습니다.

이게 왜 유용한지를 그려보려면, 레벨이 시작된 이후 얼마 있다가 바닥에 뿌려진 기름 웅덩이를 생각해 보면 됩니다.

기름 웅덩이에 대해 필요한 작업은:

  • AI 가 웅덩이를 돌아가도록 하기 위해, 웅덩이 모양 주위로 스태틱 내비게이션 메시를 분할시켜야 합니다.
  • AI 가 웅덩이를 피할 수 있다면 그러도록 하기 위해, 웅덩이에 들어갈 때의 비용을 추가시키는 방법을 강구해야 합니다.

즉 이 경우, 위의 개요대로 Interface_NavMeshPathObstacle 을 구현하여 메시에 장애물 모양을 등록한 다음, Interface_NavMeshPathObject 도 구현하여 웅덩이 내부와 외부를 연결하는 에지에 대한 인터페이스를 마련, 그 에지를 가로지르는 비용을 수정할 수 있도록 합니다.

즉 위의 목록은 실제로 다음과 같이 풀어볼 수 있습니다:

  • Interface_NavMeshPathObstacle::GetBoundingShape() 구현
  • 적절한 시간에 장애물 등록
  • AddObstacleEdge() 를 덮어써서 보통의 에지 대신 기름 웅덩이에 연결된 PathObject 에지를 추가
  • Interface_NavMeshPathObject::CostFor() 구현 (Nav Mesh Path Objects KR 참고)

AddObstacleEdge()


지금까지 자세히 다루지 않은 것은 AddObstacleEdge(), 장애물 에지 추가 함수 뿐이니, 이 함수를 자세히 들여다 봅시다.

/**
 * 이 함수는 이 장애물 내부 폴리곤과 외부 폴리곤을 잇는 에지가 추가될 때 호출됩니다.
 * 기본 행위는 그냥 보통 에지 추가이고, (패쓰 오브젝트를 장애물에 연결한다든가) 특별한 비용이나 행위를 추가하려면 덮어씁니다.
 * @param Status - 현재 에지의 상태 (, 추가가 필요한 것이 무엇인가 등)입니다.
 * @param inV1 - 에지의 첫째 버텍스 위치입니다.
 * @param inV2 - 에지의 둘째 버텍스 위치입니다.
 * @param ConnectedPolys - 이 에지가 연결되는 폴리곤입니다.
 * @param bEdgesNeedToBeDynamic - 추가된 에지가 동적이어야 하는지 (, 즉 다른 메시에 에지를 추가하는지) 입니다.
 * @param PolyAssocatedWithThisPO - 연결된 폴리곤 배열 파라미터의 인덱스로, 그 배열의 어느 폴리곤이 이 패쓰 오브젝트에 관련되었는지를 나타냅니다.
 * @return 방금 무슨 일이 벌어졌는지 (어떤 행동을 취했는지)를 나타내는 열거형을 반환합니다.
 *         다른 장애물에 의해 어떤 동반 행동을 취해야 하는지 결정하고 코드를 호출하는 데 사용됩니다.
 */
virtual EEdgeHandlingStatus AddObstacleEdge( EEdgeHandlingStatus Status, const FVector& inV1, const FVector& inV2, TArray<FNavMeshPolyBase*>& ConnectedPolys, UBOOL bEdgesNeedToBeDynamic, INT PolyAssocatedWithThisPO);

메시 분할이 완료된 이후, 새로 생긴 폴리곤 주변으로 동적 분할 코드를 실행하여, 그 새 폴리곤을 주변 월드에 연결시켜 봅니다. 연결할 만한 폴리곤을 찾으면, 제안된 에지를 가지고 이 함수를 호출하고난 후, PathObstable 은 적절한 행동 방침을 결정합니다.

이 함수의 가장 까다로운 부분은, 다른 방향에 에지가 추가되었는지를 알려주는 성가신 Status 파라미터가 따라붙는다는 것입니다. 일방통행 에지가 필요한 경우가 있기 때문인데요. 예를 들어 AI 가 패쓰 장애물을 나가기는 해도 되돌아 들어가지는 못하게 할 에지가 있을 수 있습니다.

결과적으로 에지를 추가할 때는 방금 무엇을 했는지를 반환해 주어, 호출하는 코드가 다른 방향에도 디폴트 에지를 추가해 줘야 하는지를 알 수 있도록 해야 합니다. 물론, 두 개의 PathObstacle 를 서로 가까이 두어, 둘 다에 커스텀 에지를 추가하는 것도 가능합니다.

디폴트 구현을 살펴보면:

EEdgeHandlingStatus IInterface_NavMeshPathObstacle::AddObstacleEdge( EEdgeHandlingStatus Status, const FVector& inV1, const FVector& inV2, TArray<FNavMeshPolyBase*>& ConnectedPolys, UBOOL bEdgesNeedToBeDynamic, INT PolyAssocatedWithThisPO)
{
  return EHS_AddedNone;
}

아무것도 하지 않았다고 나타난 것을 볼 수 있으니, 코드를 호출하면 폴리곤 사이에 디폴트 에지를 추가해야 함을 압니다.

이제 패쓰 장애물 자신에게 연결된 패쓰 오브젝트 에지를 추가하는 예제를 살펴보겠습니다.

EEdgeHandlingStatus UAIAvoidanceCylinderComponent::AddObstacleEdge( EEdgeHandlingStatus Status, const FVector& inV1, const FVector& inV2, TArray<FNavMeshPolyBase*>& ConnectedPolys, UBOOL bEdgesNeedToBeDynamic, INT PolyAssocatedWithThisPO)
{
  // 에지를 추가하려는 방향에 이미 에지가 추가되어 있으면, 패쓰 장애물 충돌이 있을 수 있습니다.
  // (즉 이미 에지가 추가된 다른 장애물에 들이받혔으니, 그냥 떠나는 거죠)
  if(Status == EHS_AddedBothDirs)
  {
    return Status;
  }

  // 이미 다른 폴리곤에서 이 PO 로 되돌아 들어가는 에지 포인트가 있는 경우, 떠납니다.
  if( (PolyAssocatedWithThisPO == 0 && Status == EHS_Added1to0) ||
    (PolyAssocatedWithThisPO == 1 && Status == EHS_Added0to1) )
  {
    return Status;
  }

  TArray<FNavMeshPolyBase*> ReversedConnectedPolys=ConnectedPolys;

  // 이 PO 에 관련된 폴리곤 속으로 다시 에지를 추가하고프니, 필요하다면 순서를 맞바꿉니다.
  if(PolyAssocatedWithThisPO == 0)
  {
    ReversedConnectedPolys.SwapItems(0,1);
  }

  UNavigationMeshBase* Mesh = ReversedConnectedPolys(0)->NavMesh;

  if( Mesh == NULL )
  {
    return Status;
  }

  // 메시에 에지를 추가합니다.
  TArray<FNavMeshPathObjectEdge*> CreatedEdges;
  Mesh->AddDynamicCrossPylonEdge<FNavMeshPathObjectEdge>(inV1,inV2,ReversedConnectedPolys,TRUE, &CreatedEdges);

  // 에지로의 리퍼런스를 잡아다가 이 패쓰 오브젝트에 바인딩합니다.
  FNavMeshPathObjectEdge* NewEdge = (CreatedEdges.Num() > 0) ? CreatedEdges(0) : NULL;
  checkSlowish(CreatedEdges.Num() <2);

  if(NewEdge == NULL)
  {
    return Status;
  }

  // 새 에지를 이 회피 볼륨에 바인딩합니다.
  if(NewEdge != NULL)
  {
    NewEdge->PathObject = GetOwner();
    NewEdge->InternalPathObjectID = 0;
  }

  // 대상 폴리에서 소스 폴리까지 에지를 추가했음을 나타냅니다.
  if(Status == EHS_AddedNone)
  {
    if(PolyAssocatedWithThisPO == 0)
    {
      return EHS_Added1to0;
    }
    else
    {
      return EHS_Added0to1;
    }
  }
  else
  {
    // 여기까지 왔다는 것은, 누군가 반대 방향에 이미 에지를 추가했다는 뜻입니다.
    return EHS_AddedBothDirs;
  }

}

이 경우 장애물은 보통의 에지 대신 자신에게 연결된 PathObject 에지를 추가하려 할 테지만, 오로지 장애물 바깥의 폴리곤에서 안의 폴리곤으로 연결할 때만 그렇습니다. 여기서의 로직 대부분은 그저 폴리곤 순서가 올바른지, 맞바뀐 곳은 없는지 등을 결정하여 에지가 올바른 방향으로 추가되도록 하는 것입니다.