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、動くギミックなどに適用する具体例を挙げながら説明します。

  1. コンポーネントを用意する
    上記の WanderRoam.cs をプロジェクトの Assets フォルダ内(例: Assets/Scripts/WanderSample/)に保存します。
    ファイル名はクラス名と同じ WanderRoam.cs にしてください。
  2. 徘徊させたいオブジェクトにアタッチ
    例として:
    • プレイヤーの前をうろうろする「小動物」NPC
    • ランダムに動き回る「敵スライム」
    • ふらふらと漂う「浮遊オーブ」ギミック

    などの GameObject を Hierarchy 上で選択し、
    Add Component から WanderRoam を追加します。
    Rigidbody を使いたい場合は、先に Rigidbody コンポーネントを追加しておき、
    Move ModeRigidbody に切り替えましょう。

  3. パラメータを調整する
    インスペクターで以下の値を調整して、挙動をチューニングします。
    • 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 にすると、広場の中だけをうろつく村人を簡単に作れます。
  4. プレハブ化して量産する
    調整が終わったら、そのオブジェクトを 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挙動を作っていけます。