Unityを触り始めた頃って、ついなんでもかんでも Update() に書いてしまいがちですよね。プレイヤーの移動、カメラ追従、エフェクトの制御、UIの更新……全部ひとつのスクリプトに押し込んでしまうと、最初は動いていても、あとから仕様変更やバグ修正が本当に大変になります。
とくに「敵がプレイヤーの周りをぐるぐる回る」「カメラがターゲットの周囲を円軌道で回転する」といった、周回移動の処理をその場しのぎで書いてしまうと、あちこちに似たようなコードがコピペされて、メンテナンス不能な状態になりがちです。
そこでこの記事では、「周回移動」だけを責務とする小さなコンポーネントとして、OrbitMovement を用意します。任意のターゲットの周囲を、指定した半径・速度でぐるぐる回り続ける挙動を、プレイヤー・敵・オブジェクト・カメラなどに簡単に付け替えられるようにしてみましょう。
【Unity】ターゲットの周りをぐるぐる周回!「OrbitMovement」コンポーネント
ここでは、以下のようなことができる OrbitMovement を実装します。
- 指定したターゲットの周囲を円運動(周回移動)する
- 半径・角速度・回転軸(XY, XZ, YZ など)をインスペクターから調整可能
- 開始角度や、ターゲットからのオフセットも指定可能
- ターゲットを途中で差し替えても動作
フルコード(OrbitMovement.cs)
using UnityEngine;
/// <summary>
/// 指定したターゲットの周囲を、一定の半径と角速度で周回移動させるコンポーネント。
/// カメラ、敵、アイテムなど、あらゆるオブジェクトにアタッチして使える。
/// </summary>
public class OrbitMovement : MonoBehaviour
{
// --- 設定項目 ---
[SerializeField]
[Tooltip("周回の中心となるターゲット。Transform を指定します。")]
private Transform target;
[SerializeField]
[Tooltip("ターゲットからの周回半径(距離)。")]
private float radius = 3f;
[SerializeField]
[Tooltip("角速度(度/秒)。正で反時計回り、負で時計回り。")]
private float angularSpeed = 90f;
[SerializeField]
[Tooltip("周回の開始角度(度)。0 のときは +X 方向からスタート。")]
private float startAngle = 0f;
[SerializeField]
[Tooltip("周回の回転軸。Up=Y軸周り(XZ平面の円)、Right=X軸周り(YZ平面)、Forward=Z軸周り(XY平面)。")]
private OrbitAxis orbitAxis = OrbitAxis.Up;
[SerializeField]
[Tooltip("ターゲットの位置に加算するオフセット。ターゲットのローカル座標系ではなくワールド座標系です。")]
private Vector3 targetOffset = Vector3.zero;
[SerializeField]
[Tooltip("ターゲットが null の場合は、ワールド原点(0,0,0)+オフセットを中心に周回します。")]
private bool orbitAroundWorldOriginWhenNoTarget = true;
[SerializeField]
[Tooltip("ゲーム開始時に現在位置をもとに半径と開始角度を自動算出します。")]
private bool initializeFromCurrentPosition = false;
[SerializeField]
[Tooltip("周回しながら常にターゲットの方向を向かせるかどうか。")]
private bool lookAtTarget = false;
[SerializeField]
[Tooltip("ターゲットを向くときの補間スピード。0 で即座に向く。")]
private float lookAtLerpSpeed = 10f;
// --- 内部状態 ---
// 現在の角度(度)
private float currentAngle;
// 周回の平面上での基準軸(X, Y, Zのどれを使うか)
private Vector3 orbitPlaneAxis1;
private Vector3 orbitPlaneAxis2;
/// <summary>
/// 周回する軸の種類。
/// Up: Y軸周り(XZ平面の円)
/// Right: X軸周り(YZ平面の円)
/// Forward: Z軸周り(XY平面の円)
/// </summary>
private enum OrbitAxis
{
Up,
Right,
Forward
}
private void Awake()
{
// 周回平面の基準ベクトルを決定
SetupOrbitPlaneAxes();
// 角度の初期値を決定
if (initializeFromCurrentPosition)
{
InitializeFromCurrentTransform();
}
else
{
currentAngle = startAngle;
}
}
private void Update()
{
// 毎フレーム、角度を進めて位置を更新
UpdateOrbit(Time.deltaTime);
}
/// <summary>
/// 周回平面の基準ベクトルを設定する。
/// orbitAxis によってどの平面で円を描くかを決める。
/// </summary>
private void SetupOrbitPlaneAxes()
{
// ここではワールド座標系の単位ベクトルを利用して、
// 円運動を構成する2つの直交ベクトルを決めています。
switch (orbitAxis)
{
case OrbitAxis.Up:
// Y軸周りに回転 => XZ平面で円
orbitPlaneAxis1 = Vector3.right; // +X
orbitPlaneAxis2 = Vector3.forward; // +Z
break;
case OrbitAxis.Right:
// X軸周りに回転 => YZ平面で円
orbitPlaneAxis1 = Vector3.up; // +Y
orbitPlaneAxis2 = Vector3.forward; // +Z
break;
case OrbitAxis.Forward:
// Z軸周りに回転 => XY平面で円
orbitPlaneAxis1 = Vector3.right; // +X
orbitPlaneAxis2 = Vector3.up; // +Y
break;
}
}
/// <summary>
/// 現在の Transform の位置をもとに、半径と角度を自動算出する。
/// すでにシーン上で配置した位置から自然に周回を始めたい場合に便利。
/// </summary>
private void InitializeFromCurrentTransform()
{
Vector3 center = GetCurrentCenterPosition();
// 中心から現在位置へのベクトル
Vector3 toCurrent = transform.position - center;
// 半径を現在の距離として設定
radius = toCurrent.magnitude;
if (radius <= 0.0001f)
{
// ほぼ同じ位置にある場合は、開始角度だけ使用
currentAngle = startAngle;
return;
}
// toCurrent を周回平面上の 2軸成分に分解し、
// atan2 を使って角度を求める
// ( orbitPlaneAxis1, orbitPlaneAxis2 は直交している前提)
float x = Vector3.Dot(toCurrent, orbitPlaneAxis1);
float y = Vector3.Dot(toCurrent, orbitPlaneAxis2);
// ラジアンから度に変換
currentAngle = Mathf.Atan2(y, x) * Mathf.Rad2Deg;
}
/// <summary>
/// 現在フレームの deltaTime をもとに角度と位置を更新する。
/// </summary>
/// <param name="deltaTime">前フレームからの経過時間</param>
private void UpdateOrbit(float deltaTime)
{
// 角度を進める(度)
currentAngle += angularSpeed * deltaTime;
// 角度を 0〜360 に正規化しておくと、値が暴走しにくい
currentAngle = Mathf.Repeat(currentAngle, 360f);
// 中心位置を取得
Vector3 center = GetCurrentCenterPosition();
// 角度(度)をラジアンに変換
float rad = currentAngle * Mathf.Deg2Rad;
// 周回平面上の位置(中心からの相対ベクトル)を計算
// pos = center + (axis1 * cosθ + axis2 * sinθ) * radius
Vector3 offset =
(orbitPlaneAxis1 * Mathf.Cos(rad) +
orbitPlaneAxis2 * Mathf.Sin(rad)) * radius;
Vector3 targetPosition = center + offset;
// 実際の Transform の位置を更新
transform.position = targetPosition;
// ターゲットの方向を向かせるオプション
if (lookAtTarget)
{
UpdateLookAt(center, deltaTime);
}
}
/// <summary>
/// 周回の中心となる位置を取得する。
/// ターゲットが設定されていればその位置 + オフセット、
/// そうでなければ原点 + オフセット(オプション)。
/// </summary>
private Vector3 GetCurrentCenterPosition()
{
if (target != null)
{
return target.position + targetOffset;
}
if (orbitAroundWorldOriginWhenNoTarget)
{
return targetOffset; // 原点(0,0,0) + オフセット
}
// ターゲットも原点周回も無効な場合は、現在位置を中心扱いにする
return transform.position;
}
/// <summary>
/// 周回しながらターゲットの方向を向かせる処理。
/// </summary>>
private void UpdateLookAt(Vector3 center, float deltaTime)
{
// 向き先の位置(ターゲットが null でも center を向く)
Vector3 lookTarget = center;
// 目標の回転を計算
Vector3 direction = (lookTarget - transform.position).normalized;
if (direction.sqrMagnitude < 0.0001f)
{
return; // ほぼ同じ位置なら何もしない
}
Quaternion targetRotation = Quaternion.LookRotation(direction, Vector3.up);
if (lookAtLerpSpeed <= 0f)
{
// 即座に向く
transform.rotation = targetRotation;
}
else
{
// なめらかに補間して向く
transform.rotation = Quaternion.Slerp(
transform.rotation,
targetRotation,
lookAtLerpSpeed * deltaTime
);
}
}
// --- 公開API(他のスクリプトから操作したいとき用) ---
/// <summary>
/// 周回のターゲットを動的に変更する。
/// null を渡すと、設定次第で原点周回になります。
/// </summary>
public void SetTarget(Transform newTarget)
{
target = newTarget;
}
/// <summary>
/// 周回半径を変更する。
/// </summary>
public void SetRadius(float newRadius)
{
radius = Mathf.Max(0f, newRadius);
}
/// <summary>
/// 角速度(度/秒)を変更する。
/// </summary>
public void SetAngularSpeed(float newAngularSpeed)
{
angularSpeed = newAngularSpeed;
}
/// <summary>
/// 現在の角度をリセットする。
/// </summary>
public void ResetAngle(float newAngle = 0f)
{
currentAngle = newAngle;
}
}
使い方の手順
ここからは、実際にシーンで使う手順を具体例付きで見ていきましょう。
例1:プレイヤーの周りをぐるぐる回る敵
- スクリプトを用意
上のコードをOrbitMovement.csという名前でAssetsフォルダに保存します。 - シーンにオブジェクトを配置
- プレイヤー用の GameObject(例:
Player)をシーンに配置。 - 敵キャラ用の GameObject(例:
Enemy)をシーンに配置。
- プレイヤー用の GameObject(例:
- 敵に OrbitMovement をアタッチ
Enemyを選択し、Inspector の「Add Component」からOrbitMovementを追加。TargetにPlayerの Transform をドラッグ&ドロップ。Radiusを 3〜5 くらい、Angular Speedを 60〜120 くらいに設定。Orbit AxisはUp(Y軸周り)にすると、上から見て円を描く動きになります。- 敵が常にプレイヤーの方向を向くようにしたい場合は
Look At Targetにチェックを入れます。
- ゲームを再生して確認
再生ボタンを押すと、EnemyがPlayerの周囲をぐるぐる回り続けるはずです。
半径や角速度を変えれば、複数の敵に違う周回パターンを簡単に付けられます。
例2:ターゲットの周りを回り込むカメラ
- カメラにコンポーネントを追加
Main Cameraを選択し、OrbitMovementを追加します。 - プレイヤーをターゲットに指定
TargetにPlayerの Transform を設定します。 - カメラ用にパラメータを調整
Radiusを 5〜10 程度に設定(プレイヤーからの距離)。Angular Speedは 20〜40 程度にして、ゆっくり回るように。Target Offsetの Y を 1.5〜2.0 程度にすると、プレイヤーの頭上あたりを中心に回れます。Look At Targetにチェックを入れると、常にプレイヤーを映すカメラになります。
- 再生してカメラワークを確認
プレイヤーの周りを回転するシネマティックなカメラが、ひとつのコンポーネントだけで実現できます。
例3:動く床の周りを回るコイン(ギミック用)
- 動く床とコインを配置
- 動く床用の GameObject(例:
MovingPlatform)。 - コイン用の GameObject(例:
Coin)。
- 動く床用の GameObject(例:
- コインに OrbitMovement をアタッチ
CoinにOrbitMovementを追加し、TargetにMovingPlatformを設定します。 - パラメータ調整
Radiusを 1〜2 程度にして、床の周囲を小さく周回させる。Angular Speedを 90〜180 にして、素早く回転させる。Orbit AxisをForwardにすると、画面手前奥方向の円運動(XY平面)も簡単に作れます。
- 動く床と一緒にコインも動く
床が移動しても、コインは常に床の周りを周回し続けるため、見た目に楽しいギミックが簡単に作れます。
メリットと応用
OrbitMovement をコンポーネントとして切り出すメリットは、単に「周回移動ができる」だけではありません。
- 責務が明確でテストしやすい
「周回移動だけを担当するコンポーネント」なので、バグが出たときに原因の切り分けがしやすくなります。
プレイヤーの移動ロジックや AI ロジックとは独立しているため、挙動を個別に調整できます。 - プレハブ化でレベルデザインが楽になる
たとえば「プレイヤーの周りを回るドローン」「ボスの周りを回るオーブ」「周回するカメラ」など、
周回移動が必要なオブジェクトをプレハブ化しておけば、シーンにポンポン配置してターゲットを差し替えるだけで使い回せます。 - パラメータ駆動でバリエーション豊富
半径、角速度、開始角度、回転軸、オフセットをインスペクターから変えるだけで、
同じコンポーネントから全く違う動きを簡単に生み出せます。
スクリプトを増やさずに、デザイナー側で動きのバリエーションを作れるのも大きな利点です。 - 他のコンポーネントと組み合わせやすい
OrbitMovementは Transform の位置・回転を制御するだけなので、
アニメーション、パーティクル、サウンド、AI など他のコンポーネントと干渉しづらく、組み合わせの自由度が高いです。
改造案:プレイヤー入力で周回方向を切り替える
たとえば、プレイヤーの入力によって周回方向を切り替えるような改造も簡単にできます。
以下は、キーボードの左右キーで周回の向きを変える関数の例です(新しい Input System を使わないシンプルなサンプル)。
/// <summary>
/// 簡単な入力で周回方向を切り替える例。
/// Update() から呼び出すなどして使います。
/// </summary>
private void HandleInputForDirection()
{
// 左矢印キーで反時計回り(正方向)
if (Input.GetKeyDown(KeyCode.LeftArrow))
{
angularSpeed = Mathf.Abs(angularSpeed);
}
// 右矢印キーで時計回り(負方向)
if (Input.GetKeyDown(KeyCode.RightArrow))
{
angularSpeed = -Mathf.Abs(angularSpeed);
}
}
このように、小さな責務のコンポーネントとして設計しておけば、
「周回移動」×「入力」×「エフェクト」……といった形で、機能を組み合わせていく開発スタイルに自然と移行できます。
Godクラスを避けて、小さなコンポーネントを積み重ねていく設計を意識していきましょう。
