NavMesh로 적을 돌아다니게 하고, 시야 기반 추적(Chase)을 만들고,
벽과 경사면으로 맵을 꾸민 과정을 기록한다.
1. NavMesh — 적이 걸어다닐 수 있는 지도 만들기
NavMesh(내비메시)란?
"여기는 걸어다닐 수 있는 곳이야"라고 유니티에게 알려주는 지도 데이터다.
적은 이 영역 안에서 길을 찾아 이동한다.
NavMeshAgent란?
NavMesh 위에서 자동으로 길을 찾아 이동하는 컴포넌트. "목적지만 말하면 알아서 가주는 택시"와 같다.
플레이어의 CharacterController와 비교:
CharacterControllerNavMeshAgent
| 직접 방향과 속도를 매 프레임 지정 | 목적지만 한 번 알려주면 자동 이동 |
| 길찾기 없음 (벽이 있으면 막힘) | NavMesh 기반 자동 길찾기 |
| 플레이어처럼 직접 조작하는 캐릭터에 적합 | AI처럼 스스로 움직이는 캐릭터에 적합 |
설정 과정
- Ground 오브젝트에 NavMeshSurface 컴포넌트 추가
- Bake(굽기) 버튼 클릭 → Scene 뷰에서 파란색 영역이 표시되면 성공
- Enemy 오브젝트에 NavMeshAgent 컴포넌트 추가
NavMeshAgent의 주요 설정
- Speed (속도): 3.5 → 이동 빠르기
- Stopping Distance (정지 거리): 2 → 목적지에서 이 거리에서 멈춤
Bake(굽기) 전과 후의 모습


