Godot 4でNPCや敵キャラを動かすとき、多くの人はこんな感じで書きがちですよね。
- プレイヤーや敵のスクリプトに「移動ロジック」「アニメーション制御」「AI」「エフェクト制御」など全部入り
- シーンツリーは
Player→StateMachine→AI→Navigationみたいに階層が深くなりがち - 「ランダム徘徊する敵」と「プレイヤーの仲間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)」を作ります。
手順①:スクリプトを用意する
- 上のコードを
WanderRandom.gdという名前で保存します(例:res://components/WanderRandom.gd)。 - Godotエディタで
Project→Reload Current Projectして、class_name WanderRandomが認識されるようにしておきましょう。
手順②:敵キャラシーンにコンポーネントをアタッチ
2Dの敵キャラシーンをこんな感じで組んでいるとします:
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── WanderRandom (Node)
Enemyシーンを開くEnemyの子としてNodeを追加(名前はWanderRandomなど)- その子ノードに
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→ のんびり歩くNPCmin_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であれば基本なんでも徘徊させられます。 - レベルデザインが楽:
「この敵はこの部屋だけでウロウロしてほしい」といった要望も、centerとrange_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 をベースにしたカスタムコンポーネントを育てていきましょう。
