Unityを触り始めたころは、つい Update() にプレイヤー操作も敵AIもエフェクト制御も全部書いてしまいがちですよね。最初は動くので気持ちいいのですが、少し規模が大きくなると「どこを触れば何が変わるのか」「バグの原因がどこなのか」が一気に分かりづらくなります。

とくに「敵がランダムに歩き回る」「NPCがふらふら動く」といった処理を、巨大なAIスクリプトの一部として書いてしまうと、後から「この敵だけランダム徘徊をやめて追跡に変えたい」「このNPCは速度だけ変えたい」といった調整がしづらくなってしまいます。

そこでこの記事では、「ランダムに目的地を決めて、ふらふら歩き回る」機能だけを切り出したコンポーネント 「WanderRandom」 を作っていきます。
どのキャラクターにもポン付けできる、小さな責務のコンポーネントとして用意しておくことで、プレハブ管理やレベルデザインがかなり楽になりますよ。

【Unity】ふらふらランダム徘徊AIを一発導入!「WanderRandom」コンポーネント

今回作る WanderRandom は、ざっくり言うとこんなことをします。

  • 一定間隔で「次の目的地」をランダムに決める
  • 現在位置からその目的地まで、一定速度で歩いていく
  • 指定した半径の「徘徊エリア」からはみ出さないようにする
  • 2D/3Dどちらでも使えるように「XZ平面上を歩く」想定で実装

移動自体は Transform ベースのシンプルなものにしておき、必要に応じて Rigidbody ベースに差し替えられるようにしておきます。

フルコード:WanderRandom.cs


using UnityEngine;

/// <summary>
/// 一定間隔でランダムな目的地を設定し、XZ平面上をふらふら歩き回るコンポーネント。
/// - キャラの足元あたりにアタッチして使う想定
/// - NavMesh を使わない、シンプルな Transform ベースの移動
/// - 3D/2D どちらでも利用可能(2Dなら XZ を XY に読み替えてもOK)
/// </summary>
public class WanderRandom : MonoBehaviour
{
    [Header("徘徊エリア設定")]
    [SerializeField]
    private Transform center; 
    // 徘徊の中心位置。null の場合は Start 時に自分の位置を中心として扱う

    [SerializeField]
    [Min(0f)]
    private float radius = 5f; 
    // 徘徊する円形エリアの半径(XZ平面)

    [Header("移動パラメータ")]
    [SerializeField]
    [Min(0f)]
    private float moveSpeed = 2f; 
    // 移動速度(m/s)

    [SerializeField]
    [Min(0f)]
    private float arrivalDistance = 0.2f; 
    // 目的地に到達したとみなす距離

    [Header("目的地更新パラメータ")]
    [SerializeField]
    [Min(0f)]
    private float minWaitTime = 0.5f; 
    // 次の目的地を選ぶまでの最小待機時間

    [SerializeField]
    [Min(0f)]
    private float maxWaitTime = 2.0f; 
    // 次の目的地を選ぶまでの最大待機時間

    [Header("見た目のゆらぎ")]
    [SerializeField]
    private bool rotateToMoveDirection = true; 
    // 進行方向に向きを合わせるかどうか(Y軸回転)

    [SerializeField]
    [Range(0f, 1f)]
    private float directionSmoothing = 0.2f; 
    // 回転のなめらかさ(0 = 即座に向く, 1 = ほぼ回転しない)

    // 現在の目的地(XZ平面)
    private Vector3 _targetPosition;
    // 次の目的地を決めるまでの残り時間
    private float _waitTimer;
    // 現在「目的地に向かっているか」「待機中か」
    private bool _isMoving;

    private void Start()
    {
        // center が指定されていない場合、自分の位置を中心にする
        if (center == null)
        {
            // ゲーム中に変えたい場合もあるので、Transform を自前で作らず
            // 自分の位置を基準に扱うだけにしておきます。
            GameObject centerObj = new GameObject($"{name}_WanderCenter");
            centerObj.transform.position = transform.position;
            center = centerObj.transform;
        }

        // 最初の目的地を決める
        ChooseNextDestination();
    }

    private void Update()
    {
        // 目的地に向かって移動する処理
        if (_isMoving)
        {
            MoveTowardsTarget();
        }
        else
        {
            // 待機中はタイマーを減らして、0になったら次の目的地を選ぶ
            _waitTimer -= Time.deltaTime;
            if (_waitTimer <= 0f)
            {
                ChooseNextDestination();
            }
        }
    }

    /// <summary>
    /// 新しい目的地をランダムに選び、移動を開始する
    /// </summary>
    private void ChooseNextDestination()
    {
        // 中心からランダムな方向・距離を選ぶ(XZ平面)
        Vector2 randomCircle = Random.insideUnitCircle * radius;

        // XZ 平面に変換(Y は中心の高さを維持)
        _targetPosition = new Vector3(
            center.position.x + randomCircle.x,
            transform.position.y, // 高さは現在の高さを維持
            center.position.z + randomCircle.y
        );

        _isMoving = true;
    }