적(오리)이 이동할 수 있는 영역을 파란색으로 표시해준다. 파란색이 아닌 영역을 갈 수 없는 영역.
Stopping Distance 개념
Stopping Distance = 2:
적 ────────────── 🎯 목적지
↑ 여기서 멈춤 (2m 남았을 때)
Stopping Distance = 0:
적 ──────────────🎯 딱 붙어서 멈춤
적이 플레이어에게 딱 붙으면 부자연스러우니까 약간 거리를 두는 것.
2. Patrol(순찰) 구현 - 적이 맵을 돌아다니기
FSM 확장: Patrol 상태 추가
플레이어의 FSM(Idle ↔ Jump)에서 배운 패턴을 적에게도 적용했다.
Idle (가만히 서있기) → 2초 후 → Patrol (랜덤 위치로 이동) → 도착 → Idle → 반복
NavMeshAgent 핵심 코드 패턴
_agent.SetDestination(위치) // "여기로 가!" (택시에게 목적지 말하기)
_agent.ResetPath() // "멈춰!" (택시 하차)
_agent.remainingDistance // 목적지까지 남은 거리
_agent.stoppingDistance // 이 거리에서 멈춤
_agent.pathPending // 길 계산이 아직 진행 중이면 true
도착 판정 패턴
if (!_agent.pathPending && _agent.remainingDistance <= _agent.stoppingDistance)
{
// 도착!
}
왜 2개를 확인할까?
- pathPending 체크: 경로 계산 중에는 remainingDistance가 0이라서 오판할 수 있음
- remainingDistance 체크: 실제로 가까이 도착했는지 확인
랜덤 위치 만들기
// 1. 랜덤한 점을 뽑는다
Vector3 randomDirection = Random.insideUnitSphere * patrolRange;
// 2. NavMesh 위의 유효한 위치로 보정한다
NavMesh.SamplePosition(randomDirection, out hit, patrolRange, NavMesh.AllAreas);
랜덤으로 뽑은 점이 벽 안이나 맵 밖일 수 있으니, SamplePosition으로 가장 가까운 NavMesh 위의 점으로 보정한다.
3. Chase(추적) 구현 — 적이 플레이어를 쫓아오기
적이 플레이어를 쫓으려면 플레이어를 발견하는게 우선이다. 그래서 요구사항은 "적의 정면 방향으로 플레이어가 보일 경우, 플레이어 쪽으로 접근" 이 된다.
시야 판정의 2단계
적이 플레이어를 "본다"는 건 두 조건을 모두 만족해야 한다:
1단계: 가까운가? (거리 체크)
→ 감지 거리(detectRange) 안에 있어야 함
2단계: 정면인가? (각도 체크)
→ 시야각(fieldOfViewAngle) 안에 있어야 함
시야각 = 60도
╱‾‾‾‾‾‾‾‾‾‾╲
╱ 60° ↑ 60° ╲
╱ (정면) ╲
적 ──────────────────────
╲ 여기는 안 보임 ╱
╲ ╱
╲_____________╱
거리만 보면 뒤에서도 발견됨 (부자연스러움)
각도만 보면 100m 밖에서도 발견됨 (비현실적)
둘 다 확인해야 자연스럽다!
벡터와 각도
벡터(Vector): 방향을 가진 화살표
// 적 → 플레이어 방향 벡터
Vector3 directionToPlayer = (플레이어위치 - 적위치).normalized;
// .normalized: 방향만 남기고 길이를 1로 만든다
Vector3.Angle: 두 방향 사이의 각도
float angle = Vector3.Angle(transform.forward, directionToPlayer);
// 적의 정면(forward)과 플레이어 방향 사이의 각도
// 이 각도가 시야각보다 작으면 → "보인다!"
Patrol vs Chase: 목적지 갱신 차이
// Patrol: 고정된 목적지 → 초기화할 때 한 번만 설정
case State.Patrol:
_agent.SetDestination(랜덤위치); // 1번만
break;
// Chase: 움직이는 목적지 → 매 프레임 갱신
if (state == State.Chase)
_agent.SetDestination(플레이어위치); // 매 프레임
순찰은 목적지가 안 움직이니까 한 번만 설정.
추적은 플레이어가 계속 움직이니까 매 프레임 위치를 업데이트해야 한다.
Tag(태그)로 오브젝트 찾기
GameObject player = GameObject.FindWithTag("Player");
Unity에서 모든 오브젝트에 Tag(이름표)를 붙일 수 있다.
그리고 적이 플레이어를 추적하기 위해서는 Player 오브젝트의 Inspector 상단에서 Tag를 "Player"로 설정해야 적이 찾을 수 있다!
최종 FSM 흐름도
2초 대기 시야에서 벗어남
┌──────┐ ──────────→ ┌────────┐ ┌──────┐
│ Idle │ │ Patrol │ ─시야발견→ │Chase │
│(대기) │ ←────────── │(순찰) │ │(추적) │
└──┬───┘ 도착 └────────┘ └──┬───┘
│ │
│ 시야발견 │
▼ │
┌──────┐ ←──── 공격 범위 진입 ────────────────┘
│Attack│
│(공격) │ → 공격 끝 → Idle
└──────┘
4. 벽과 경사면 만들기 — 맵 꾸미기
오브젝트 만들기
- Hierarchy 우클릭 → 3D Object > Cube → 벽이나 경사면으로 변형
- Scale: 크기 조절 (X=가로, Y=높이, Z=깊이)
- Rotation: 회전 (경사면은 X축을 15도 정도 기울임)
아직은 직접 3D 모델링하는 방법은 모르기 때문에 기본 도형으로 주변 오브젝트를 만들어주었다.
복제와 정리
- Cmd(⌘) + D: 오브젝트 복제
- Create Empty: 빈 오브젝트를 만들어서 폴더처럼 그룹화(모두 root 경로에 있으면 찾기도 어렵고 Hierarchy가 지저분해짐)
Hierarchy:
▶ Obstacles ← 빈 오브젝트 (폴더 역할)
Wall
Wall (1)
Wall (2)
Slope
Static 체크 & NavMesh 다시 Bake
벽을 추가한 후 Static(정적) 체크하고 NavMesh를 다시 Bake하면,
적이 벽을 피해서 돌아다닌다.
그림자가 너무 진한 문제
벽의 한쪽 면이 검은색으로 보였다. 뭔가 너무 아마추어 같달까? 그래서 조명을 하나 더 추가하기로 했다.

확인해보니 기본적으로 Directional Light가 한쪽에서만 비추니까, 반대편은 빛이 없어서 검게 보였다.
하여, 보조 Directional Light를 추가했다.
- Hierarchy 우클릭 → Light → Directional Light
- Rotation을 기존 빛 반대 방향으로 설정
- Intensity(밝기)를 낮게 (0.3 정도)
- Shadow Type: No Shadows (보조 조명이니까 그림자 불필요)

검은 그림자가 밝아지니 훨씬 더 자연스러워진 모습!
다음에 계속..!
'Unity 궁둥이 쪼물쪼물' 카테고리의 다른 글
| 006. 총알에 오브젝트 풀링 적용하기 (0) | 2026.04.06 |
|---|---|
| 005. 발사체(Projectile) 구현 (0) | 2026.04.06 |
| 004. Enemy(적) 애니메이션 구현 (0) | 2026.04.06 |
| 002. CharacterController로 플레이어 움직임 구현 (0) | 2026.04.03 |
| 001. Unity 환경 설정 & New Input System (0) | 2026.04.02 |