Unityを触り始めた頃にありがちなのが、プレイヤー操作も、カメラ制御も、弾の挙動も、敵AIも、全部ひとつの Update() に書いてしまうパターンですね。
動き始めたときはうまくいっているように見えますが、少し仕様変更が入ると「どこを直せばいいのか分からない」「別のシーンで使い回せない」「バグが出ても原因を切り分けられない」といった問題が一気に噴き出します。

そこでおすすめなのが、「弾の誘導ロジックだけ」を担当する小さなコンポーネントを用意してしまうやり方です。
今回は、ターゲットに向かって徐々に旋回して追いかける 誘導弾 を実装する HomingMissile コンポーネントを作ってみましょう。

【Unity】徐々に曲がる追尾弾をコンポーネント化!「HomingMissile」コンポーネント

このコンポーネントは、

  • ターゲットの方向を毎フレーム計算
  • 最大旋回角度を超えないように少しずつ向きを変える
  • 常に「前方向」に進む

という役割だけに絞っています。
爆発処理やダメージ処理は別コンポーネントに分けることで、きれいなコンポーネント指向な構成にできます。


HomingMissile.cs(フルコード)


using UnityEngine;

/// <summary>
/// ターゲットに向かって徐々に旋回しながら進む誘導弾コンポーネント。
/// 「移動」と「旋回」のみを担当し、ダメージやエフェクトは別コンポーネントに任せる想定。
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class HomingMissile : MonoBehaviour
{
    // --- 基本設定 ---

    [Header("移動設定")]
    [SerializeField]
    [Tooltip("ミサイルの前方向(transform.forward)に対する移動速度(m/s)")]
    private float moveSpeed = 10f;

    [SerializeField]
    [Tooltip("1秒あたりに回転できる最大角度(度)。大きいほど急カーブできる")]
    private float turnSpeedDegPerSec = 180f;

    [Header("ターゲット設定")]
    [SerializeField]
    [Tooltip("追尾するターゲット。プレハブ生成時に外部からセットするのがおすすめ")]
    private Transform target;

    [SerializeField]
    [Tooltip("ターゲットを見失ったと判定する最大距離。0以下なら無制限")]
    private float maxChaseDistance = 0f;

    [Header("寿命設定")]
    [SerializeField]
    [Tooltip("この秒数が経過したら自動で破棄する。0以下なら無効")]
    private float lifeTime = 10f;

    [Header("その他オプション")]
    [SerializeField]
    [Tooltip("ターゲットが消えた/範囲外になった後も、最後の向きのまま直進し続けるか")]
    private bool keepStraightWhenLost = true;

    [SerializeField]
    [Tooltip("Y軸を固定して水平面上だけで旋回させる(トップダウン・見下ろしゲーム向け)")]
    private bool lockRotationToY = false;

    // --- 内部状態 ---

    private Rigidbody rb;
    private float lifeTimer;
    private bool hasLostTarget;

    private void Awake()
    {
        // 必須コンポーネントを取得
        rb = GetComponent<Rigidbody>();

        // 物理挙動はスクリプトで制御したいので、慣性は無効化しておく
        rb.useGravity = false;
        rb.isKinematic = false;
    }

    private void OnEnable()
    {
        // 有効化されるたびに寿命タイマーをリセット
        lifeTimer = 0f;
        hasLostTarget = false;
    }

    private void FixedUpdate()
    {
        // 物理系の移動なので FixedUpdate で制御する
        UpdateLifeTime();
        UpdateRotation();
        UpdateMovement();
    }

    /// <summary>
    /// 寿命タイマーの更新と自動破棄
    /// </summary>
    private void UpdateLifeTime()
    {
        if (lifeTime <= 0f)
        {
            return;
        }

        lifeTimer += Time.fixedDeltaTime;
        if (lifeTimer >= lifeTime)
        {
            Destroy(gameObject);
        }
    }

    /// <summary>
    /// ターゲットの方向に向かって徐々に旋回する処理
    /// </summary>
    private void UpdateRotation()
    {
        // すでにターゲットを見失っていて、直進モードの場合は何もしない
        if (hasLostTarget && keepStraightWhenLost)
        {
            return;
        }

        // ターゲットがセットされていない場合は見失い扱い
        if (target == null)
        {
            hasLostTarget = true;
            return;
        }

        // 距離制限がある場合は、範囲外なら見失い扱い
        if (maxChaseDistance > 0f)
        {
            float sqrDistance = (target.position - transform.position).sqrMagnitude;
            if (sqrDistance > maxChaseDistance * maxChaseDistance)
            {
                hasLostTarget = true;
                return;
            }
        }

        // 現在位置からターゲットまでの方向ベクトル(正規化)
        Vector3 toTarget = (target.position - transform.position).normalized;

        // Y軸固定モードの場合は、垂直方向の差分を無視して水平面だけで追尾
        if (lockRotationToY)
        {
            toTarget.y = 0f;
            // 完全に0だと方向が定まらないのでチェック
            if (toTarget.sqrMagnitude < 0.0001f)
            {
                return;
            }
            toTarget.Normalize();
        }

        // 現在の forward から toTarget へ向けて、一定角速度で回転させる
        Quaternion currentRot = transform.rotation;
        Quaternion targetRot = Quaternion.LookRotation(toTarget, Vector3.up);

        // 1ステップで回転できる最大角度(ラジアンではなく度)を計算
        float maxStep = turnSpeedDegPerSec * Time.fixedDeltaTime;

        // Slerp ではなく RotateTowards を使うと「最大何度まで回せるか」を指定できる
        Quaternion newRot = Quaternion.RotateTowards(currentRot, targetRot, maxStep);

        transform.rotation = newRot;
    }

    /// <summary>
    /// 現在の forward 方向に進ませる処理
    /// </summary>
    private void UpdateMovement()
    {
        // Rigidbody の velocity を直接設定して前進させる
        Vector3 velocity = transform.forward * moveSpeed;
        rb.velocity = velocity;
    }

    /// <summary>
    /// 外部からターゲットをセットするための公開メソッド。
    /// プレハブ生成直後に呼び出す想定。
    /// </summary>
    /// <param name="newTarget">追尾したい Transform</param>
    public void SetTarget(Transform newTarget)
    {
        target = newTarget;
        hasLostTarget = false;
    }

    /// <summary>
    /// 外部から弾速を変更したい場合用。
    /// </summary>
    public void SetMoveSpeed(float newSpeed)
    {
        moveSpeed = Mathf.Max(0f, newSpeed);
    }

    /// <summary>
    /// 外部から旋回速度を変更したい場合用(度/秒)。
    /// </summary>
    public void SetTurnSpeed(float newTurnSpeedDegPerSec)
    {
        turnSpeedDegPerSec = Mathf.Max(0f, newTurnSpeedDegPerSec);
    }
}

