Unityを触り始めた頃って、ついプレイヤー操作もカメラ制御も砲台の制御も、全部ひとつの Update() に書いてしまいがちですよね。
最初は動くので満足できるのですが、少し機能を足すたびに「どこを直せばいいんだっけ?」「この回転は戦車本体用?砲塔用?」とコードがぐちゃぐちゃになりがちです。
特に「戦車の本体は移動方向を向きつつ、砲塔だけはターゲットを追尾する」といった処理を、1つのスクリプトにまとめてしまうと、移動ロジックと照準ロジックが絡み合って保守性が一気に落ちます。
そこでこの記事では、砲塔の旋回だけに責務を絞ったコンポーネント 「TurretRotation」 を用意して、
「戦車の移動」と「砲塔の照準」をきれいに分離する方法を紹介します。
【Unity】ターゲットを追尾する砲塔をサクッと実装!「TurretRotation」コンポーネント
このコンポーネントは、
- 戦車本体(親オブジェクト)は自由に移動・回転
- 砲塔(子オブジェクト)は、指定したターゲット方向だけを向く
- 回転速度や制限角度をインスペクターから調整可能
といった形で、砲塔の「横方向の旋回」専用として動作します。
縦方向(ピッチ)を別コンポーネントに切り出すことで、さらに責務を分割しやすくなりますね。
フルソースコード(TurretRotation.cs)
using UnityEngine;
/// <summary>
/// 親(戦車など)の移動・回転とは独立して、
/// 砲塔オブジェクトだけをターゲット方向に旋回させるコンポーネント。
/// 水平方向(Y軸)への旋回に特化しています。
/// </summary>
public class TurretRotation : MonoBehaviour
{
[Header("ターゲット設定")]
[SerializeField]
private Transform target;
// 砲塔が向くターゲット。プレイヤー、敵、マウス位置を表すダミーなど。
[Header("回転パラメータ")]
[SerializeField]
[Tooltip("1秒あたりの回転速度(度)。0 の場合は瞬時に向く。")]
private float rotationSpeed = 180f;
[SerializeField]
[Tooltip("砲塔の基準方向からの最大旋回角度(度)。0以下なら無制限。")]
private float maxYawAngle = 0f;
[SerializeField]
[Tooltip("ターゲットとの距離がこの値より小さいときは旋回しない。")]
private float minTargetDistance = 0.1f;
[Header("デバッグ")]
[SerializeField]
[Tooltip("ターゲットが設定されていないときに警告を出すかどうか。")]
private bool warnIfNoTarget = true;
// 砲塔の「基準向き」(ワールド空間)を保持するためのクォータニオン
private Quaternion _initialWorldRotation;
// Start よりも前に呼ばれる。親の回転などが確定した後に初期向きを保存したいので Awake で取得。
private void Awake()
{
// シーン開始時点での砲塔のワールド回転を保持
_initialWorldRotation = transform.rotation;
}
private void Update()
{
if (target == null)
{
if (warnIfNoTarget)
{
// 毎フレーム出すとうるさいので、Editor 実行中のみ、かつ一定間隔にするのもアリ
Debug.LogWarning($"[TurretRotation] ターゲットが設定されていません: {name}");
// 一度だけ出したい場合はフラグを持つなどで制御しましょう
warnIfNoTarget = false;
}
return;
}
// ターゲットへの方向ベクトル(ワールド空間)
Vector3 toTarget = target.position - transform.position;
// ターゲットが近すぎる場合は無視(ゼロ除算や急激な回転を避けるため)
if (toTarget.sqrMagnitude < minTargetDistance * minTargetDistance)
{
return;
}
// 水平方向だけを考慮したいので、Y成分を無視して水平面に投影
Vector3 toTargetOnPlane = new Vector3(toTarget.x, 0f, toTarget.z);
// ターゲットが真上/真下に近すぎて水平方向の成分がほぼゼロなら、回転しない
if (toTargetOnPlane.sqrMagnitude < 0.0001f)
{
return;
}
// 砲塔が向くべき「理想の回転」を計算(ワールド空間)
Quaternion desiredWorldRotation = Quaternion.LookRotation(toTargetOnPlane.normalized, Vector3.up);
// 最大旋回角度を制限したい場合はここでクランプ
if (maxYawAngle > 0f)
{
desiredWorldRotation = ClampYaw(desiredWorldRotation);
}
// rotationSpeed が 0 の場合は瞬時に向く
if (rotationSpeed <= 0f)
{
transform.rotation = desiredWorldRotation;
}
else
{
// 現在の回転から理想の回転へ、一定速度で補間
transform.rotation = Quaternion.RotateTowards(
transform.rotation,
desiredWorldRotation,
rotationSpeed * Time.deltaTime
);
}
}
/// <summary>
/// 砲塔の「基準向き」からの Y 軸回転(ヨー角)を maxYawAngle の範囲に収める。
/// 基準向きはシーン開始時点のワールド回転。
/// </summary>
private Quaternion ClampYaw(Quaternion desiredWorldRotation)
{
// 基準回転の逆を掛けて、基準からの相対回転を求める
Quaternion relative = Quaternion.Inverse(_initialWorldRotation) * desiredWorldRotation;
// 相対回転をオイラー角に変換
Vector3 relativeEuler = relative.eulerAngles;
// Unity のオイラー角は 0〜360 の範囲なので、-180〜180 に変換して扱いやすくする
float yaw = NormalizeAngle(relativeEuler.y);
// 指定範囲にクランプ
yaw = Mathf.Clamp(yaw, -maxYawAngle, maxYawAngle);
// クランプしたヨー角だけを使って相対回転を再構築
Quaternion clampedRelative = Quaternion.Euler(0f, yaw, 0f);
// 基準回転に相対回転を掛けて、ワールド回転に戻す
return _initialWorldRotation * clampedRelative;
}
/// <summary>
/// 角度を -180〜180 の範囲に正規化するヘルパー関数。
/// 例: 350 → -10, 190 → -170
/// </summary>
private float NormalizeAngle(float angle)
{
angle %= 360f;
if (angle > 180f)
{
angle -= 360f;
}
return angle;
}
/// <summary>
/// 外部からターゲットを動的に変更したいとき用の公開メソッド。
/// 例: 敵がプレイヤーを見失ったらターゲットを null にする、など。
/// </summary>
public void SetTarget(Transform newTarget)
{
target = newTarget;
}
/// <summary>
/// 現在のターゲットを取得するためのアクセサ。
/// </summary>
public Transform GetTarget()
{
return target;
}
}
使い方の手順
ここでは代表的な例として「戦車の砲塔がプレイヤーを追尾する」ケースを想定します。
-
砲塔オブジェクトを用意する
戦車プレハブの階層を次のように分けておきます。- Tank (親) … 本体。移動や旋回を行う。
- └ Turret (子) … 砲塔メッシュがアタッチされたオブジェクト。
砲塔を水平に回転させたいので、Turret のローカルY軸が「砲塔の上方向」、Z軸が「砲身の向き」になるようモデルを調整しておくと扱いやすいです。
-
TurretRotation コンポーネントをアタッチ
Turret オブジェクトを選択し、
Add Component > TurretRotationを追加します。
これで砲塔の回転制御はこのコンポーネントに任せられるようになります。 -
ターゲットを設定する
シーン内にプレイヤーオブジェクト(例: Player)を用意し、その Transform を TurretRotation のTargetにドラッグ&ドロップします。
これだけで、ゲーム再生中に戦車が動いても、砲塔は常にプレイヤー方向を追尾するようになります。他の具体例:
- 敵タレットがプレイヤーを狙う:敵タレットの砲塔に TurretRotation を付け、ターゲットにプレイヤーを設定。
- 固定砲台が動く床上にある:固定砲台のベースは床の動きに追従、砲塔は TurretRotation でプレイヤーを追尾。
- タワーディフェンスの砲台:敵ウェーブごとに一番近い敵の Transform を
SetTarget()で渡す。
-
回転パラメータを調整する
Rotation Speed… 砲塔の追尾速度。遅くすると「重たい砲塔」感が出ます。Max Yaw Angle… 左右の最大旋回角度。例えば 90 にすると、前方 ±90度の範囲だけを狙う砲塔になります。Min Target Distance… 砲塔のすぐ近くのターゲットを無視したい場合に使用します。
これらはプレハブ化しておけば、シーンごとに異なる砲塔の性格(高速回転、限定射角など)を簡単に作り分けられます。
メリットと応用
TurretRotation を導入する一番のメリットは、戦車本体のスクリプトから「砲塔の向き」という責務を切り離せることです。
- 戦車の移動ロジック(入力、物理、サスペンションなど)と、砲塔の照準ロジックが完全に分離できる。
- 砲塔の回転仕様を変えたいとき、戦車本体のコードに触る必要がない。
- 同じ TurretRotation を、戦車、固定砲台、ボスの肩キャノンなど、複数のプレハブで使い回せる。
レベルデザインの観点でも、
- 「この砲台は左右 45 度だけ」「この砲台は 180 度まで回る」などをインスペクターで調整できる。
- プレハブを複製してパラメータだけ変えることで、バリエーション豊かな敵を量産しやすい。
- ターゲットを差し替えるだけで、プレイヤー追尾タレット・拠点防衛タレットなどを簡単に作れる。
といった形で、「プレハブを組み合わせてゲームを組み立てる」スタイルに非常に相性が良いコンポーネントです。
応用として、例えば「ターゲットが一定距離以内に入ったときだけ砲塔を向ける」ようなロジックを足したい場合、
責務を増やしすぎない範囲で、次のような小さなメソッドを追加するのもアリです。
/// <summary>
/// ターゲットが有効レンジ内にいるかどうかを判定する簡易チェック。
/// 例えば射撃コンポーネント側から呼び出して、
/// 「砲塔が狙える距離にいる敵だけを攻撃する」といった使い方ができます。
/// </summary>
public bool IsTargetInRange(float range)
{
if (target == null)
{
return false;
}
float sqrDistance = (target.position - transform.position).sqrMagnitude;
return sqrDistance <= range * range;
}
このように、砲塔の「向き」と「射程チェック」を小さなメソッドとして切り出しておくと、
別コンポーネントの「射撃制御」「AI制御」から再利用しやすくなり、結果的にプロジェクト全体の見通しが良くなります。
