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. 使い方チュートリアル
このコンポーネントを使えば、敵キャラの「スライム」を作るのが一瞬で終わります。
手順①:敵キャラクターの用意
- 新しいシーンを作成し、ルートを
CharacterBody2Dにします(名前はSlimeなど)。 CollisionShape2D(円形など)を追加します。Sprite2Dを追加し、スライムの画像をセットします。
手順②:コンポーネントのアタッチ
Slimeの子ノードとしてNodeを追加し、名前をWanderRoamにします。- スクリプト
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:
# 通常通りランダム
...
