Unity의 NavMesh(Navigation Mesh) 시스템은 AI 캐릭터들이 지형을 자동으로 탐색하고 장애물을 피해 목적지까지 이동할 수 있게 해주는 강력한 도구입니다. 이번 글에서는 NavMesh 시스템의 기본 설정부터 실제 AI 구현까지 단계별로 알아보겠습니다.
NavMesh는 Navigation Mesh의 줄임말로, AI 에이전트가 이동할 수 있는 영역을 미리 계산해둔 메쉬입니다. 이 시스템을 통해 AI는 복잡한 지형에서도 최적의 경로를 찾아 자동으로 이동할 수 있습니다.
먼저 Unity에서 AI Navigation 패키지를 설치해야 합니다.
Package Manager → Unity Registry → "nav" 검색 → AI Navigation → Install
NavMesh 시스템의 첫 번째 단계는 AI가 걸을 수 있는 표면을 정의하는 것입니다.
// 기본 지면 오브젝트(Plane) 선택
// Add Component → NavMesh Surface
베이킹 후 파란색 영역이 표시되지 않으면:
기본적으로는 Plane만 걸을 수 있는 영역으로 설정됩니다. 다른 오브젝트들도 포함시키려면:
Window → AI → Navigation에서 에이전트 설정을 조정할 수 있습니다.
Navigation Window 설정:
- Agent Radius: 0.25 (에이전트의 반지름)
- Agent Height: 0.7 (에이전트의 키)
- Max Slope: 35 (오를 수 있는 최대 경사도)
- Step Height: 0.1 (오를 수 있는 계단 높이)
// 권장 설정값
Agent Radius: 0.25f // 좁은 공간 통과 가능
Agent Height: 0.7f // 일반적인 캐릭터 크기
Max Slope: 35f // 적당한 경사면 이동
Step Height: 0.1f // 작은 턱 극복 가능
using UnityEngine;
using UnityEngine.AI;
public class SimpleAI : MonoBehaviour
{
[Header("Navigation")]
public NavMeshAgent agent;
public Transform target;
private void Start()
{
// NavMeshAgent 컴포넌트 자동 할당
if (agent == null)
agent = GetComponent<NavMeshAgent>();
}
private void Update()
{
// 타겟이 설정되어 있으면 해당 위치로 이동
if (target != null)
{
agent.SetDestination(target.position);
}
}
}
using UnityEngine;
using UnityEngine.AI;
public class AdvancedAI : MonoBehaviour
{
[Header("Navigation")]
public NavMeshAgent agent;
public Transform target;
[Header("AI Settings")]
public float detectionRange = 10f;
public float stopDistance = 2f;
private void Start()
{
agent = GetComponent<NavMeshAgent>();
// 에이전트 설정
agent.stoppingDistance = stopDistance;
}
private void Update()
{
if (target != null)
{
float distanceToTarget = Vector3.Distance(transform.position, target.position);
// 탐지 범위 내에 있을 때만 추적
if (distanceToTarget <= detectionRange)
{
agent.SetDestination(target.position);
}
else
{
// 범위를 벗어나면 정지
agent.ResetPath();
}
}
}
private void OnDrawGizmosSelected()
{
// 탐지 범위 시각화
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, detectionRange);
// 정지 거리 시각화
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, stopDistance);
}
}
플랫폼 간 점프나 낙하가 필요한 경우 Off Mesh Link를 사용합니다.
// Off Mesh Link 컴포넌트를 수동으로 추가
// Start Point와 End Point를 설정하여 정확한 점프 지점 지정
public class AIAnimationController : MonoBehaviour
{
private NavMeshAgent agent;
private Animator animator;
private void Start()
{
agent = GetComponent<NavMeshAgent>();
animator = GetComponent<Animator>();
}
private void Update()
{
// 이동 속도에 따른 애니메이션
float speed = agent.velocity.magnitude;
animator.SetFloat("Speed", speed);
// Off Mesh Link 사용 중인지 확인 (점프/낙하)
if (agent.isOnOffMeshLink)
{
animator.SetTrigger("Jump");
}
}
}
using System.Collections.Generic;
using UnityEngine;
public class AIManager : MonoBehaviour
{
[Header("AI Management")]
public List<GameObject> aiPrefabs;
public Transform[] spawnPoints;
public Transform commonTarget;
[Header("Spawn Settings")]
public int aiCount = 10;
private List<GameObject> spawnedAIs = new List<GameObject>();
private void Start()
{
SpawnMultipleAIs();
}
private void SpawnMultipleAIs()
{
for (int i = 0; i < aiCount; i++)
{
// 랜덤 스폰 지점 선택
Transform spawnPoint = spawnPoints[Random.Range(0, spawnPoints.Length)];
// 랜덤 AI 프리팹 선택
GameObject aiPrefab = aiPrefabs[Random.Range(0, aiPrefabs.Length)];
// AI 생성
GameObject newAI = Instantiate(aiPrefab, spawnPoint.position, spawnPoint.rotation);
// 타겟 설정
SimpleAI aiScript = newAI.GetComponent<SimpleAI>();
if (aiScript != null)
{
aiScript.target = commonTarget;
}
spawnedAIs.Add(newAI);
}
}
public void ChangeAllTargets(Transform newTarget)
{
foreach (GameObject ai in spawnedAIs)
{
SimpleAI aiScript = ai.GetComponent<SimpleAI>();
if (aiScript != null)
{
aiScript.target = newTarget;
}
}
}
}
public class OptimizedAI : MonoBehaviour
{
private NavMeshAgent agent;
private float updateInterval = 0.1f; // 0.1초마다 업데이트
private float lastUpdateTime;
private void Start()
{
agent = GetComponent<NavMeshAgent>();
// 성능 최적화 설정
agent.acceleration = 8f; // 가속도
agent.angularSpeed = 120f; // 회전 속도
agent.obstacleAvoidanceType = ObstacleAvoidanceType.LowQualityObstacleAvoidance;
}
private void Update()
{
// 일정 간격으로만 경로 업데이트
if (Time.time - lastUpdateTime >= updateInterval)
{
UpdateNavigation();
lastUpdateTime = Time.time;
}
}
private void UpdateNavigation()
{
// 실제 네비게이션 로직
if (target != null && agent.enabled)
{
agent.SetDestination(target.position);
}
}
}
public class AILODManager : MonoBehaviour
{
[Header("LOD Settings")]
public float highDetailDistance = 20f;
public float mediumDetailDistance = 50f;
private Transform player;
private NavMeshAgent agent;
private void Start()
{
player = FindObjectOfType<Player>().transform;
agent = GetComponent<NavMeshAgent>();
}
private void Update()
{
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
// 거리에 따른 업데이트 빈도 조절
if (distanceToPlayer <= highDetailDistance)
{
// 고품질: 매 프레임 업데이트
agent.enabled = true;
GetComponent<Animator>().enabled = true;
}
else if (distanceToPlayer <= mediumDetailDistance)
{
// 중품질: 낮은 빈도 업데이트
agent.enabled = true;
GetComponent<Animator>().enabled = false;
}
else
{
// 저품질: 네비게이션 비활성화
agent.enabled = false;
GetComponent<Animator>().enabled = false;
}
}
}
using System.Collections;
using UnityEngine;
public class PatrolAI : MonoBehaviour
{
[Header("Patrol Settings")]
public Transform[] patrolPoints;
public float waitTime = 2f;
public bool randomPatrol = false;
private NavMeshAgent agent;
private int currentPatrolIndex = 0;
private bool isWaiting = false;
private void Start()
{
agent = GetComponent<NavMeshAgent>();
if (patrolPoints.Length > 0)
{
agent.SetDestination(patrolPoints[0].position);
}
}
private void Update()
{
// 목적지에 도달했는지 확인
if (!agent.pathPending && agent.remainingDistance < 0.5f && !isWaiting)
{
StartCoroutine(WaitAndMoveToNext());
}
}
private IEnumerator WaitAndMoveToNext()
{
isWaiting = true;
yield return new WaitForSeconds(waitTime);
// 다음 순찰 지점 결정
if (randomPatrol)
{
currentPatrolIndex = Random.Range(0, patrolPoints.Length);
}
else
{
currentPatrolIndex = (currentPatrolIndex + 1) % patrolPoints.Length;
}
// 다음 목적지로 이동
agent.SetDestination(patrolPoints[currentPatrolIndex].position);
isWaiting = false;
}
}
using UnityEngine;
public enum AIState
{
Idle,
Patrol,
Chase,
Attack,
Return
}
public class StateMachineAI : MonoBehaviour
{
[Header("AI Settings")]
public Transform player;
public float detectionRange = 10f;
public float attackRange = 2f;
public float loseTargetRange = 15f;
[Header("Patrol")]
public Transform[] patrolPoints;
private NavMeshAgent agent;
private AIState currentState = AIState.Patrol;
private Vector3 initialPosition;
private int patrolIndex = 0;
private void Start()
{
agent = GetComponent<NavMeshAgent>();
initialPosition = transform.position;
if (patrolPoints.Length > 0)
{
agent.SetDestination(patrolPoints[0].position);
}
}
private void Update()
{
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
switch (currentState)
{
case AIState.Patrol:
HandlePatrolState(distanceToPlayer);
break;
case AIState.Chase:
HandleChaseState(distanceToPlayer);
break;
case AIState.Attack:
HandleAttackState(distanceToPlayer);
break;
case AIState.Return:
HandleReturnState();
break;
}
}
private void HandlePatrolState(float distanceToPlayer)
{
// 플레이어 탐지
if (distanceToPlayer <= detectionRange)
{
ChangeState(AIState.Chase);
return;
}
// 순찰 지점 도달 확인
if (!agent.pathPending && agent.remainingDistance < 0.5f)
{
patrolIndex = (patrolIndex + 1) % patrolPoints.Length;
agent.SetDestination(patrolPoints[patrolIndex].position);
}
}
private void HandleChaseState(float distanceToPlayer)
{
// 공격 범위 내 진입
if (distanceToPlayer <= attackRange)
{
ChangeState(AIState.Attack);
return;
}
// 플레이어를 놓침
if (distanceToPlayer > loseTargetRange)
{
ChangeState(AIState.Return);
return;
}
// 플레이어 추적
agent.SetDestination(player.position);
}
private void HandleAttackState(float distanceToPlayer)
{
// 공격 범위를 벗어남
if (distanceToPlayer > attackRange)
{
ChangeState(AIState.Chase);
return;
}
// 공격 로직 (애니메이션, 데미지 등)
// 여기서 실제 공격 구현
}
private void HandleReturnState()
{
// 초기 위치로 복귀
agent.SetDestination(initialPosition);
if (Vector3.Distance(transform.position, initialPosition) < 1f)
{
ChangeState(AIState.Patrol);
}
}
private void ChangeState(AIState newState)
{
currentState = newState;
Debug.Log($"AI State changed to: {newState}");
}
}
// 해결 방법:
// 1. 오브젝트가 Navigation Static으로 설정되어 있는지 확인
// 2. NavMesh Surface 컴포넌트가 올바르게 설정되어 있는지 확인
// 3. Bake 버튼을 다시 클릭
public class NavMeshDebugger : MonoBehaviour
{
private NavMeshAgent agent;
private void Start()
{
agent = GetComponent<NavMeshAgent>();
}
private void Update()
{
// 디버그 정보 출력
if (agent != null)
{
Debug.Log($"Agent enabled: {agent.enabled}");
Debug.Log($"Has path: {agent.hasPath}");
Debug.Log($"Path status: {agent.pathStatus}");
Debug.Log($"Remaining distance: {agent.remainingDistance}");
}
}
}
using UnityEngine;
public class NavMeshProfiler : MonoBehaviour
{
[Header("Performance Monitoring")]
public bool showDebugInfo = true;
private NavMeshAgent[] allAgents;
private void Start()
{
allAgents = FindObjectsOfType<NavMeshAgent>();
}
private void OnGUI()
{
if (showDebugInfo)
{
GUILayout.BeginArea(new Rect(10, 10, 300, 200));
GUILayout.Label($"Active NavMesh Agents: {allAgents.Length}");
int activeAgents = 0;
foreach (var agent in allAgents)
{
if (agent.enabled && agent.gameObject.activeInHierarchy)
activeAgents++;
}
GUILayout.Label($"Enabled Agents: {activeAgents}");
GUILayout.Label($"FPS: {1f / Time.unscaledDeltaTime:F1}");
GUILayout.EndArea();
}
}
}
Unity의 NavMesh 시스템은 복잡한 AI 내비게이션을 간단하게 구현할 수 있게 해주는 강력한 도구입니다. 기본적인 설정부터 고급 상태 머신까지, 이 가이드를 통해 여러분만의 지능적인 AI 시스템을 구축할 수 있을 것입니다.
핵심 포인트:
이제 여러분의 게임에 생동감 넘치는 AI 캐릭터들을 추가해보세요!