Unityを触り始めた頃、「とりあえず全部 Update() に書いておけば動くし楽!」と感じて、そのままプレイヤー移動、敵AI、UI更新、エフェクト制御…と全部を1つのスクリプトに詰め込んでしまいがちですよね。
でもそれを続けると、少し仕様を変えたいだけで巨大な Update() をスクロールし続けるハメになり、「どこを触れば壊れないのか」が分からなくなってしまいます。
そこでおすすめなのが、「1つのコンポーネントは1つの責務だけを持つ」方針で小さなスクリプトを積み上げていくやり方です。
この記事では、その一例として「一定時間ごとにランダムな方向へ親を移動させ、待機と移動を繰り返す」挙動だけを担当するコンポーネント WanderRoam を作ってみましょう。
【Unity】放っておくだけでそれっぽい敵AI!「WanderRoam」コンポーネント
WanderRoam は、オブジェクトを「待機 → ランダムな方向へ移動 → また待機…」というループで徘徊させるためのコンポーネントです。
敵キャラ、動物NPC、パトロール用のドローン、ランダムに動くギミックなどにそのまま使えます。
ポイントは以下の通りです。
- 「ランダムに動き回る」という単機能に絞ったコンポーネント
- 待機時間・移動時間・速度・移動範囲などをインスペクターから調整可能
- Rigidbody を使った物理移動にも対応(任意)
- 移動方向は水平面(XZ平面)に限定し、Y軸は固定
フルコード:WanderRoam.cs
using UnityEngine;
namespace WanderSample
{
/// <summary>
/// 一定時間ごとにランダムな方向へ移動し、
/// 「待機」と「移動」を繰り返すシンプルな徘徊コンポーネント。
///
/// - Transform だけで移動するモード
/// - Rigidbody を使って移動するモード
/// の両方に対応しています。
/// </summary>
[DisallowMultipleComponent]
public class WanderRoam : MonoBehaviour
{
/// <summary>
/// どの方法で移動させるか
/// </summary>
private enum MoveMode
{
Transform, // Transform.Translate / position を直接いじる
Rigidbody // Rigidbody.velocity を使う
}
[Header("基本設定")]
[SerializeField]
private MoveMode moveMode = MoveMode.Transform;
[Tooltip("移動速度(1秒あたりのユニット数)")]
[SerializeField]
private float moveSpeed = 2.0f;
[Tooltip("1回の移動フェーズの継続時間(秒)")]
[SerializeField]
private float moveDuration = 1.5f;
[Tooltip("1回の待機フェーズの継続時間(秒)")]
[SerializeField]
private float waitDuration = 1.0f;
[Header("ランダム方向設定")]
[Tooltip("ランダム方向をXZ平面上で選ぶ際の最小角度ステップ(度)\n例: 45 にすると 0,45,90... のような方向だけ選ばれる")]
[SerializeField]
private float directionStepDegree = 15.0f;
[Tooltip("上下方向の移動を許可するか(通常は false でXZ平面のみ)")]
[SerializeField]
private bool allowVertical = false;
[Header("徘徊エリア制限(任意)")]
[Tooltip("徘徊の中心位置。未指定なら開始時の位置を使う")]
[SerializeField]
private Transform centerPoint;
[Tooltip("徘徊範囲の半径。0 以下なら制限なし")]
[SerializeField]
private float roamRadius = 0f;
[Header("デバッグ表示")]
[Tooltip("シーンビューに徘徊範囲を表示するか")]
[SerializeField]
private bool drawGizmo = true;
[Tooltip("Gizmoの色")]
[SerializeField]
private Color gizmoColor = new Color(0.2f, 0.8f, 0.4f, 0.25f);
// 内部状態
private Rigidbody _rigidbody;
private Vector3 _currentDirection = Vector3.zero;
private float _stateTimer = 0f;
private bool _isMoving = false;
private Vector3 _roamCenter;
private void Awake()
{
// Rigidbody モードの時だけ取得を試みる
if (moveMode == MoveMode.Rigidbody)
{
_rigidbody = GetComponent<Rigidbody>();
if (_rigidbody == null)
{
Debug.LogWarning(
$"[WanderRoam] Rigidbody モードですが Rigidbody が見つかりません。" +
$" 自動で Transform モードに切り替えます。", this);
moveMode = MoveMode.Transform;
}
else
{
// 物理挙動を自前で制御したいので、回転や重力などを調整するのもアリ
// (ここではあえて何もしません。必要であればインスペクター側で設定しましょう)
}
}
// 徘徊中心の初期化
_roamCenter = centerPoint != null ? centerPoint.position : transform.position;
// 初期状態は「待機」からスタート
_isMoving = false;
_stateTimer = waitDuration;
_currentDirection = Vector3.zero;
}
private void Update()
{
// 状態タイマーを減らす
_stateTimer -= Time.deltaTime;
if (_stateTimer <= 0f)
{
// 状態切り替え
_isMoving = !_isMoving;
if (_isMoving)
{
// 移動フェーズ開始
_stateTimer = moveDuration;
PickNewDirection();
}
else
{
// 待機フェーズ開始
_stateTimer = waitDuration;
StopMovement();
}
}
// 実際の移動処理
if (_isMoving)
{
Move();
}
}
/// <summary>
/// 新しいランダム方向を決める
/// </summary>
private void PickNewDirection()
{
// ランダムな角度を生成
float horizontalAngle = 0f;
float verticalAngle = 0f;
if (allowVertical)
{
// 上下方向も含めたランダムな方向
horizontalAngle = Random.Range(0f, 360f);
verticalAngle = Random.Range(-45f, 45f); // 上下は少しだけに制限
}
else
{
// XZ平面上でランダムな方向
float step = Mathf.Max(1f, directionStepDegree);
float maxStepCount = 360f / step;
int stepIndex = Random.Range(0, Mathf.CeilToInt(maxStepCount));
horizontalAngle = step * stepIndex;
verticalAngle = 0f;
}
// 角度をベクトルに変換
Quaternion rot = Quaternion.Euler(verticalAngle, horizontalAngle, 0f);
Vector3 dir = rot * Vector3.forward;
// 念のため正規化
_currentDirection = dir.normalized;
}
/// <summary>
/// 現在の方向に応じて移動させる
/// </summary>
private void Move()
{
if (_currentDirection.sqrMagnitude <= 0.0001f)
{
return;
}
Vector3 velocity = _currentDirection * moveSpeed;
switch (moveMode)
{
case MoveMode.Transform:
// Transform を直接動かす
Vector3 delta = velocity * Time.deltaTime;
Vector3 targetPos = transform.position + delta;
// 徘徊範囲の制限がある場合は、中心からの距離をチェック
if (roamRadius > 0f)
{
Vector3 offset = targetPos - _roamCenter;
if (offset.magnitude > roamRadius)
{
// 範囲外に出そうなら、その方向には進まないようにする
// ここでは単純に移動をキャンセル
return;
}
}
transform.position = targetPos;
break;
case MoveMode.Rigidbody:
// Rigidbody の velocity を設定
if (_rigidbody != null)
{
_rigidbody.velocity = velocity;
}
break;
}
}
/// <summary>
/// 移動を停止する(待機状態へ)
/// </summary>
private void StopMovement()
{
_currentDirection = Vector3.zero;
if (moveMode == MoveMode.Rigidbody && _rigidbody != null)
{
_rigidbody.velocity = Vector3.zero;
}
}
/// <summary>
/// シーンビューに徘徊範囲をGizmoで表示
/// </summary>
private void OnDrawGizmosSelected()
{
if (!drawGizmo || roamRadius <= 0f)
{
return;
}
Vector3 center = centerPoint != null ? centerPoint.position : transform.position;
Gizmos.color = gizmoColor;
#if UNITY_EDITOR
// UnityEditor がある場合は半透明の球を描画
Gizmos.DrawSphere(center, roamRadius);
#else
// ビルド時はワイヤーフレームだけにしておく
Gizmos.DrawWireSphere(center, roamRadius);
#endif
}
/// <summary>
/// 外部から徘徊を一時停止/再開したい場合に使える補助メソッド
/// </summary>
public void SetActive(bool active)
{
if (!active)
{
StopMovement();
}
enabled = active;
}
}
}
使い方の手順
ここでは、敵キャラや動物NPC、動くギミックなどに適用する具体例を挙げながら説明します。
-
コンポーネントを用意する
上記のWanderRoam.csをプロジェクトのAssetsフォルダ内(例:Assets/Scripts/WanderSample/)に保存します。
ファイル名はクラス名と同じWanderRoam.csにしてください。 -
徘徊させたいオブジェクトにアタッチ
例として:- プレイヤーの前をうろうろする「小動物」NPC
- ランダムに動き回る「敵スライム」
- ふらふらと漂う「浮遊オーブ」ギミック
などの GameObject を Hierarchy 上で選択し、
Add Componentから WanderRoam を追加します。
Rigidbody を使いたい場合は、先にRigidbodyコンポーネントを追加しておき、
Move ModeをRigidbodyに切り替えましょう。 -
パラメータを調整する
インスペクターで以下の値を調整して、挙動をチューニングします。- Move Speed: 速度。小動物なら 1.0〜2.0、敵なら 3.0〜5.0 など。
- Move Duration: 1回の移動時間。短いほど「小刻みに方向転換」します。
- Wait Duration: 移動と移動の間の待機時間。長いほど「ボーッとしている」感じに。
- Direction Step Degree: 方向の刻み幅。45 にすると8方向だけ、15 なら24方向、0〜5 ならほぼ完全ランダム。
- Allow Vertical: 空中をふわふわ漂わせたいオーブなどはオンに、地上の敵はオフに。
- Center Point / Roam Radius: 徘徊エリアを制限したい場合に設定。
例: 「村の広場」用の空オブジェクトを Center にし、Radius を 5〜10 にすると、広場の中だけをうろつく村人を簡単に作れます。
-
プレハブ化して量産する
調整が終わったら、そのオブジェクトを Prefab 化しておきましょう。
例えば:Slime_Wander.prefab… ランダム徘徊するスライム敵Bird_Wander.prefab… 空中をふわふわ飛ぶ鳥FloatingOrb_Wander.prefab… ランダムに動く光るオーブ
あとはシーンにポンポン配置するだけで、それぞれが自律的にランダム徘徊してくれるので、
レベルデザインが一気に楽になります。
メリットと応用
WanderRoam を導入する一番のメリットは、「ランダムに動き回る」というよくある挙動を、
巨大なAIスクリプトの一部としてではなく、独立した小さなコンポーネントとして切り出せることです。
- プレハブ管理が楽になる
「この敵は巡回パターン」「この敵はランダム徘徊」「この敵はプレイヤー追尾」といったパターンを、
それぞれ別コンポーネントとして用意しておけば、プレハブごとに付け替えるだけで挙動を切り替えられます。
1つの巨大 AI クラスを継承地獄で分岐させる必要がなくなります。 - レベルデザインの試行錯誤がしやすい
敵やNPCの「うろつき方」をインスペクターのパラメータだけで調整できるので、
プログラマがコードを書き換えなくても、レベルデザイナーが自分でチューニングできます。
「このエリアだけは徘徊範囲を狭くしたい」「この敵だけ少し早く動かしたい」といった要望にもすぐ対応できます。 - 他のコンポーネントと組み合わせやすい
WanderRoam は「ランダムに動く」だけに責務を絞っているので、
例えば「プレイヤーを見つけたら追尾に切り替える」コンポーネントと組み合わせて、- 普段は WanderRoam で徘徊
- 一定距離以内にプレイヤーが来たら WanderRoam を無効化して、追尾コンポーネントを有効化
といった拡張もシンプルに実現できます。
最後に、簡単な改造案として、「特定の方向にだけ行きたくない」場合のフィルタリング例を載せておきます。
例えば「+Z 方向(北)には絶対に進ませたくない」というようなケースです。
/// <summary>
/// 特定の方向を避けながら新しいランダム方向を決める例
/// (例: forwardDirection 付近の方向は選ばない)
/// </summary>
private void PickNewDirectionAvoiding(Vector3 forwardDirection, float avoidAngleDegree = 45f)
{
const int maxTry = 10;
for (int i = 0; i < maxTry; i++)
{
// 既存の PickNewDirection のロジックを流用して一旦方向を決める
PickNewDirection();
// forwardDirection との角度をチェック
float angle = Vector3.Angle(forwardDirection.normalized, _currentDirection);
if (angle > avoidAngleDegree)
{
// 十分離れていれば採用
return;
}
}
// どうしても避けられなかった場合は、最後に決めた方向をそのまま使う
}
このように、コアとなる WanderRoam を小さくシンプルに保ったまま、
「方向に重み付けをする」「障害物を避ける」「プレイヤーから離れるように動く」など、
別メソッドや別コンポーネントとして少しずつ拡張していくと、
プロジェクト全体の見通しが良いまま、柔軟なAI挙動を作っていけます。