    /// <summary>
    /// 現在の目的地に向かって移動し、到達したら待機状態に入る
    /// </summary>
    private void MoveTowardsTarget()
    {
        // 現在位置と目的地の差分ベクトル(XZ)
        Vector3 currentPosition = transform.position;
        Vector3 toTarget = _targetPosition - currentPosition;
        toTarget.y = 0f; // 水平移動のみ

        float distance = toTarget.magnitude;

        // すでに目的地に十分近い場合は到達扱い
        if (distance <= arrivalDistance)
        {
            ArriveAtTarget();
            return;
        }

        // 正規化して進行方向を計算
        Vector3 direction = toTarget.normalized;

        // 実際に移動させる
        float moveStep = moveSpeed * Time.deltaTime;
        Vector3 nextPosition = currentPosition + direction * moveStep;
        transform.position = nextPosition;

        // 見た目として、進行方向に向きを合わせる
        if (rotateToMoveDirection && direction.sqrMagnitude > 0.0001f)
        {
            // Y軸だけ回転させる
            Quaternion targetRotation = Quaternion.LookRotation(direction, Vector3.up);

            // directionSmoothing を 0〜1 で扱うため、1 - smoothing を補間係数に利用
            float t = 1f - directionSmoothing;
            transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, t);
        }
    }

    /// <summary>
    /// 目的地に到達した際に呼ばれる処理
    /// 次の目的地を選ぶまでの待機時間をセットする
    /// </summary>
    private void ArriveAtTarget()
    {
        _isMoving = false;

        // min > max になっていた場合は、max を補正しておく(安全策)
        float min = Mathf.Min(minWaitTime, maxWaitTime);
        float max = Mathf.Max(minWaitTime, maxWaitTime);

        // min と max が同じなら固定時間、それ以外ならランダム時間
        _waitTimer = (Mathf.Approximately(min, max))
            ? min
            : Random.Range(min, max);
    }

    /// <summary>
    /// Gizmos で徘徊エリアと現在の目的地を可視化する
    /// </summary>
    private void OnDrawGizmosSelected()
    {
        // 中心位置を決定(プレイ前でも分かりやすくする)
        Vector3 centerPos = center != null ? center.position : transform.position;

        // 徘徊エリア(円)を描画
        Gizmos.color = new Color(0f, 0.7f, 1f, 0.25f);
        DrawWireDisc(centerPos, Vector3.up, radius);

        // 現在の目的地を赤い球で描画(プレイ中のみ)
        if (Application.isPlaying)
        {
            Gizmos.color = Color.red;
            Gizmos.DrawSphere(_targetPosition, 0.15f);
        }
    }

    /// <summary>
    /// シーンビューで円(ディスク)を描画するヘルパー
    /// Unity 6 では Handles を使わなくても簡易的な円を描けるように、
    /// 自前で線を何本か描画して近似します。
    /// </summary>
    private void DrawWireDisc(Vector3 centerPos, Vector3 normal, float r, int segments = 32)
    {
        if (r <= 0f) return;

        // 法線ベクトルに対して垂直な2軸を求める
        Vector3 tangent = Vector3.Cross(normal, Vector3.right);
        if (tangent.sqrMagnitude < 0.001f)
        {
            tangent = Vector3.Cross(normal, Vector3.forward);
        }
        tangent.Normalize();
        Vector3 bitangent = Vector3.Cross(normal, tangent);

        Vector3 prevPoint = centerPos + tangent * r;
        float angleStep = 360f / segments;

        for (int i = 1; i <= segments; i++)
        {
            float angle = angleStep * i * Mathf.Deg2Rad;
            Vector3 dir = Mathf.Cos(angle) * tangent + Mathf.Sin(angle) * bitangent;
            Vector3 nextPoint = centerPos + dir * r;
            Gizmos.DrawLine(prevPoint, nextPoint);
            prevPoint = nextPoint;
        }
    }
}

使い方の手順

