Godot 4のコンポーネント指向開発シリーズ、今回は敵キャラクターやNPCに「生命感」を吹き込む**「WanderRoam (ランダム徘徊)」**です。

スライムや村人など、プレイヤーが操作しないキャラクターが棒立ちだと世界が寂しくなります。このコンポーネントをつけるだけで、彼らが自由に歩き回り、時々立ち止まるような**「気まぐれな動き」**を自動化できます。


このコンポーネントは、「移動」と「待機」をランダムな時間間隔で繰り返す無限ループを自動で実行します。ステートマシン(状態遷移)のような難しいコードを書かずに、自律的な動きを実現します。

1. コンポーネントのコード (Full Code)

以下のコードをコピーして、WanderRoam.gd という名前で保存してください。

class_name WanderRoam
extends Node

## 親ノードをランダムな方向に徘徊(移動&待機)させるコンポーネント
## 親ノードは CharacterBody2D を想定しています。

# --- 設定パラメータ ---
@export_group("Wander Settings")
@export var move_speed: float = 50.0        ## 移動速度
@export var move_duration_min: float = 1.0  ## 移動時間の最小値(秒)
@export var move_duration_max: float = 3.0  ## 移動時間の最大値(秒)
@export var wait_duration_min: float = 1.0  ## 待機時間の最小値(秒)
@export var wait_duration_max: float = 3.0  ## 待機時間の最大値(秒)

# --- 内部変数 ---
var _parent: CharacterBody2D
var _current_velocity: Vector2 = Vector2.ZERO
var _is_active: bool = true

func _ready() -> void:
	# 親ノードの取得と確認
	_parent = get_parent() as CharacterBody2D
	if not _parent:
		push_error("WanderRoam: 親が CharacterBody2D ではありません。")
		set_physics_process(false)
		return

	# 徘徊ルーチンを開始(非同期ループ)
	_start_wander_loop()

func _physics_process(_delta: float) -> void:
	# 決定された速度を親に適用して動かす
	# 物理挙動なので、壁に当たっても move_and_slide が上手く処理してくれる
	_parent.velocity = _current_velocity
	_parent.move_and_slide()

# --- 思考ルーチン ---
func _start_wander_loop() -> void:
	# ノードが存在する限り無限ループ
	while is_inside_tree() and _is_active:
		
		# 1. 待機フェーズ (Wait)
		_current_velocity = Vector2.ZERO
		var wait_time = randf_range(wait_duration_min, wait_duration_max)
		await get_tree().create_timer(wait_time).timeout
		
		# ループ再開時にまだツリーにいるか確認(安全策)
		if not is_inside_tree(): return

		# 2. 移動方向の決定 (Decide Direction)
		var random_angle = randf() * TAU # 0 ~ 2π (360度)
		var direction = Vector2.RIGHT.rotated(random_angle)
		_current_velocity = direction * move_speed
		
		# 3. 移動フェーズ (Move)
		var move_time = randf_range(move_duration_min, move_duration_max)
		await get_tree().create_timer(move_time).timeout

2. 使い方チュートリアル

このコンポーネントを使えば、敵キャラの「スライム」を作るのが一瞬で終わります。

手順①:敵キャラクターの用意

  1. 新しいシーンを作成し、ルートを CharacterBody2D にします(名前は Slime など)。
  2. CollisionShape2D(円形など)を追加します。
  3. Sprite2D を追加し、スライムの画像をセットします。

手順②:コンポーネントのアタッチ

  1. Slime の子ノードとして Node を追加し、名前を WanderRoam にします。
  2. スクリプト WanderRoam.gd をアタッチします。

シーン構成図:

Slime (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── WanderRoam (Node)  <-- これだけで動き出します

手順③:パラメータ調整

インスペクターで以下のように設定すると、生き物らしい動きになります。

  • Move Speed: 30 (スライムなので遅く)
  • Move Duration Min/Max: 0.5 / 1.5 (ちょこまか動く)
  • Wait Duration Min/Max: 1.0 / 4.0 (たまに長くぼーっとする)

手順④:実行

シーンを実行して見てください。スライムが勝手に動き出し、止まり、また別の方向へ歩き出すはずです。

CharacterBody2D の物理演算を使っているため、壁にぶつかっても突き抜けず、壁沿いに滑ったり止まったりする挙動が自動で付いてきます。


3. 応用テクニック:見た目との連動

「動いている時は歩行アニメーションさせたい」「右に進むなら右を向かせたい」という場合、親ノードのスクリプトで velocity を監視するのが最もきれいな設計(Observerパターン的アプローチ)です。

例:親(Slime.gd)でアニメーション制御する

コンポーネント側には「動きの計算」だけを任せ、親側で「見た目」を管理します。

# Slime.gd (親ノードのスクリプト例)
extends CharacterBody2D

@onready var sprite = $Sprite2D

func _process(_delta):
    # velocity(WanderRoamが勝手に書き換えている値)を見て向きを変える
    if velocity.length() > 0:
        # 動いている時
        # velocity.x がプラスなら右向き、マイナスなら左向き
        sprite.flip_h = velocity.x < 0
        # $AnimationPlayer.play("walk") 
    else:
        # 止まっている時
        # $AnimationPlayer.play("idle")
        pass

例:エリア制限(簡易版)

もしスライムが遠くに行き過ぎないようにしたい場合、前述の「TargetFollower」の発想を少し借りて、WanderRoam.gd を改造し、「初期位置から離れすぎたら、ランダム移動ではなく初期位置に戻る移動をする」というロジックを挟むと、**「縄張りを持つ敵」**になります。

# 改造ヒント: 初期位置を覚えておき、遠すぎたら戻る
var _start_pos: Vector2

func _ready():
    _start_pos = _parent.global_position
    # ...

func _start_wander_loop():
    while ...:
        # 方向決定フェーズでの改造
        if _parent.global_position.distance_to(_start_pos) > 200.0:
             # 離れすぎ!家(初期位置)の方向へ帰る
             var direction = (_start_pos - _parent.global_position).normalized()
             _current_velocity = direction * move_speed
        else:
             # 通常通りランダム
             ...