본문 바로가기
유니티

unity navmesh random위치 구하기(sampleposition)

by 유니티세상 2025. 8. 26.
반응형

랜덤 점 → 경로 가능 확인 → 이동 → 도착하면 다음 점

 

 

   void Start()
   {
       SetDestination(); // 시작하자마자 첫 목적지 지정
   }

   void Update()
   {

       if (agent.isOnNavMesh)
       {
           // 1) 목적지가 없거나 경로가 무효라면 목적지
           if (!isHasTarget || agent.pathStatus != NavMeshPathStatus.PathComplete)
           {
               SetDestination();
           }
           else
           {
               float threshold = Mathf.Max(distance, agent.stoppingDistance);//감속을 고려해서 
               if (!agent.pathPending && agent.remainingDistance <= threshold)
               {
                   SetDestination();
               }
           }
       }

   }


   private void SetDestination()
   {
       Vector3 randomPoint;

       for (int i = 0; i < tryGetPoint; i++)
       {
           if (TryGetRandomPoint(transform.position, range, out randomPoint))
           {
               var path = new NavMeshPath(); // 이동 가능한지 확인하기 위해 경로 객체를 만든다.
               if (agent.CalculatePath(randomPoint, path) && path.status == NavMeshPathStatus.PathComplete) //실제로 도달 가능한 목적지인지 체크
               {
                   targetPosition = randomPoint; // 목표 위치 저장
                   isHasTarget = true;  // 목표 있음
                   agent.SetDestination(randomPoint);// 실제 이동 시작

                   Debug.DrawRay(randomPoint, Vector3.up, Color.blue); // 목표점 1회 시각화

               }
           }
           else
           {
               isHasTarget = false;
               // 못 찾았으면 다음 프레임에 다시 시도할 수 있도록 플래그 해제
           }
       }
   }


   private bool TryGetRandomPoint(Vector3 center, float range, out Vector3 result)
   {
       for (int i = 0; i < tryGetPoint; i++)
       {
           Vector2 circle = Random.insideUnitCircle * range;               // XZ 평면에서 원형 랜덤, 수평(XZ) 원판에서 임의 좌표뽑기

           Vector3 randomPoint = new Vector3(center.x + circle.x, center.y + 1f, center.z + circle.y);

           if (NavMesh.SamplePosition(randomPoint, out NavMeshHit hit, sampleMaxDistance, NavMesh.AllAreas))//반경 내에서 가장 가까운 NavMesh 점
           {
               result = hit.position;    // NavMesh 위 정확 좌표
               return true;   // 성공
           }
       }
       result = Vector3.zero;  // 실패 시 기본값
       return false;
   }

 

 

TryGetRandomPoint가 하는 일(XZ 평면)

  • 큰 원: Random.insideUnitCircle * range로 뽑는 탐색 반경(range)
  • 파란 점들: 원 안에서 뽑은 임의 후보들
  • 주황 점(probe): 이번에 선택된 randomPoint (Y는 코드에서 +1f로 약간 올려 샘플)
    지형이 울퉁불퉁하거나 피벗 높이가 약간 어긋나 있으면, 기준점이 바닥보다 아래/안쪽에 들어가 sampleMaxDistance(예: 1m)로는 NavMesh를 못 잡을 수 있기 때문
  • 점선 원: 그 점을 중심으로 한 sampleMaxDistance (이 반경 안에서 NavMesh를 탐색)
  • 연한 사각 띠: “워커블 NavMesh 영역”을 개념적으로 표현(실제 엔진의 NavMesh는 더 복잡함)
  • 초록 삼각(hit): NavMesh.SamplePosition이 찾아 준 가장 가까운 NavMesh 점 = hit.position
  • 점선: probe에서 hit로 가장 가까운 위치로 스냅되는 느낌

코드 흐름

  1. insideUnitCircle*range로 probe(랜덤 샘플점)을 만든다
  2. NavMesh.SamplePosition(probe, out hit, sampleMaxDistance, mask)로
    주변(sampleMaxDistance) 안에서 NavMesh 위의 최근접 유효 좌표를 찾는다
  3. 성공하면 result = hit.position을 반환(true), 실패하면 다음 후보를 시도(최대 tryGetPoint번)

 

