
Unity에서 AI를 쓴다면 꼭! 써야할 기능인 NavMesh입니다.
이걸 잘 몰라서 여러번 헤맸는데... 정리겸 개념을 알려드리기 위해 글을 씁니다.
Unity에서는 별도의 AI 시스템이 존재합니다. 이것으로 간편하게 AI몹들이 움직이게 할 수 있습니다. 장애물을 알아서 피하게 할 수 있고, 속도나 회전을 모두 설정할 수 있습니다.
Package Manager에서 Unity Registry -> AI Navigation을 설치합니다.
자식이 아닌 최상위 부모. 즉 지형지물을 담은 Map에서 NavMeshSurface 컴포넌트를 추가합니다. 그리고 Bake 해주세요. 그러면 Nav Mesh Data에 데이터가 생길겁니다.
Bake를 하고 나면 이러한 파란색이 영역이 뜹니다. 뜨지 않을 경우 오른쪽 상단 gizmo를 클릭해주세요.
움직일 AI 캐릭터에 NavMesh Agent 컴포넌트를 추가합니다.
각 Agent Type마다 AI 행동반경을 조정할 수 있는데요. 에디터 상단바에서 Window->AI->Navigation을 들어가줍니다.
여러가지가 뜨겠죠? 이것들을 설명해봅시다.
- Name (이름)
Agent 타입의 이름으로 여러 타입의 AI가 있을 때 구분용입니다. 코드에서 특정 Agent 타입을 지정할 때 사용됩니다.
- Radius (반지름)
AI 캐릭터의 두께로 측면 충돌 범위를 지정합니다. 좁은 통로를 지날 수 있는지 결정합니다. 작을수록 좁은 곳을 통과 가능하지만, 벽에 파묻힐 위험이 있습니다. 클수록 안전하지만 좁은 곳을 통과하는 것이 불가능합니다.
- Height (높이)
AI 캐릭터의 키로 수직 충돌 범위를 지정합니다. 낮은 천장 아래 지날 수 있는지 결정합니다. 작을수록 낮은 곳을 통과 가능하고, 클수록 높은 장애물을 감지합니다.
- Step Height (계단 높이)
한 번에 올라갈 수 있는 계단/단차 높이입니다. 이 값 이하의 단차는 부드럽게 올라가고 이 값을 초과하면 장애물로 인식합니다.
- Max Slope (최대 경사)
올라갈 수 있는 최대 경사각 (도 단위)입니다. 이 각도 이하의 경사는 걸어 올라가고 이 각도를 초과하면 벽으로 인식합니다.
- Drop Height (낙하 높이)
떨어져도 괜찮은 최대 높이입니다. 이 높이 이하면 뛰어내려서 이동하고 이 높이를 초과하면 다른 경로를 찾습니다.
- Jump Distance (점프 거리)
수평으로 뛸 수 있는 최대 거리입니다. 작은 틈을 뛰어넘을 수 있는 거리를 알려줍니다. 0이면 점프가 불가능합니다.재시도Claude는 실수를 할 수 있습니다. 응답을 반드시 다시 확인해 주세요.
Map의 컴포넌트인 NavMeshSurface 설명입니다.
- Agent Type (에이전트 타입)
어떤 크기/특성의 AI가 사용할 NavMesh인지 지정합니다. Navigation → Agents에서 정의한 타입들 중 선택하며, 서로 다른 크기의 AI가 다른 경로를 가질 수 있게 합니다.
- Default Area (기본 영역)
NavMesh의 기본 영역 타입을 설정합니다. AI가 이동할 때의 비용을 결정하며, 여러 영역이 있을 때 AI는 비용이 낮은 곳을 선호합니다. Walkable, Not Walkable, Jump 등이 있습니다.
- Generate Links (링크 생성)
떨어진 NavMesh 영역들을 자동으로 연결할지 결정합니다. 체크하면 작은 틈이나 낙하지점을 자동으로 점프 연결하고, 체크 해제하면 수동으로 NavMesh Link를 추가해야 합니다.
- Use Geometry (지오메트리 사용)
NavMesh 생성 시 어떤 지오메트리를 사용할지 선택합니다. Render Meshes는 보이는 모든 지형/건물을 포함하고, Physics Colliders는 Collider가 있는 오브젝트들만 사용합니다.
- Collect Objects (오브젝트 수집)
NavMesh 생성 시 어떤 범위의 오브젝트를 포함할지 설정합니다. All Game Objects는 Scene의 모든 오브젝트를 검사하고, Children은 현재 오브젝트의 자식들만, Volume은 특정 범위 안의 오브젝트만 검사합니다.
- Include Layers (포함 레이어)
어떤 레이어의 오브젝트들을 NavMesh에 포함할지 레이어 마스크로 선택합니다. Default, Ground는 지형만 포함하고, Everything은 모든 레이어를 포함합니다.
- NavMesh Data (NavMesh 데이터)
생성된 NavMesh 정보가 저장되는 곳입니다. 여러 Scene에서 같은 NavMesh를 공유하거나, NavMesh 데이터를 에셋으로 저장할 때 사용합니다.
AI캐릭터에 있을 NavMesh Agent 설명입니다.
- Agent Type (에이전트 타입)
사용할 Agent 타입을 지정합니다. Navigation → Agents에서 정의한 타입 중 선택하며, 각 타입은 서로 다른 이동 특성(크기, 속도, 점프능력 등)을 가집니다.
- Base Offset (기본 오프셋)
Agent의 기준점에서 실제 바닥까지의 거리를 설정합니다. 캐릭터가 바닥에 정확히 서있도록 조정하는 값으로, 양수는 위로, 음수는 아래로 이동시킵니다.
- Speed (속도)
Agent가 이동할 때의 최대 속도입니다. 목적지까지 이동하는 기본 속력을 결정하며, 코드에서 동적으로 변경 가능합니다.
- Angular Speed (회전 속도)
Agent가 방향을 바꿀 때의 회전 속도 (도/초)입니다. 값이 클수록 빠르게 회전하고, 값이 작으면 천천히 부드럽게 회전합니다.
- Acceleration (가속도)
Agent가 속도를 변경할 때의 가속도입니다. 정지 상태에서 최대 속도까지 도달하는 시간과 급정거 시 멈추는 시간을 결정합니다.
- Stopping Distance (정지 거리)
목적지에 도달했다고 판단하는 거리입니다. 이 거리 내에 들어오면 Agent가 목적지에 도착한 것으로 간주하고 이동을 멈춥니다.
- Auto Braking (자동 제동)
목적지 근처에서 자동으로 감속할지 결정합니다. 체크하면 목적지 근처에서 부드럽게 멈추고, 체크 해제하면 일정 속도로 이동합니다.
- Radius (반지름)
Agent의 충돌 반지름을 설정합니다. 다른 Agent나 장애물과의 거리를 유지하며, 값이 클수록 다른 객체들과 멀리 떨어져서 이동합니다.
- Height (높이)
Agent의 충돌 높이를 설정합니다. 낮은 천장이나 장애물 아래를 지날 수 있는지 판단하는 기준이 됩니다.
- Quality (품질)
경로 계산의 정밀도를 설정합니다. High Quality는 정확하지만 무겁고, Low Quality는 빠르지만 부정확할 수 있습니다.
- Priority (우선순위)
다른 Agent들과 충돌 회피 시의 우선순위입니다. 낮은 값일수록 높은 우선순위를 가지며, 다른 Agent들이 이 Agent를 피해서 이동합니다.
- Obstacle Avoidance (장애물 회피)
동적 장애물 회피 기능을 사용할지 결정합니다. 체크하면 움직이는 장애물이나 다른 Agent들을 실시간으로 피해서 이동합니다.
- Auto Traverse Off Mesh Link (자동 오프메시 링크 횡단)
NavMesh 간의 연결고리를 자동으로 건드릴지 결정합니다. 체크하면 점프나 낙하 지점을 자동으로 통과하고, 체크 해제하면 Off Mesh Link에 도달했을 때 수동으로 처리해야 합니다.
- Auto Repath (자동 경로 재계산)
경로가 무효화되었을 때 자동으로 새 경로를 계산할지 결정합니다. 체크하면 장애물이 생기거나 목적지가 변경될 때 자동으로 경로를 다시 찾고, 체크 해제하면 수동으로 경로를 재계산해야 합니다.
- Area Mask (영역 마스크)
Agent가 이동 가능한 NavMesh 영역을 제한합니다. 선택된 영역에만 경로를 생성하며, 특정 영역을 금지구역으로 설정하거나 특별한 경로만 사용하도록 제한할 수 있습니다.
// 목표 지점으로 이동
NavMeshAgent agent = GetComponent<NavMeshAgent>();
agent.SetDestination(targetPosition);
// 이동 중인지 확인
if (agent.pathPending || agent.remainingDistance > 0.1f)
{
// 아직 이동 중
}
// 목표 지점에 도달했는지 확인
if (!agent.pathPending && agent.remainingDistance < 0.1f)
{
// 목표 지점 도달
}
// 이동 중지
agent.Stop();
agent.isStopped = true;
// 이동 재개
agent.isStopped = false;
agent.Resume();
// 이동 속도 설정
agent.speed = 5f;
// 회전 속도 설정
agent.angularSpeed = 120f;
// 가속도 설정
agent.acceleration = 8f;
// 정지 거리 설정
agent.stoppingDistance = 2f;
// 자동 회전 끄기/켜기
agent.updateRotation = false;
agent.updateRotation = true;
// 경로 계산
NavMeshPath path = new NavMeshPath();
agent.CalculatePath(targetPosition, path);
// 경로 상태 확인
if (path.status == NavMeshPathStatus.PathComplete)
{
// 경로 완성됨
}
// 경로 설정
agent.SetPath(path);
// 현재 경로 정보
Vector3[] pathCorners = agent.path.corners;
float pathLength = agent.path.GetCornersNonAlloc(pathCorners);
// 가장 가까운 NavMesh 포인트 찾기
NavMeshHit hit;
if (NavMesh.SamplePosition(transform.position, out hit, 10f, NavMesh.AllAreas))
{
Vector3 closestPoint = hit.position;
}
// 특정 영역에서 랜덤 포인트 찾기
Vector3 randomPoint = Random.insideUnitSphere * 10f;
randomPoint += transform.position;
if (NavMesh.SamplePosition(randomPoint, out hit, 10f, NavMesh.AllAreas))
{
Vector3 validRandomPoint = hit.position;
}
// 두 점 사이의 경로 계산
NavMeshPath path = new NavMeshPath();
bool pathFound = NavMesh.CalculatePath(startPos, endPos, NavMesh.AllAreas, path);
if (pathFound && path.status == NavMeshPathStatus.PathComplete)
{
// 유효한 경로 존재
Vector3[] corners = path.corners;
}
// NavMesh 레이캐스트 (장애물 확인)
NavMeshHit hit;
bool blocked = NavMesh.Raycast(transform.position, targetPosition, out hit, NavMesh.AllAreas);
if (blocked)
{
// 장애물 있음
Vector3 hitPoint = hit.position;
}
// 특정 영역만 사용
int walkableArea = 1 << NavMesh.GetAreaFromName("Walkable");
int jumpArea = 1 << NavMesh.GetAreaFromName("Jump");
int combinedAreas = walkableArea | jumpArea;
agent.areaMask = combinedAreas;
// 모든 영역 사용
agent.areaMask = NavMesh.AllAreas;
// 영역별 이동 비용 설정
NavMeshAgent agent = GetComponent<NavMeshAgent>();
agent.SetAreaCost(NavMesh.GetAreaFromName("Water"), 3f);
agent.SetAreaCost(NavMesh.GetAreaFromName("Mud"), 2f);
// Off-Mesh Link 사용 중인지 확인
if (agent.isOnOffMeshLink)
{
// 수동으로 Off-Mesh Link 완료
agent.CompleteOffMeshLink();
}
// Off-Mesh Link 자동 처리 끄기
agent.autoTraverseOffMeshLink = false;
// 장애물 회피 설정
agent.obstacleAvoidanceType = ObstacleAvoidanceType.HighQualityObstacleAvoidance;
agent.avoidancePriority = 50; // 0(높음) ~ 99(낮음)
agent.radius = 0.5f;
agent.height = 2f;
// NavMesh 베이킹 (런타임)
NavMeshBuilder.BuildNavMesh();
// NavMesh 데이터 업데이트
NavMeshHit hit;
if (NavMesh.SamplePosition(transform.position, out hit, 1f, NavMesh.AllAreas))
{
// 새로운 NavMesh 데이터 사용
}
// 현재 상태 확인
bool isMoving = agent.velocity.magnitude > 0.1f;
bool hasPath = agent.hasPath;
bool isOnNavMesh = agent.isOnNavMesh;
bool pathPending = agent.pathPending;
// 거리 정보
float remainingDistance = agent.remainingDistance;
float stoppingDistance = agent.stoppingDistance;
// 현재 위치 정보
Vector3 currentPosition = agent.transform.position;
Vector3 destination = agent.destination;
Vector3 nextPosition = agent.nextPosition;
Vector3 velocity = agent.velocity;
public class EnemyAI : MonoBehaviour
{
private NavMeshAgent agent;
private Transform target;
void Start()
{
agent = GetComponent<NavMeshAgent>();
target = GameObject.FindGameObjectWithTag("Player").transform;
}
void Update()
{
if (target != null)
{
agent.SetDestination(target.position);
}
}
}
public class PatrolAI : MonoBehaviour
{
private NavMeshAgent agent;
public Transform[] waypoints;
private int currentWaypoint = 0;
void Start()
{
agent = GetComponent<NavMeshAgent>();
if (waypoints.Length > 0)
{
agent.SetDestination(waypoints[0].position);
}
}
void Update()
{
if (!agent.pathPending && agent.remainingDistance < 0.5f)
{
currentWaypoint = (currentWaypoint + 1) % waypoints.Length;
agent.SetDestination(waypoints[currentWaypoint].position);
}
}
}
public class RandomMovement : MonoBehaviour
{
private NavMeshAgent agent;
public float wanderRadius = 10f;
void Start()
{
agent = GetComponent<NavMeshAgent>();
SetRandomDestination();
}
void Update()
{
if (!agent.pathPending && agent.remainingDistance < 0.5f)
{
SetRandomDestination();
}
}
void SetRandomDestination()
{
Vector3 randomDirection = Random.insideUnitSphere * wanderRadius;
randomDirection += transform.position;
NavMeshHit hit;
if (NavMesh.SamplePosition(randomDirection, out hit, wanderRadius, NavMesh.AllAreas))
{
agent.SetDestination(hit.position);
}
}
}