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;
}
}
}
使い方の手順
ここからは、実際にシーンに配置して動かすまでの手順を具体的に見ていきましょう。
-
スクリプトを作成する
- Unity の Project ウィンドウで右クリック →
Create > C# Scriptを選択。 - 名前を
WanderRandomにして作成。 - 自動生成された中身をすべて削除し、上記のフルコードを丸ごとコピペして保存。
- Unity の Project ウィンドウで右クリック →
-
キャラクターにアタッチする
- シーン上の「ふらふら歩き回らせたいオブジェクト」を選択(例:
EnemySlimeやNPC_Villagerなど)。 - Inspector の
Add ComponentボタンからWanderRandomを検索して追加。
この時点で、特別な他コンポーネントへの依存はないので、どんな GameObject にも付けられます。
- シーン上の「ふらふら歩き回らせたいオブジェクト」を選択(例:
-
徘徊エリアを設定する
Centerを空欄のままにすると、ゲーム開始時のキャラ位置が中心になります。- 特定のエリア内だけを歩かせたい場合は、空の GameObject を作って「徘徊エリアの中心」として配置し、それを
Centerにドラッグ&ドロップします。 Radiusで徘徊する範囲の半径を調整しましょう(例: 3〜10 くらい)。
シーンビューでオブジェクトを選択すると、水色の円で徘徊エリアが表示されるので、レベルデザイン時の確認がしやすくなります。
-
移動と待機のパラメータを調整する
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)を置き、WanderRandomのCenterに設定。
Radius = 4、Move Speed = 1.2、Min/Max Wait Time = 0.5 / 2.0くらいにしておくと、広場の中をうろうろしている村人が簡単に作れます。 -
例2: アクションゲームの雑魚敵スライム
スライムのスポーン位置を中心にしてRadius = 3、Move 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 クラスを作るのではなく、こうした小さなコンポーネントを組み合わせてキャラクターの振る舞いを構築していくと、プロジェクト全体の保守性がぐっと上がりますよ。