center = (10, 0, 20), range = 30, offset2D = (-7.3, 4.2)라면
→ probe = (10-7.3, 0+1, 20+4.2) = (2.7, 1, 24.2)

 

  public partial class NonOverlappingSpawner : MonoBehaviour
  {
      [Header("Overlap Settings")]
      public float checkRadius = 0.5f;                // 겹침 검사에 사용할 반지름
      public LayerMask obstacleLayer = ~0;            // 위치를 차지하는 레이어(캐릭터/장애물)

      // 트리거(Collider.isTrigger)도 포함해서 겹침을 검사할지 여부
      public bool includeTriggers = false;

      [Header("Search Area (검색 영역)")]
      public Vector3 areaCenter = Vector3.zero;       // 검색 영역의 중심
      public Vector3 areaSize = new Vector3(10, 0, 10);// 검색 영역의 크기 (가로 x 세로)

      public int maxAttempts = 30;                    // 최대 시도 횟수 (실패하면 기본 위치 사용)

      [Header("Ground Snap")]
      public bool alignToGround = true;               // 지면에 붙일지 여부
      public float groundRayHeight = 1f;              // 지면을 향해 쏠 Ray 높이

      [Tooltip("땅 레이어 마스크")]
      public LayerMask groundLayer = ~0;              // 지면으로 인식할 레이어

      [Header("Gizmos")]
      public bool showAreaGizmo = true;               // 검색 영역 Gizmo 표시 여부
      public bool showLastPick = true;                // 마지막 선택 위치 표시 여부

      // 내부 상태
      private readonly Collider[] _hits = new Collider[32]; // OverlapSphereNonAlloc 결과 저장 (GC 방지용)
      private Vector3 _lastPickedPos;                  // 마지막 선택된 위치
      private bool _hasPick;                           // 마지막 위치가 유효한지 여부
  }

  public partial class NonOverlappingSpawner : MonoBehaviour
  {
      /// <summary>
      /// 겹치지 않는 랜덤 위치 하나를 반환한다.
      /// </summary>
      public Vector3 TryPickPosition()
      {
          Vector3 position = default;

          var queryTriggerInteraction = includeTriggers ? QueryTriggerInteraction.Collide : QueryTriggerInteraction.Ignore;

          for (int i = 0; i < maxAttempts; i++)
          {
              // 1) 랜덤 후보 위치 뽑기
              Vector3 candidate = GetRandomPointInArea();

              // 2) 지면 맞추기 (옵션)
              if (alignToGround && !TryProjectToGround(ref candidate))
                  continue;

              // 3) 다른 오브젝트와 겹치지 않는지 검사
              if (!IsNotOverlap(candidate, checkRadius, obstacleLayer, queryTriggerInteraction))
                  continue;

              // 성공
              _hasPick = true;
              _lastPickedPos = candidate;
              position = candidate;

              Debug.Log("스폰 성공 → " + position);
              return position;
          }

          Debug.Log("스폰 실패 (기본 위치 반환)");
          return position;
      }

      /// 여러 개의 위치를 한 번에 뽑는다. 
      public int PickPositions(int count, List<Vector3> results, float minSeparation)
      {
          if (results == null)
          {
              results = new List<Vector3>();
          }

          int added = 0;
          var queryTriggerInteraction = includeTriggers ? QueryTriggerInteraction.Collide : QueryTriggerInteraction.Ignore;

          var picked = new List<Vector3>(count); // 임시 저장소

          for (int index = 0; index < count; index++)
          {
              bool ok = false;

              for (int attempt = 0; attempt < maxAttempts; attempt++)
              {
                  Vector3 candidate = GetRandomPointInArea();
                  if (alignToGround && !TryProjectToGround(ref candidate))
                      continue;

                  // 장애물과 겹치는지 확인
                  if (!IsNotOverlap(candidate, checkRadius, obstacleLayer, queryTriggerInteraction))
                      continue;

                  // 기존 위치들과 충분히 떨어져 있는지 확인
                  if (!IsFarEnough(candidate, picked, minSeparation))
                      continue;

                  picked.Add(candidate);
                  ok = true;

                  Debug.Log("스폰 성공 → " + candidate);
                  break;
              }

              if (!ok)
              {
                  Debug.Log("더 이상 위치를 찾을 수 없음");
                  break;
              }
          }

          results.AddRange(picked);
          added = picked.Count;

          if (added > 0)
          {
              _hasPick = true;
              _lastPickedPos = picked[picked.Count - 1];
          }

          return added;
      }
  }

  public partial class NonOverlappingSpawner : MonoBehaviour
  {
      // 검색 영역 안에서 랜덤 위치 뽑기
      private Vector3 GetRandomPointInArea()
      {
          Vector3 half = areaSize * 0.5f;
          Vector3 local = new Vector3(Random.Range(-half.x, half.x), 0f, Random.Range(-half.z, half.z));

          return transform.TransformPoint(areaCenter + local);
      }

      // 특정 위치가 겹치지 않는지 검사
      private bool IsNotOverlap(Vector3 position, float radius, LayerMask mask, QueryTriggerInteraction queryTriggerInteraction)
      {
          int count = Physics.OverlapSphereNonAlloc(position, radius, _hits, mask, queryTriggerInteraction);
          return count == 0;
      }

      // 기존 위치들과 최소 간격 이상 떨어졌는지 검사
      private bool IsFarEnough(Vector3 candidate, List<Vector3> picked, float minSep)
      {
          float minSqr = minSep * minSep;
          for (int i = 0; i < picked.Count; i++)
          {
              if ((candidate - picked[i]).sqrMagnitude < minSqr)
                  return false;
          }
          return true;
      }

      // 랜덤 좌표를 지면 위로 스냅
      private bool TryProjectToGround(ref Vector3 pos)
      {
          int mask = groundLayer.value != 0 ? groundLayer : ~0;
          var queryTriggerInteraction = includeTriggers ? QueryTriggerInteraction.Collide : QueryTriggerInteraction.Ignore;

          float castH = Mathf.Max(groundRayHeight, 50f);
          Vector3 origin = new Vector3(pos.x, pos.y + castH, pos.z);
          float maxDist = castH * 2f;

          // SphereCast로 지면 탐지
          float sphereR = Mathf.Max(0.05f, checkRadius * 0.5f);
          if (Physics.SphereCast(origin, sphereR, Vector3.down, out RaycastHit hitS, maxDist, mask, queryTriggerInteraction))
          {
              pos = hitS.point;
              return true;
          }

          // 일반 Raycast
          if (Physics.Raycast(origin, Vector3.down, out RaycastHit hit, maxDist, mask, queryTriggerInteraction))
          {
              pos = hit.point;
              return true;
          }

          // 혹시 지하에서 위로 맞는 경우
          if (Physics.Raycast(new Vector3(pos.x, pos.y - 0.1f, pos.z), Vector3.up, out RaycastHit upHit, maxDist, mask, queryTriggerInteraction))
          {
              pos = upHit.point;
              return true;
          }

          return false;
      }
  }

  public partial class NonOverlappingSpawner : MonoBehaviour
  {
      // private void OnDrawGizmosSelected()
      // {
      //     if (showAreaGizmo)
      //     {
      //         Gizmos.color = new Color(0f, 0.6f, 1f, 0.25f);
      //         var old = Gizmos.matrix;
      //         Gizmos.matrix = transform.localToWorldMatrix;
      //         Gizmos.DrawCube(areaCenter + Vector3.up * 0.01f, new Vector3(areaSize.x, 0.02f, areaSize.z));
      //         Gizmos.matrix = old;
      //     }
      //
      //     if (showLastPick && _hasPick)
      //     {
      //         Gizmos.color = Color.red;
      //         Gizmos.DrawWireSphere(_lastPickedPos, checkRadius);
      //     }
      // }
  }
반응형

'유니티' 카테고리의 다른 글

Unity 6 Awaitable, AsyncOperation  (0) 2025.09.15
Physics, SphereCastNonAlloc  (0) 2025.09.15
Random.insideUnitSphere  (0) 2025.08.26
NavMeshHit hit  (0) 2025.08.26
NavMesh.AllAreas/ NavMesh.GetAreaFromName  (0) 2025.08.26