使い方の手順

ここでは「プレイヤーを追いかける敵の誘導ミサイル」を例に、基本的な使い方を見ていきます。

  1. プレハブの準備
    • 空の GameObject を作成し、名前を HomingMissile に変更
    • 子オブジェクトとしてミサイルの見た目用 Mesh や Sprite を配置
    • 親オブジェクトに Rigidbody を追加(Use Gravity はオフ推奨)
    • 親オブジェクトに上記の HomingMissile スクリプトをアタッチ
    • 必要なら Collider も追加しておく(当たり判定用)
    • この GameObject を Project ビューにドラッグしてプレハブ化
  2. プレイヤー(ターゲット)の用意
    すでにプレイヤーオブジェクトがある場合は、それをターゲットとして使います。
    プレイヤーの Transform を、ミサイル生成時に SetTarget() で渡す形にすると、シーンごとに柔軟に差し替えられます。
  3. 敵からミサイルを発射するスクリプトを書く
    例として、敵が一定間隔でプレイヤーに向けて誘導ミサイルを撃つコンポーネントを作ってみます。
    
    using UnityEngine;
    
    public class EnemyMissileShooter : MonoBehaviour
    {
        [SerializeField]
        private HomingMissile missilePrefab;
    
        [SerializeField]
        private Transform firePoint; // 発射位置(敵の子オブジェクトなど)
    
        [SerializeField]
        private Transform player;    // プレイヤーの Transform
    
        [SerializeField]
        private float fireInterval = 2f;
    
        private float fireTimer;
    
        private void Update()
        {
            fireTimer += Time.deltaTime;
    
            if (fireTimer >= fireInterval)
            {
                fireTimer = 0f;
                FireMissile();
            }
        }
    
        private void FireMissile()
        {
            if (missilePrefab == null || firePoint == null || player == null)
            {
                return;
            }
    
            // ミサイルを生成
            HomingMissile missile =
                Instantiate(missilePrefab, firePoint.position, firePoint.rotation);
    
            // 追尾ターゲットをプレイヤーに設定
            missile.SetTarget(player);
    
            // 必要に応じて速度や旋回速度を調整
            // missile.SetMoveSpeed(15f);
            // missile.SetTurnSpeed(240f);
        }
    }
        

    このスクリプトを敵オブジェクトにアタッチし、

    • Missile Prefab に先ほど作ったミサイルプレハブ
    • Fire Point に発射位置の Transform(敵の子オブジェクトなど)
    • Player にプレイヤーの Transform

    をドラッグ&ドロップでセットすれば、一定間隔で誘導弾がプレイヤーを追いかけるようになります。

  4. 別の用途への使い回し
    • ボスが撃つ「ゆっくり曲がる巨大ミサイル」:
      moveSpeed を遅く、turnSpeedDegPerSec も小さめに設定。
    • プレイヤーのホーミングショット:
      プレイヤーの攻撃スクリプトから SetTarget(ロックオン中の敵) を呼び出すだけでOK。
    • 動く床に追従するエネルギーボール:
      床の Transform をターゲットにすれば、「床の前方を常に少し先回りする球体」なども実現できます。

