Godot 4でNPCや敵キャラを動かすとき、多くの人はこんな感じで書きがちですよね。

  • プレイヤーや敵のスクリプトに「移動ロジック」「アニメーション制御」「AI」「エフェクト制御」など全部入り
  • シーンツリーは PlayerStateMachineAINavigation みたいに階層が深くなりがち
  • 「ランダム徘徊する敵」と「プレイヤーの仲間NPC」両方に似たようなランダム移動コードをコピペ…

これ、最初は動くから気持ちいいんですが、あとから

  • 「この敵だけランダム徘徊やめたい」→ 条件分岐だらけ
  • 「別のシーンでも同じ徘徊行動を使いたい」→ スクリプトをコピペ or 複雑な継承ツリー

と、だんだんつらくなってきます。
そこで今回は、「ランダム徘徊だけ」を独立したコンポーネントとして切り出した WanderRandom コンポーネント を用意しました。

移動したいキャラに「ポンッ」とアタッチするだけで、
「一定間隔でランダムな位置を目的地にして、ふらふら歩き回る」挙動を付けられるようにしていきましょう。

【Godot 4】ふらふら歩くNPCを一発実装!「WanderRandom」コンポーネント

今回の WanderRandom は、

  • 2D向け(Node2D / CharacterBody2D / Area2D などにアタッチ可能)
  • 一定間隔で「ランダムな目標座標」を決めて、そこへ向かって移動
  • 移動範囲(矩形エリア)や待機時間、速度などを @export で調整可能
  • 「移動だけ」責務を持つコンポーネントとして、他のAIやアニメーションと組み合わせやすい

という設計になっています。


フルコード:WanderRandom.gd


extends Node
class_name WanderRandom
## ランダムに目的地を設定して徘徊させるコンポーネント
##
## 親ノードの position を直接いじるので、
## CharacterBody2D や Node2D 系にアタッチして使うことを想定しています。

@export_category("Wander Settings")

@export var enabled: bool = true:
	set(value):
		enabled = value
		if not enabled:
			_velocity = Vector2.ZERO

## 徘徊する中心位置。Vector2.ZERO のままなら、開始時の親ノードの位置を基準にします。
@export var center: Vector2 = Vector2.ZERO

## 徘徊エリアの幅(X方向の半径、矩形の半幅)
@export var range_x: float = 200.0

## 徘徊エリアの高さ(Y方向の半径、矩形の半高さ)
@export var range_y: float = 200.0

## 1秒あたりの移動速度(ピクセル)
@export var speed: float = 80.0

## 新しい目的地を選ぶまでの最小待機時間
@export var min_wait_time: float = 0.5

## 新しい目的地を選ぶまでの最大待機時間
@export var max_wait_time: float = 2.0

## 目的地に「到達した」とみなす距離
@export var arrive_threshold: float = 8.0

## 移動を一時停止させるか(外部からAI制御等で切り替えたいとき用)
@export var paused: bool = false

@export_category("Debug")

## デバッグ用に徘徊エリアと目標位置を描画するか
@export var debug_draw: bool = false

## デバッグ描画の色
@export var debug_color: Color = Color(0.3, 0.8, 1.0, 0.5)

## デバッグ描画の Z インデックス(CanvasItem の z_index とは別物です)
@export var debug_z_index: int = 0


# 内部状態
var _target_position: Vector2
var _velocity: Vector2 = Vector2.ZERO
var _wait_timer: float = 0.0
var _has_center_initialized: bool = false

func _ready() -> void:
	# center がゼロベクトルなら、開始時の親ノードの位置を中心とする
	if center == Vector2.ZERO:
		if owner and owner is Node2D:
			center = (owner as Node2D).position
		elif get_parent() and get_parent() is Node2D:
			center = (get_parent() as Node2D).position
	_has_center_initialized = true

	# 最初の目的地を設定
	_pick_new_target()

	# デバッグ描画用に再描画を要求
	if debug_draw:
		queue_redraw()


func _process(delta: float) -> void:
	if not enabled:
		return
	if paused:
		_velocity = Vector2.ZERO
		return

	# 親ノード(移動させたいノード)を取得
	var target_node := _get_target_node()
	if target_node == null:
		return

	# 目的地までのベクトル
	var to_target: Vector2 = _target_position - target_node.position
	var distance: float = to_target.length()

	# 目的地に到達したら、しばらく待ってから次の目的地へ
	if distance <= arrive_threshold:
		_velocity = Vector2.ZERO
		_wait_timer -= delta
		if _wait_timer <= 0.0:
			_pick_new_target()
	else:
		# ターゲットに向かって移動
		if distance > 0.001:
			var dir: Vector2 = to_target.normalized()
			_velocity = dir * speed
			target_node.position += _velocity * delta

	# デバッグ描画更新
	if debug_draw:
		queue_redraw()


func _get_target_node() -> Node2D:
	# 基本的には親ノードを動かす想定
	if get_parent() and get_parent() is Node2D:
		return get_parent() as Node2D
	# owner 側に Node2D がいればそちらを使っても良い
	if owner and owner is Node2D:
		return owner as Node2D
	return null


func _pick_new_target() -> void:
	# ランダムなオフセットを生成(矩形エリア内)
	var offset_x := randf_range(-range_x, range_x)
	var offset_y := randf_range(-range_y, range_y)
	_target_position = center + Vector2(offset_x, offset_y)

	# 次の目的地までの待機時間をランダムに設定
	_wait_timer = randf_range(min_wait_time, max_wait_time)

	# デバッグ描画更新
	if debug_draw:
		queue_redraw()


