소개

Godot 4는 NavigationServer2D/3D, NavigationRegion 노드, NavigationAgent 노드를 기반으로 하는 견고한 내비게이션 시스템을 갖추고 있습니다. Godot 3의 시스템에 비해 크게 개선되어 — 더 유연하고, 더 뛰어난 성능을 발휘하며, 동적 장애물이나 여러 종류의 에이전트 같은 복잡한 시나리오도 훨씬 쉽게 구성할 수 있습니다.

간단 개요: NavigationRegion은 에이전트가 걸어 다닐 수 있는 영역을 정의합니다. NavigationAgent는 개별 엔티티의 경로 탐색을 처리합니다. NavigationServer는 배경에서 모든 것을 관리합니다.

핵심 개념

  • NavigationRegion2D / NavigationRegion3D — NavigationPolygon(2D) 또는 NavigationMesh(3D)를 사용해 걸어 다닐 수 있는 영역을 정의합니다. 여러 개의 리전을 가질 수 있으며, 서버가 이를 자동으로 병합합니다.
  • NavigationAgent2D / NavigationAgent3D — CharacterBody에 붙여 경로 탐색을 처리합니다. 다음 경로 위치를 계산하고, 회피를 처리하며, 내비게이션이 완료되면 시그널을 발생시킵니다.
  • NavigationServer2D / NavigationServer3D — 모든 내비게이션 데이터를 관리하는 싱글턴입니다. 직접 다룰 일은 드물지만, 맵 업데이트, 경로 쿼리, 리전 연결을 처리합니다.

2D 내비게이션 설정

1단계: NavigationRegion2D 추가

  1. 씬에 NavigationRegion2D 노드를 추가합니다.
  2. 인스펙터에서 새 NavigationPolygon 리소스를 생성합니다.
  3. 2D 에디터에서 걸어 다닐 수 있는 폴리곤을 그립니다. 이 폴리곤은 에이전트가 걸어 다닐 수 있는 영역을 정의합니다.

TileMap 통합: NavigationPolygon은 TileMap 데이터에서 베이크할 수 있습니다. 타일에 TileSet에서 정의된 내비게이션 폴리곤이 있다면, NavigationRegion2D를 추가하고 베이크하기만 하면 됩니다 — 타일 레이아웃으로부터 걸어 다닐 수 있는 영역을 자동으로 생성해 줍니다.

NavigationAgent2D 추가하기

CharacterBody2D의 자식으로 NavigationAgent2D를 추가합니다. 다음은 완전한 이동 스크립트입니다:

extends CharacterBody2D

@export var speed: float = 200.0
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D

func _ready() -> void:
    # Wait for the navigation map to be ready
    await get_tree().physics_frame

    nav_agent.path_desired_distance = 4.0
    nav_agent.target_desired_distance = 4.0

func set_target(target_pos: Vector2) -> void:
    nav_agent.target_position = target_pos

func _physics_process(delta: float) -> void:
    if nav_agent.is_navigation_finished():
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    velocity = direction * speed
    move_and_slide()

중요: NavigationServer는 동기화를 위해 한 번의 물리 프레임이 필요합니다. _ready()에서 첫 번째 타겟 위치를 설정하기 전에 항상 await get_tree().physics_frame을 실행하세요. 그렇지 않으면 에이전트가 유효한 경로를 찾지 못할 수 있습니다.

주요 NavigationAgent2D 속성

  • target_position — 에이전트가 향할 목적지
  • path_desired_distance — 다음 경로 지점으로 넘어가기 위해 에이전트가 각 경로 지점에 얼마나 가까워야 하는지
  • target_desired_distance — 내비게이션이 완료된 것으로 간주하기 위해 에이전트가 타겟에 얼마나 가까워야 하는지
  • max_speed — 회피 계산에 사용됩니다(이동 코드를 제한하지는 않습니다)

3D 내비게이션

3D 내비게이션 시스템은 2D와 동일하게 작동하며, 노드 타입만 다릅니다:

  • NavigationRegion3D + NavigationMesh
  • NavigationAgent3D
  • NavigationServer3D
extends CharacterBody3D

@export var speed: float = 5.0
@onready var nav_agent: NavigationAgent3D = $NavigationAgent3D

func _ready() -> void:
    await get_tree().physics_frame
    nav_agent.path_desired_distance = 0.5
    nav_agent.target_desired_distance = 0.5

func set_target(target_pos: Vector3) -> void:
    nav_agent.target_position = target_pos

func _physics_process(delta: float) -> void:
    if nav_agent.is_navigation_finished():
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    velocity = direction * speed
    velocity.y -= 9.8 * delta  # Apply gravity
    move_and_slide()

내비게이션 메시 베이킹

3D에서는 보통 손으로 그리는 대신 레벨 지오메트리로부터 내비게이션 메시를 베이크합니다.

베이킹 설정

NavigationMesh 리소스의 주요 속성:

  • Agent Radius — 에이전트가 벽에서 얼마나 떨어져 있는지. 값이 클수록 더 보수적인 경로가 생성됩니다.
  • Agent Height — 걸어 다닐 수 있는 영역의 최소 천장 높이.
  • Agent Max Climb — 에이전트가 걸어 올라갈 수 있는 최대 단차 높이(예: 계단).
  • Agent Max Slope — 걸어 다닐 수 있는 최대 경사 각도(도 단위).
# Bake navigation mesh at runtime
var nav_region: NavigationRegion3D = $NavigationRegion3D
nav_region.bake_navigation_mesh()

# Wait for baking to complete
await nav_region.bake_finished
print("Navigation mesh baked!")

내비게이션 레이어

