본문 바로가기
Unity 궁둥이 쪼물쪼물

003. 적 AI 구현 & 맵 꾸미기

by daco2020 2026. 4. 6.

NavMesh로 적을 돌아다니게 하고, 시야 기반 추적(Chase)을 만들고,
벽과 경사면으로 맵을 꾸민 과정을 기록한다.

 

 

1. NavMesh — 적이 걸어다닐 수 있는 지도 만들기

NavMesh(내비메시)란?

"여기는 걸어다닐 수 있는 곳이야"라고 유니티에게 알려주는 지도 데이터다.
적은 이 영역 안에서 길을 찾아 이동한다.

NavMeshAgent란?

NavMesh 위에서 자동으로 길을 찾아 이동하는 컴포넌트. "목적지만 말하면 알아서 가주는 택시"와 같다.

플레이어의 CharacterController와 비교:

CharacterControllerNavMeshAgent

직접 방향과 속도를 매 프레임 지정 목적지만 한 번 알려주면 자동 이동
길찾기 없음 (벽이 있으면 막힘) NavMesh 기반 자동 길찾기
플레이어처럼 직접 조작하는 캐릭터에 적합 AI처럼 스스로 움직이는 캐릭터에 적합

설정 과정

  1. Ground 오브젝트에 NavMeshSurface 컴포넌트 추가
  2. Bake(굽기) 버튼 클릭 → Scene 뷰에서 파란색 영역이 표시되면 성공
  3. Enemy 오브젝트에 NavMeshAgent 컴포넌트 추가

NavMeshAgent의 주요 설정

  • Speed (속도): 3.5 → 이동 빠르기
  • Stopping Distance (정지 거리): 2 → 목적지에서 이 거리에서 멈춤

 

Bake(굽기) 전과 후의 모습

Bake 전 모습

 

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 (보조 조명이니까 그림자 불필요)

 

 

명도가 적절해졌다!

 

검은 그림자가 밝아지니 훨씬 더 자연스러워진 모습!

 

 

다음에 계속..!