func _draw() -> void:
	if not debug_draw:
		return
	if not _has_center_initialized:
		return

	# 中心と範囲を矩形として描画
	var rect := Rect2(
		center - Vector2(range_x, range_y),
		Vector2(range_x * 2.0, range_y * 2.0)
	)
	draw_rect(rect, debug_color, false, 2.0)

	# 現在のターゲット位置を小さな円で描画
	draw_circle(_target_position, 4.0, debug_color)


# --- 公開API ---

## 外部から徘徊エリアを設定し直したいとき用
func set_area(_center: Vector2, _range_x: float, _range_y: float) -> void:
	center = _center
	range_x = _range_x
	range_y = _range_y
	_pick_new_target()


## 一時的に目的地を上書きしたい場合(例:音に反応してその方向へ移動など)
func set_temporary_target(pos: Vector2, wait_time: float = 1.0) -> void:
	_target_position = pos
	_wait_timer = wait_time
	if debug_draw:
		queue_redraw()

使い方の手順

ここからは、実際にシーンに組み込む手順を見ていきましょう。
例として「ふらふら歩き回る敵キャラ(Enemy)」を作ります。

手順①:スクリプトを用意する

  1. 上のコードを WanderRandom.gd という名前で保存します(例:res://components/WanderRandom.gd)。
  2. Godotエディタで ProjectReload Current Project して、class_name WanderRandom が認識されるようにしておきましょう。

手順②:敵キャラシーンにコンポーネントをアタッチ

2Dの敵キャラシーンをこんな感じで組んでいるとします:

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── WanderRandom (Node)
  1. Enemy シーンを開く
  2. Enemy の子として Node を追加(名前は WanderRandom など)
  3. その子ノードに WanderRandom.gd をアタッチ

この構成にしておくと、「移動ロジック」はコンポーネント側に閉じ込められるので、
Enemy 本体のスクリプトは「HP管理」「攻撃」「アニメーション制御」などに集中できます。

手順③:インスペクタでパラメータを調整

WanderRandom ノードを選択すると、インスペクタに以下のようなプロパティが出てきます。

  • enabled … コンポーネント全体のON/OFF。デバッグや一時停止に便利です。
  • center … 徘徊エリアの中心。デフォルトは「開始時の親ノード位置」。
  • range_x, range_y … 徘徊範囲の矩形サイズ(半径)。
  • speed … 移動速度。
  • min_wait_time, max_wait_time … 目的地に到達後の待機時間のランダム範囲。
  • arrive_threshold … どのくらい近づいたら「到達」とみなすか。
  • paused … 外部からAIで制御したいときに使うフラグ。
  • debug_draw … 徘徊エリアとターゲットをエディタ上でも描画して確認できます。

例えば、

  • range_x = 300, range_y = 150 → 横に広くウロウロする敵
  • speed = 40 → のんびり歩くNPC
  • min_wait_time = 0.0, max_wait_time = 0.5 → ほとんど止まらずに動き回る敵

…といった感じで、数値をいじるだけで「キャラの性格」が変わっていくのが気持ちいいですね。

手順④:他のシーンでも再利用する

コンポーネント化の真価は「再利用性」です。
たとえば、動く足場やふらふら動くアイテムにも、そのままアタッチできます。

例:動く床(MovingPlatform)

MovingPlatform (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── WanderRandom (Node)

例:フィールドを歩き回るモブNPC(TownNPC)

TownNPC (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── WanderRandom (Node)

コードを書き換える必要はなく、
「WanderRandom ノードを足す → range_x / range_y / speed を調整する」だけでOKです。


メリットと応用

この WanderRandom コンポーネントを使うことで、

  • シーン構造がシンプル
    深い継承やゴチャっとしたノード階層ではなく、「移動したいノード + WanderRandom(Node)」というフラットな構成になります。
  • 責務の分離
    「移動ロジック」は WanderRandom、「アニメーション制御」は AnimationController、「攻撃AI」は AttackAI…といった形で、役割ごとにスクリプトを分割できます。
  • 使い回しが超簡単
    プレイヤーの仲間NPC、敵、動く床、飾りの鳥など、Node2D であれば基本なんでも徘徊させられます。
  • レベルデザインが楽
    「この敵はこの部屋だけでウロウロしてほしい」といった要望も、centerrange_x / range_y をインスペクタでいじるだけで完結します。

「継承より合成(Composition over Inheritance)」の思想にぴったりで、
「歩き回る」という行動を、どんなオブジェクトにも後付けできるのが気持ちいいですね。

改造案:プレイヤーが近づいたら徘徊を止める

応用の一例として、「プレイヤーが近づいたら徘徊を止める」関数を追加してみましょう。
以下の関数を WanderRandom.gd の末尾あたりに足すだけでOKです。


## プレイヤーが近くにいるかどうかで徘徊ON/OFFを切り替える例
func update_pause_by_player(player: Node2D, stop_radius: float) -> void:
	# 親ノードがいない場合は何もしない
	var target_node := _get_target_node()
	if target_node == null:
		return

	var distance := target_node.position.distance_to(player.position)

	# 一定距離以内なら停止、それ以外なら徘徊再開
	if distance <= stop_radius:
		paused = true
	else:
		paused = false

これを呼び出す側(例:Enemy.gd)で、


func _process(delta: float) -> void:
	var wander: WanderRandom = $WanderRandom
	var player: Node2D = get_tree().get_first_node_in_group("player")
	if player and wander:
		wander.update_pause_by_player(player, 120.0)

のように使えば、「プレイヤーが近づいたら立ち止まり、離れたらまたウロウロ」という挙動になります。
このように、コンポーネント側に小さなユーティリティ関数を足していくと、
プロジェクト全体で「行動パターンのライブラリ」が育っていくのでおすすめです。

ぜひ、自分のゲーム用に WanderRandom をベースにしたカスタムコンポーネントを育てていきましょう。