내비게이션 레이어를 사용하면 서로 다른 종류의 에이전트를 위해 걸어 다닐 수 있는 영역을 분리할 수 있습니다. 예를 들어, 지상 유닛과 비행 유닛은 서로 다른 내비게이션 메시를 가질 수 있습니다.

# Set navigation layers on the agent
nav_agent.set_navigation_layer_value(1, true)   # Ground layer
nav_agent.set_navigation_layer_value(2, false)  # Not flying layer

# Set navigation layers on the region
nav_region.set_navigation_layer_value(1, true)   # This region is for ground
nav_region.set_navigation_layer_value(2, false)  # Not for flying

일반적인 레이어 구성:

  • Layer 1 — 지상 유닛(병사, 차량)
  • Layer 2 — 비행 유닛(드론, 새)
  • Layer 3 — 대형 유닛(넓은 통로만)

회피(Avoidance)

NavigationAgent에는 에이전트끼리 겹치는 것을 방지하기 위한 로컬 회피 기능이 내장되어 있습니다. 회피가 활성화되면 에이전트는 다른 에이전트를 피하는 안전한 속도를 계산합니다.

extends CharacterBody2D

@export var speed: float = 200.0
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D

func _ready() -> void:
    await get_tree().physics_frame
    nav_agent.avoidance_enabled = true
    nav_agent.radius = 20.0
    nav_agent.max_speed = speed
    nav_agent.velocity_computed.connect(_on_velocity_computed)

func set_target(target_pos: Vector2) -> void:
    nav_agent.target_position = target_pos

func _physics_process(delta: float) -> void:
    if nav_agent.is_navigation_finished():
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    # Set the desired velocity — the agent will compute a safe one
    nav_agent.velocity = direction * speed

func _on_velocity_computed(safe_velocity: Vector2) -> void:
    velocity = safe_velocity
    move_and_slide()

작동 방식: 속도를 직접 사용하는 대신, nav_agent.velocity에 원하는 속도를 설정합니다. 그러면 에이전트가 velocity_computed 시그널을 통해 근처의 에이전트를 피하는 safe_velocity를 계산합니다. 콜백에서 이 안전한 속도를 적용하면 됩니다.

동적 내비게이션

문, 파괴 가능한 벽, 변화하는 지형에 대응하기 위해 런타임에 내비게이션 메시를 수정할 수 있습니다:

# Enable/disable a navigation region (e.g., open/close a door)
var door_region: NavigationRegion2D = $DoorNavigationRegion
door_region.enabled = false  # Block this path
door_region.enabled = true   # Open this path

# Add a navigation obstacle (blocks agent paths)
var obstacle := NavigationObstacle2D.new()
obstacle.radius = 30.0
add_child(obstacle)

# Re-bake after level changes (3D)
nav_region.bake_navigation_mesh()
await nav_region.bake_finished

성능 참고: 런타임에 3D 내비게이션 메시를 베이크하는 것은 비용이 큽니다. 3D의 동적 장애물에는 재베이크 대신 NavigationObstacle3D를 사용하는 것이 좋습니다. 2D에서는 NavigationRegion2D.enabled를 저렴하게 전환할 수 있습니다.

자주 쓰는 패턴

플레이어를 추격하는 적 AI

extends CharacterBody2D

@export var speed: float = 150.0
@export var chase_range: float = 300.0
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
var player: Node2D

func _ready() -> void:
    await get_tree().physics_frame
    player = get_tree().get_first_node_in_group("player")

func _physics_process(delta: float) -> void:
    if not player:
        return

    var distance := global_position.distance_to(player.global_position)
    if distance > chase_range:
        return  # Too far, don't chase

    # Update target every frame (or throttle to every N frames)
    nav_agent.target_position = player.global_position

    if nav_agent.is_navigation_finished():
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    velocity = direction * speed
    move_and_slide()

NPC 순찰 경로

extends CharacterBody2D

@export var speed: float = 100.0
@export var patrol_points: Array[Vector2] = []
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D
var current_patrol_index: int = 0

func _ready() -> void:
    await get_tree().physics_frame
    if patrol_points.size() > 0:
        nav_agent.target_position = patrol_points[0]

func _physics_process(delta: float) -> void:
    if patrol_points.size() == 0:
        return

    if nav_agent.is_navigation_finished():
        # Move to next patrol point
        current_patrol_index = (current_patrol_index + 1) % patrol_points.size()
        nav_agent.target_position = patrol_points[current_patrol_index]
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    velocity = direction * speed
    move_and_slide()

클릭 이동(탑다운)

extends CharacterBody2D

@export var speed: float = 200.0
@onready var nav_agent: NavigationAgent2D = $NavigationAgent2D

func _ready() -> void:
    await get_tree().physics_frame

func _unhandled_input(event: InputEvent) -> void:
    if event is InputEventMouseButton and event.pressed:
        nav_agent.target_position = get_global_mouse_position()

func _physics_process(delta: float) -> void:
    if nav_agent.is_navigation_finished():
        return

    var next_pos := nav_agent.get_next_path_position()
    var direction := global_position.direction_to(next_pos)
    velocity = direction * speed
    move_and_slide()

Godot 3에서 Godot 4로의 변경 사항

Godot 3 Godot 4
Navigation2D 노드 제거됨. NavigationRegion2D + NavigationAgent2D를 사용
Navigation.get_simple_path() NavigationServer2D.map_get_path()
수동 경로 추적 NavigationAgent.get_next_path_position()이 처리
내장 회피 기능 없음 NavigationAgent.avoidance_enabled
내비게이션 레이어 없음 맵당 32개의 내비게이션 레이어
장애물 없음 NavigationObstacle2D/3D