メリットと応用

HomingMissile をコンポーネントとして切り出すメリットはかなり多いです。

  • プレハブ化して量産できる
    動き方がコンポーネントに閉じているので、「見た目だけ変えた別スキンの誘導弾」プレハブを簡単に増やせます。
    レベルデザイナーは moveSpeedturnSpeedDegPerSec をインスペクターから調整するだけで、
    「緩やかに曲がる弾」「超高機動の弾」などをノーコードで作り分けられます。
  • 責務がはっきりしていてバグを追いやすい
    このコンポーネントは「ターゲットを追いかける動き」だけを担当しているので、
    爆発しない、ダメージが入らない、といった問題は別コンポーネント側を疑えばよく、原因の切り分けがしやすくなります。
  • 他のシステムと組み合わせやすい
    ロックオンシステム、弾幕生成システム、オブジェクトプールなど、
    どんな仕組みから生成されても、SetTarget() さえ呼べば同じように誘導してくれるので、
    プロジェクト全体の構造がシンプルになります。

応用として、「ターゲットに十分近づいたら爆発する」処理を別コンポーネントに切り出すのもおすすめです。
例として、ミサイルに取り付ける簡単な自爆コンポーネントを示します。


using UnityEngine;

/// <summary>
/// 一定距離まで近づいたら自動で爆発(ここでは Destroy)するコンポーネント。
/// ダメージ処理やエフェクトは別途追加してください。
/// </summary>
public class ProximityDetonator : MonoBehaviour
{
    [SerializeField]
    private Transform target;

    [SerializeField]
    [Tooltip("この距離以内に入ったら爆発する")]
    private float explodeDistance = 1.5f;

    private void Update()
    {
        if (target == null)
        {
            return;
        }

        float sqrDist = (target.position - transform.position).sqrMagnitude;
        if (sqrDist <= explodeDistance * explodeDistance)
        {
            // 本来はここでエフェクト再生やダメージ処理を行う
            Destroy(gameObject);
        }
    }

    public void SetTarget(Transform newTarget)
    {
        target = newTarget;
    }
}

このように、「追尾するコンポーネント」と「爆発するコンポーネント」を分けておけば、
・追尾しかしないデコイ弾
・着弾時にだけ爆発する直進弾
なども簡単にバリエーション展開できます。
小さな責務ごとにコンポーネントを分けていくと、プロジェクトがかなり扱いやすくなるので、ぜひ意識してみてください。