【Godot】敵の移動アルゴリズム

今回はGodot Engineで敵の移動アルゴリズムを作る方法を解説します。

追跡

まずはプレイヤーの方向に愚直に向かってくる方法です。 このパターンは近距離攻撃と得意とする敵が使います。

この実装をするのは簡単で、プレイヤーの座標がわかればそこに向かっていくだけです。

const MOVE_SPEED = 100.0

func _process(delta:float) -> void:
  var player:Player # プレイヤー.
  var v := player.position - self.position # プレイヤーへの方向ベクトルを計算.
  v = v.normalize() # 正規化.
  v *= MOVE_SPEED # 移動速度.
  self.position += v * delta

ただこの動きは単調なので「一時停止」「旋回速度を考慮する」といった変化をつけると良いです。

var _timer := 0.0 # 経過時間.
var _wait_time := 0.0 # 一時停止時間.

# 移動処理.
func _update_move(delta:float) -> void:
    if _wait_time > 0:
        # 一時停止中.
        _wait_time -= delta
        return
    
    _timer += delta
    if _timer > 3.0:
        # 3秒動いたら1秒止まる.
        _wait_time = 1.0
        _timer -= 3.0
        return
    
    # 移動処理.
    var v = Vector2.LEFT
    ...

完全に停止するだけでなく、速度に緩急をつけてみても良いと思います。

「旋回」は移動する方向を角度として保持し、旋回速度に制限を持たせる方法です。

旋回速度の上限を持たせると、プレイヤーは後ろに回り込むことで安全に敵を倒せるという攻略が生まれます。

# 狙い撃ち角度を取得.
var aim = Common.get_aim(self)
# 現在の角度に対する角度差を求める.
var d = Common.diff_angle(_angle, aim)
if abs(d) > 90:
    _speed = MOVE_SPEED * 0.3 # 速度制限.
else:
    _speed = MOVE_SPEED
if abs(d) > 30: # 旋回速度制限 (30度想定).
    d = sign(d) * 30
aim = _angle + (d * delta)

diff_angle() は角度差を求めるもので、作っておくと旋回処理を行う際にとても便利です。

extends Node

class_name Common

# プレイヤー.
static var _player:Player

# セットアップ.
static func setup(player:Player) -> void:
    _player = player

# 狙う撃ち角度を求める.
static func get_aim(node:Node2D) -> float:
    var angle := _player.position.angle_to_point(node.position)
    return rad_to_deg(angle * -1) # 上を90度にしたいので反転.

# 最短の角度差を求める
# @param now 現在の角度
# @param next 目標となる角度
static func diff_angle(now:float, next:float) -> float:
    var d := next - now
    d -= floor(d / 360.0) * 360.0
    if d > 180:
        d -= 360
    return d

2025.9.22 追記:lerp_angle()で角度の補間が可能

Godotの標準関数をあまり調べていなかったのですが lerp_angle() を使うと2つの角度から最短の角度に向かって補間した角度を求めることができます。

以下、実装例です。

extends Node2D

const ROTATE_SPEED = 0.5 # 旋回速度.

var _obj_pos := Vector2(480, 320) # 初期位置.
var _obj_angle := PI/2 # 最初は下向き.

func _process(_delta: float) -> void:
    # マウスカーソルに向かって動く.
    var target = get_global_mouse_position()
    # 狙う撃ち角度.
    var aim = _obj_pos.angle_to_point(target)
    _obj_angle = lerp_angle(_obj_angle, aim, ROTATE_SPEED * _delta)
    
    # 描画.
    queue_redraw()

func _draw() -> void:
    # オブジェクトの位置を描画.
    var size := Vector2(32, 32)
    var rect := Rect2(_obj_pos-(size/2), size)
    draw_rect(rect, Color.FOREST_GREEN)
    # 視線を描画.
    var lay = Vector2(1280, 0).rotated(_obj_angle)
    draw_line(_obj_pos, _obj_pos+lay, Color.RED)

ただ私が自作した diff_angle() と比較すると、「1. 補間率に合わせた回転後の角度を返す(角度差ではない)」「2. 角度の単位は "ラジアン" を入出力とする」といった相違点があります。そのため、場合によっては使い方に工夫が必要となるかもしれません。

逃走

プレイヤーとは逆の方向に移動するパターンです。

これは「追跡」のベクトルを逆、または180度反転させます。

# 狙い撃ち (プレイヤーへの) 角度.
var aim = Common.get_aim(self)
# 180度回転すると逆向きになるので離れる
aim += 180

間合い (距離) を取る

プレイヤーから離れすぎず、近づきもしないパターンです。 このパターンは中〜遠距離から攻撃する敵の行動として使います。

例えば以下の行動ルールです。

  • 「300」より近づくと「逃走」
  • 「500」より離れると「追跡」
  • それ以外 (300〜500)の場合は立ち止まる

このルールを実装すると以下のコードとなります。

# 狙い撃ち角度の取得.
var aim = Common.get_aim(self)
# プレイヤーとの距離を計算.
var dist = Common.get_target_distance(self)
# 速度を設定.
_speed = MOVE_SPEED
if dist < 300:
    aim += 180 # 近づいたら離れる
elif dist < 500:
    _speed = 0 # 300〜500の場合は様子見 (立ち止まる).
else:
    pass # 500以上離れていたら近づく.

回り込む

間合いを取るに近いですが、周りを取り囲む(周りを回転する) ように移動するパターン。

例えば以下の行動ルールです。

  • 「200」より近づくと「逃走」
  • 「300」より離れると「追跡」
  • それ以外 (200〜300)の場合は回り込む

このルールを実装すると以下のコードとなります。

var aim = Common.get_aim(self)
var dist = Common.get_target_distance(self)
_speed = MOVE_SPEED
if dist < 200:
    aim += 180 # 近づいたら離れる
elif dist < 300:
    aim += 90 # 回転する.
else:
    pass # 500以上離れていたら近づく.

関連記事

2dgames.jp