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
日本語訳
中国翻译
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
내비게이션 메시 동적 장애물 분할 기술적 개요
문서 변경내역: Matt Tonks 작성. James Tan 수정. 홍성진 번역.
개요
동적으로 변경되는 상황에 따라 실행시간에 내비게이션 메시(와 AI 내비게이션 상태)에 영향을 끼칠 수 있었으면 하는 상황이 종종 있습니다. 여기서는 내비게이션 메시 시스템으로 그 작업이 어떻게 이루어지나 알아보겠습니다.
하이 레벨 개요
기본 개념은 Interface_NavMeshPathObstacle (인터페이스_내비 메시 패쓰 장애물)은 내비게이션 메시를 어떤 모양으로 분할할지를 나타내는 인터페이스를 사용자에게 제공합니다. AI 가 돌아다닐 때 주의를 해야 하는 장애물이 게임 월드에 새로 들어설 때마다, 사용자는 RegisterObstacleWithNavMesh() (장애물을 내비 메시로 등록 함수)를 호출하고, 그러면 기존 메시의 폴리곤 중 어느 것이 이 장애물에 영향을 받는지 판단하여, 필요하다면 제공된 모양으로 메시를 분할합니다.

계층구조
메시가 장애물 주변으로 나뉠 때 실제로 벌어지는 작업은, 기존 폴리곤 자리에 새로운 메시를 만드는 것입니다. 이런 식으로 AI 가 영향받은 폴리곤을 사용할 때마다 찾아보는 일종의 계층구조를 형성합니다. 중간 (또는 사칭) 메시를 갖는 폴리곤을 통해 길찾기를 할 때는, 그냥 그 폴리곤의 에지에서 길찾기를 하기 보단, 해당 서브메시를 찾아보게 됩니다.요청시 빌드
AI 가 장애물에 영향받은 것으로 태깅된 폴리곤을 통해 길찾기 요청을 할 때, 해당 장애물 주변으로 폴리곤이 분할되지 않은 상태라면 바로 그 때 분할시킵니다. 즉 장애물을 등록하자마자 분할하는 것이 아닌데, 그 이유는 장애물 등록 비용을 어떻게든 경감시키기 위함입니다. 예를 들어 등록과 해제가 매우 자주 일어나는 장애물이 있는 경우라면, 이런 비싼 장애물 처리는 AI 가 실제로 영향받은 메시 데이터에 있을 때만 해 주면 될 것입니다. 추가적으로 폴리곤이 분할되고 나면, 해당 지오메트리를 통하는 길찾기에 관련된 추가 비용은 발생하지 않습니다. 즉 장애물 등록에 있어 계산 비용은 딱 한 번만 일어난다는 뜻입니다. 또한 메시의 부모 구조가 바뀌고 있지는 않으므로, 장애물이 제거됐을 때 원래 상태로 돌리는 비용은 거의 공짜입니다.구현 세부사항
패쓰 장애물의 매우 간단한 예제로 NavMeshObstacle 를 봅시다. 여기서 보면 장애물을 켜고/끄는 키즈멧 로직을 몇 가지 볼 수 있습니다만, 재밌는 부분은 여깁니다:
cpptext { /** * 이 함수는 out_polyshape 를 이 오브젝트의 볼록 경계 모양을 나타내는 * 버텍스 리스트로 채웁니다. * @param out_PolyShape - 이 장애물의 경계 폴리모양에 대한 버텍스 버퍼를 담는 출력 배열입니다. * @return 참이면 이 오브젝트는 바로 장애물 역할을 합니다. (거짓이면 이 장애물은 메시에 영향을 끼치지 않습니다.) */ virtual UBOOL GetBoundingShape(TArray<FVector>& out_PolyShape); virtual UBOOL PreserveInternalPolys() { return TRUE; } }

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


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

Interface_NavMeshPathObject 와 함께 사용
흔히 AddObstacleEdge() 를 덮어쓰게 되는 경우는, Path 오브젝트 에 링크되는 에지를 추가할 때입니다. 예를 들면 Interface_NavMeshPathObstacle 와 Interface_NavMeshPathObject 둘 다 구현하는 장애물을 만들 수 있습니다. 이게 왜 유용한지를 그려보려면, 레벨이 시작된 이후 얼마 있다가 바닥에 뿌려진 기름 웅덩이를 생각해 보면 됩니다. 기름 웅덩이에 대해 필요한 작업은:
- AI 가 웅덩이를 돌아가도록 하기 위해, 웅덩이 모양 주위로 스태틱 내비게이션 메시를 분할시켜야 합니다.
- AI 가 웅덩이를 피할 수 있다면 그러도록 하기 위해, 웅덩이에 들어갈 때의 비용을 추가시키는 방법을 강구해야 합니다.
- 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);
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; } }