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

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로 가장 가까운 위치로 스냅되는 느낌
코드 흐름
- insideUnitCircle*range로 probe(랜덤 샘플점)을 만든다
- NavMesh.SamplePosition(probe, out hit, sampleMaxDistance, mask)로
주변(sampleMaxDistance) 안에서 NavMesh 위의 최근접 유효 좌표를 찾는다 - 성공하면 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 |