ここからは、実際にシーンに配置して動かすまでの手順を具体的に見ていきましょう。

  1. スクリプトを作成する
    • Unity の Project ウィンドウで右クリック → Create > C# Script を選択。
    • 名前を WanderRandom にして作成。
    • 自動生成された中身をすべて削除し、上記のフルコードを丸ごとコピペして保存。
  2. キャラクターにアタッチする
    • シーン上の「ふらふら歩き回らせたいオブジェクト」を選択(例: EnemySlimeNPC_Villager など)。
    • Inspector の Add Component ボタンから WanderRandom を検索して追加。

    この時点で、特別な他コンポーネントへの依存はないので、どんな GameObject にも付けられます。

  3. 徘徊エリアを設定する
    • Center を空欄のままにすると、ゲーム開始時のキャラ位置が中心になります。
    • 特定のエリア内だけを歩かせたい場合は、空の GameObject を作って「徘徊エリアの中心」として配置し、それを Center にドラッグ&ドロップします。
    • Radius で徘徊する範囲の半径を調整しましょう(例: 3〜10 くらい)。

    シーンビューでオブジェクトを選択すると、水色の円で徘徊エリアが表示されるので、レベルデザイン時の確認がしやすくなります。

  4. 移動と待機のパラメータを調整する
    • Move Speed: 1〜3 くらいにすると、ゆっくりふらふら歩く感じになります。
    • Arrival Distance: 0.1〜0.3 くらいにしておくと、目的地の「手前」でも到達扱いになるので、カクカクしにくいです。
    • Min Wait Time / Max Wait Time: 0〜2 秒くらいの範囲でランダムに待機時間が変わるようにすると、より自然な動きになります。
    • Rotate To Move Direction: 有効にすると、進行方向にキャラの向きが変わります(3Dキャラ向け)。2Dゲームでスプライトの向きだけを変えたい場合は、ここをオフにして別コンポーネントで制御してもOKです。
    • Direction Smoothing: 0 に近いほどキビキビ方向転換し、1 に近づけるとゆっくりと滑らかに回転します。

具体的な使用例

  • 例1: RPGの村人NPCに付ける
    村の広場に空の GameObject(例: VillageCenter)を置き、WanderRandomCenter に設定。
    Radius = 4Move Speed = 1.2Min/Max Wait Time = 0.5 / 2.0 くらいにしておくと、広場の中をうろうろしている村人が簡単に作れます。
  • 例2: アクションゲームの雑魚敵スライム
    スライムのスポーン位置を中心にして Radius = 3Move Speed = 1.5 くらいに設定。
    プレイヤーが近づいたら別コンポーネントで追跡AIに切り替える、といった構成にすると、責務を分けた設計になります。
  • 例3: 動く床やギミック
    動く足場に WanderRandom を付けると、「なんとなく行ったり来たりする足場」が簡単に作れます。
    Rotate To Move Direction をオフにして、Move Speed を小さめ(0.5〜1.0)にすると、ゆっくり動くギミックとして使えます。

メリットと応用

WanderRandom を単独のコンポーネントとして切り出しておくと、以下のようなメリットがあります。

  • プレハブがシンプルになる
    「ランダム徘徊」はこのコンポーネントだけが担当するので、敵AIやNPC制御スクリプトは「会話」「追跡」「攻撃」など別の責務に集中できます。
    例えば EnemyAI からランダム徘徊ロジックを全部追い出してしまえば、コードの見通しがかなり良くなります。
  • レベルデザイン時の調整が楽
    シーンビューで徘徊エリア(円)が見えるので、「このNPCはこの範囲だけ」「この敵はここから出てこない」といった調整がインスペクタの数値変更だけで完結します。
    スクリプトを触らずにデザイナーさんがパラメータをいじれるのも大きな利点ですね。
  • 再利用性が高い
    「ただランダムに動き回る」という汎用的な振る舞いなので、敵、NPC、ギミック、エフェクト用ダミーなど、いろいろなオブジェクトにそのまま流用できます。
    「とりあえずランダムに動かしておきたい」プロトタイプ段階でも役立ちます。

改造案:プレイヤーが近づいたら徘徊を止める

簡単な応用として、「プレイヤーが近づいてきたらランダム徘徊を一時停止する」処理を追加してみましょう。
以下は WanderRandom に追記できる void 関数の例です(プレイヤーとの距離チェック用)。


/// <summary>
/// プレイヤーとの距離をチェックして、近づいたら徘徊を止める例。
/// - playerTransform: プレイヤーの Transform
/// - stopDistance: この距離以内に入ったら停止
/// この関数を Update() の末尾などから呼び出して使います。
/// </summary>
private void CheckPlayerAndStopWandering(Transform playerTransform, float stopDistance)
{
    if (playerTransform == null) return;

    float distance = Vector3.Distance(transform.position, playerTransform.position);

    if (distance <= stopDistance)
    {
        // プレイヤーが近いので移動停止
        _isMoving = false;
        _waitTimer = Mathf.Infinity; // 無限待機(外部から再開させる前提)
    }
}

このように、「ランダム徘徊」という1つの責務を小さなコンポーネントに閉じ込めておけば、別のコンポーネントから「開始」「停止」「追跡へ切り替え」などの制御を簡単に行えます。
巨大な God クラスを作るのではなく、こうした小さなコンポーネントを組み合わせてキャラクターの振る舞いを構築していくと、プロジェクト全体の保守性がぐっと上がりますよ。