【Godot】NavigationAgent2Dによるパスファインディング - 敵AIの追跡と巡回

作成: 2025-12-08最終更新: 2026-07-08

GodotのNavigationAgent2Dで、障害物を避けながら敵AIの追跡・巡回を実装する方法を解説。基本の処理サイクル、get_next_path_positionの意味、追跡と巡回の実装、RVOによるエージェント同士の回避、経路再計算のパフォーマンス最適化まで、コードと図で学びます。

敵がプレイヤーを障害物ごしに追いかけてくる。警備兵が決まったルートを黙々と巡回する——こうした「賢い移動」は、ナビゲーションメッシュ(AI用の地図) の上を歩く NavigationAgent2D が実現します。

前回の記事で「歩ける地図」をベイクしました。この記事は、 その地図の上を実際に移動させる 側の話です。目的地を指定するだけで、経路計算も障害物回避もエージェントが引き受けてくれるので、私たちは「どこへ向かうか」を決めるだけで済みます。追跡と巡回、2つの定番を作れるようにしましょう。

2Dマップで、エージェントが中継点をつないだ経路の点線に沿って障害物を避け、ゴールの旗へ向かうパスファインディングのイメージ

この記事でわかること

  • NavigationAgent2D基本の処理サイクル(目標→経路→次の点→移動)
  • get_next_path_position() が返すのは 「次の中継点」 という要点
  • 追跡AI巡回AI の実装
  • velocity_computed による エージェント同士の回避(RVO)
  • 経路再計算の パフォーマンス最適化AStar2D との違い

Sponsored

NavigationAgent2Dの基本サイクル

NavigationAgent2D は、移動させたいキャラ(CharacterBody2D)の 子ノード として置きます。使い方は、毎フレーム次のサイクルを回すだけです。

NavigationAgent2Dの処理サイクル。target_positionで目標を決め、経路を計算し、get_next_path_position()で次の点を受け取り、move_and_slide()で移動、を繰り返す循環図
  1. 目標を決めるtarget_position に行き先の座標を入れる
  2. 経路を計算 :ナビゲーションが自動で最短経路を求める
  3. 次の点を受け取るget_next_path_position() で経路上の次の中継点を取得
  4. そこへ移動 :その点へ向かって move_and_slide()

経路探索や障害物回避の難しい部分は、すべてエージェントが内部で処理してくれます。私たちが書くのは「目標を決める」と「返ってきた点へ動く」だけです。

get_next_path_positionは「次の中継点」

初心者がつまずきやすいのが、get_next_path_position() の意味です。これは ゴールそのものではなく、経路上の「次に向かうべき中継点」 を返します。

get_next_path_position()はゴール(target_position)ではなく、障害物を回り込む経路上の次のウェイポイントを返すことを示す俯瞰図

だから、返ってきた座標へ まっすぐ向かって移動 させます。ゴール(target_position)に直接向かうと障害物にぶつかってしまいますが、中継点を1つずつたどれば、経路に沿って自然に回り込めます。

var next_point := navigation_agent.get_next_path_position()
var direction := global_position.direction_to(next_point)  # 次の中継点への向き
velocity = direction * speed
move_and_slide()
Sponsored

追跡AI:動く目標を追う

まずは、敵がプレイヤーを追いかける追跡AIです。target_positionプレイヤーの現在地 を入れ続けるだけで、障害物を避けながら追ってきます。

追跡は動くプレイヤーを経路で追う、巡回は決めた4地点を順に回る、NavigationAgent2Dの2大用途の対比図

ポイントは、 target_position を毎フレーム更新しない ことです。目標を更新するたびに経路が再計算されるため、毎フレームだと重くなります。Timer で0.2秒ごとくらいに間引きます。

extends CharacterBody2D

@export var speed: float = 250.0
@export var player: Node2D

@onready var navigation_agent: NavigationAgent2D = $NavigationAgent2D
@onready var repath_timer: Timer = $RepathTimer   # 0.2秒周期でautostart

func _ready() -> void:
    repath_timer.timeout.connect(_update_target)

func _update_target() -> void:
    if is_instance_valid(player):
        navigation_agent.target_position = player.global_position  # 目標更新=経路再計算

func _physics_process(_delta: float) -> void:
    if navigation_agent.is_navigation_finished():   # 到達したら止まる
        return
    var next_point := navigation_agent.get_next_path_position()
    velocity = global_position.direction_to(next_point) * speed
    move_and_slide()

is_navigation_finished() で「もう着いた?」を判定できるので、着いたら移動を止めます。経路の再計算は Timer に任せ、_physics_process移動だけ に集中させるのがきれいな形です。

巡回AI:決めた地点を順に回る

次は、決められたポイントを順番に回る巡回AIです。目標に着いたら次のポイントへ切り替える——is_navigation_finished() がここで活きます。

extends CharacterBody2D

@export var speed: float = 150.0
@export var patrol_points: Array[Vector2] = [
    Vector2(100, 100), Vector2(800, 100), Vector2(800, 500), Vector2(100, 500)
]

@onready var navigation_agent: NavigationAgent2D = $NavigationAgent2D
var index := 0

func _ready() -> void:
    navigation_agent.target_position = patrol_points[index]

func _physics_process(_delta: float) -> void:
    if navigation_agent.is_navigation_finished():
        # 次のポイントへ(末尾まで行ったら最初に戻る)
        index = (index + 1) % patrol_points.size()
        navigation_agent.target_position = patrol_points[index]
        return
    var next_point := navigation_agent.get_next_path_position()
    velocity = global_position.direction_to(next_point) * speed
    move_and_slide()

巡回では目標が ポイント到達時にだけ 変わるので、追跡のような間引きは不要です。「いつ追跡に切り替えるか(プレイヤーを見つけたら)」は ステートマシン と組み合わせると、巡回⇄追跡を自然に行き来する敵が作れます。

Sponsored

エージェント同士の回避(RVO)

敵を複数出すと、今度は 敵同士が重なって団子になる 問題が出ます。これを防ぐのが RVO(相互速度障害物法) による回避です。

2体のエージェントが直進すると衝突する場面で、互いに外側へ膨らんでぶつからずにすれ違う様子。velocity_computedで安全な速度を得る図

使うには、エージェントの avoidance_enabled を有効にし、 速度を直接動かす代わりに set_velocity() へ「進みたい速度」を渡します。すると、他のエージェントを避けた 安全な速度velocity_computed シグナルで返ってくるので、それで move_and_slide() します。

func _physics_process(_delta: float) -> void:
    if navigation_agent.is_navigation_finished():
        return
    var next_point := navigation_agent.get_next_path_position()
    var desired := global_position.direction_to(next_point) * speed
    navigation_agent.set_velocity(desired)   # 進みたい速度を渡す(そのまま動かさない)

# 回避を考慮した安全な速度が返ってくる
func _on_navigation_agent_2d_velocity_computed(safe_velocity: Vector2) -> void:
    velocity = safe_velocity
    move_and_slide()

avoidance_enabled がオンのときは、 必ず velocity_computed の中で move_and_slide() する のが肝心です。_physics_process でも直接動かすと二重移動になり、回避が効きません。

パフォーマンスとAStar2Dとの違い

大量のエージェントを動かすときは、 経路の再計算頻度 が最大のボトルネックです。

毎フレーム再計算は計算しすぎで重い、タイマーで0.2秒ごとに間引くと軽い、という経路再計算の負荷の対比図
  • 再計算を間引くtarget_position の更新は Timer で0.2秒ごとなどに。重要度の低い敵はさらに間隔を空ける
  • 画面外は止める :遠くや画面外のAIは set_physics_process(false) で処理を止める
  • NavigationObstacle2D は最小限に :動的障害物は便利ですが高コストなので数を絞る

AStar2Dとの使い分け

グリッドベースのゲームでは、より低レベルな AStar2D が向く場合もあります。

NavigationAgent2DAStar2D
抽象度高レベル(使いやすい)低レベル(柔軟)
セットアップメッシュをベイクするだけポイントと接続を手動定義
障害物回避自動(動的障害物・RVOも)自前で実装
向くケースキャラの自由な空間移動マス目移動・ターン制ストラテジー

マップ上を リアルタイムに自由移動 する一般的な敵AIなら NavigationAgent2D、盤面がマス目で 経路をきっちり制御 したいなら AStar2D、と覚えておけば十分です。

Sponsored

おまけ:先に知っておくと良いこと

  • まず地図を確認 :エージェントが動かない9割は、地図(メッシュ)側の問題です。NavigationRegion2D が範囲を覆ってベイクできているかを最初に疑います。
  • 止まらない・震えるときは許容距離 :目的地でピタッと止まらないなら target_desired_distance を少し大きく(例: 10.0)します。
  • 速度を滑らかにvelocity = velocity.lerp(desired, 0.1) のように補間すると、キビキビしすぎない自然な加減速になります。
  • 判断はAI側に分ける :「巡回するか・追うか・逃げるか」の判断は ステートマシン やビヘイビアツリーの担当です。ナビは「決まった目標への移動」に徹させると、設計がすっきりします。

まとめ

  • 基本サイクルtarget_position を決め、get_next_path_position() の中継点へ move_and_slide()
  • 中継点を1つずつ たどるから、障害物を自然に回り込める
  • 追跡target_positionTimer で間引き更新、巡回 は到達ごとに次ポイントへ
  • RVOavoidance_enabledset_velocity()velocity_computed でエージェント同士の重なりを防ぐ
  • 最適化 は再計算の間引きと画面外の停止が基本

まずは ベイクした地図 の上に敵を1体置き、プレイヤーを target_position にして追跡させてみてください。障害物を回り込んで追ってくる姿が見えたら、ナビゲーションの土台は完成です。あとは巡回やRVO、状態管理を足していけば、ぐっとゲームらしい敵AIになります。

さらに学ぶために