Unityを触り始めた頃は、つい「とりあえず全部 Update に書いちゃえ!」となりがちですよね。
プレイヤー入力、弾の発射、エフェクト、スコア更新…全部を1つのスクリプト・1つの Update に押し込んでしまうと、少し仕様変更が入っただけでコード全体を読み直すハメになってしまいます。

特に「弾の発射ロジック」は、プレイヤー・敵・タレット・ギミックなど、いろいろなオブジェクトで再利用したくなる処理です。
にもかかわらず、巨大な PlayerController の中にベタ書きしてしまうと、他のオブジェクトで使い回すのが一気に面倒になります。

そこでこの記事では、「一度に複数の弾を、角度をずらして扇状に発射する」処理だけを切り出したコンポーネント
「ShotgunSpawner」 を作っていきます。
発射のタイミングは別コンポーネント(入力処理やAIなど)に任せて、このコンポーネントは「散弾をきれいに並べて撃つこと」だけに集中させましょう。

【Unity】扇状にバラまく散弾ショット!「ShotgunSpawner」コンポーネント

以下が、Unity6 (C#) で動作する「ShotgunSpawner」のフルコードです。
弾プレハブを指定し、発射数・扇の角度・初速などをインスペクターから調整できるようにしてあります。


using UnityEngine;

/// <summary>
/// Shotgun(散弾)風に、複数の弾を扇状に発射するコンポーネント。
/// 
/// ・弾プレハブを指定
/// ・発射数、扇の角度、初速、ランダムゆらぎなどを指定
/// ・外部からは Fire() を呼ぶだけでOK
/// 
/// 発射の「タイミング」は、別コンポーネント(入力、AI、タイマーなど)で管理し、
/// このクラスは「どう弾を並べて撃つか」だけに責務を絞っています。
/// </summary>
public class ShotgunSpawner : MonoBehaviour
{
    [Header("弾プレハブ設定")]
    [SerializeField] private GameObject bulletPrefab;
    
    [Tooltip("弾を生成する起点。未指定の場合はこのオブジェクトのTransformを使用")]
    [SerializeField] private Transform muzzleTransform;

    [Header("散弾パラメータ")]
    [Tooltip("一度に発射する弾の数")]
    [SerializeField] private int bulletCount = 5;

    [Tooltip("扇状に広がる角度(度数法)。例:60なら-30〜+30度に散開")]
    [SerializeField] private float spreadAngle = 60f;

    [Tooltip("弾をまっすぐ撃つ基準方向。通常は Transform の forward を使用")]
    [SerializeField] private Vector3 baseDirection = Vector3.forward;

    [Header("弾の速度設定")]
    [Tooltip("弾の初速度(単位/秒)。RigidbodyがあればVelocityに適用")]
    [SerializeField] private float bulletSpeed = 10f;

    [Tooltip("弾の寿命(秒)。0以下で無効")]
    [SerializeField] private float bulletLifeTime = 3f;

    [Header("ランダムゆらぎ")]
    [Tooltip("各弾にランダムで追加される角度(度数法)の最大値")]
    [SerializeField] private float randomAngleOffset = 0f;

    [Tooltip("各弾の速度にランダムで追加される割合(0.1で±10%)")]
    [SerializeField] private float randomSpeedRate = 0f;

    [Header("デバッグ")]
    [Tooltip("Gizmos で発射方向の可視化を行うか")]
    [SerializeField] private bool drawGizmos = true;

    [Tooltip("Gizmos の線の長さ")]
    [SerializeField] private float gizmoRayLength = 2f;

    /// <summary>
    /// 外部から呼び出す公開メソッド。
    /// Shotgun(散弾)を一度発射します。
    /// 
    /// 例:
    /// GetComponent<ShotgunSpawner>().Fire();
    /// </summary>
    public void Fire()
    {
        // 弾プレハブが設定されていなければ何もしない
        if (bulletPrefab == null)
        {
            Debug.LogWarning($"{nameof(ShotgunSpawner)}: bulletPrefab が設定されていません。", this);
            return;
        }

        // 発射位置のTransform。未指定なら自分自身を使う
        Transform origin = muzzleTransform != null ? muzzleTransform : transform;

        // 弾数が1以下の場合は、真っ直ぐに1発だけ撃つ
        if (bulletCount <= 1)
        {
            SpawnSingleBullet(origin, baseDirection.normalized);
            return;
        }

        // 扇状の中心を0度とし、左右に等間隔で角度を割り振る
        // 例:bulletCount=5, spreadAngle=60 の場合
        // stepAngle = 60 / (5 - 1) = 15
        // オフセット角度: -30, -15, 0, 15, 30
        float stepAngle = spreadAngle / (bulletCount - 1);
        float startAngle = -spreadAngle * 0.5f;

        // 基準方向を正規化しておく
        Vector3 baseDirNormalized = baseDirection.sqrMagnitude > 0.0001f 
            ? baseDirection.normalized 
            : origin.forward;

        for (int i = 0; i < bulletCount; i++)
        {
            // i番目の弾の角度(中心からのオフセット角)
            float angle = startAngle + stepAngle * i;

            // ランダムな角度オフセットを加える(-randomAngleOffset〜+randomAngleOffset)
            if (randomAngleOffset > 0f)
            {
                float random = Random.Range(-randomAngleOffset, randomAngleOffset);
                angle += random;
            }

            // Y軸を中心に回転させる(2Dでも3Dでも使いやすいように)
            Quaternion rotation = Quaternion.AngleAxis(angle, Vector3.up);
            Vector3 shotDirection = rotation * baseDirNormalized;

            SpawnSingleBullet(origin, shotDirection);
        }
    }

    /// <summary>
    /// 1発分の弾を生成して、指定方向へ飛ばす内部処理。
    /// </summary>
    /// <param name="origin">発射位置となるTransform</param>
    /// <param name="direction">発射方向(正規化されている前提)</param>
    private void SpawnSingleBullet(Transform origin, Vector3 direction)
    {
        // 弾を生成
        GameObject bullet = Instantiate(
            bulletPrefab,
            origin.position,
            Quaternion.LookRotation(direction, Vector3.up) // 向きをそろえる
        );

        // 速度のランダムゆらぎを計算(例:0.1で±10%)
        float speedMultiplier = 1f;
        if (randomSpeedRate > 0f)
        {
            float random = Random.Range(-randomSpeedRate, randomSpeedRate);
            speedMultiplier += random;
        }

        float finalSpeed = bulletSpeed * speedMultiplier;

        // Rigidbody が付いていれば、velocity で初速を与える
        Rigidbody rb = bullet.GetComponent<Rigidbody>();
        if (rb != null)
        {
            rb.velocity = direction * finalSpeed;
        }
        else
        {
            // Rigidbody が無い場合は、簡易的に Transform.Translate で移動させるための
            // BulletMover コンポーネントを自動付与してもよい
            BulletMover mover = bullet.GetComponent<BulletMover>();
            if (mover == null)
            {
                mover = bullet.AddComponent<BulletMover>();
            }
            mover.Initialize(direction, finalSpeed);
        }

        // 寿命が設定されていれば、一定時間後に自動で破棄
        if (bulletLifeTime > 0f)
        {
            Destroy(bullet, bulletLifeTime);
        }
    }

    /// <summary>
    /// エディタ上で発射方向のイメージを可視化するための Gizmos 描画。
    /// </summary>
    private void OnDrawGizmosSelected()
    {
        if (!drawGizmos)
        {
            return;
        }

        Transform origin = muzzleTransform != null ? muzzleTransform : transform;
        Vector3 baseDirNormalized = baseDirection.sqrMagnitude > 0.0001f
            ? baseDirection.normalized
            : origin.forward;

        Gizmos.color = Color.yellow;

        // 弾数が1以下の場合は、真っ直ぐ1本だけ描画
        if (bulletCount <= 1)
        {
            Gizmos.DrawRay(origin.position, baseDirNormalized * gizmoRayLength);
            return;
        }

        float stepAngle = spreadAngle / (bulletCount - 1);
        float startAngle = -spreadAngle * 0.5f;

        for (int i = 0; i < bulletCount; i++)
        {
            float angle = startAngle + stepAngle * i;
            Quaternion rotation = Quaternion.AngleAxis(angle, Vector3.up);
            Vector3 dir = rotation * baseDirNormalized;

            Gizmos.DrawRay(origin.position, dir * gizmoRayLength);
        }
    }
}

/// <summary>
/// Rigidbody が無い弾用の、超シンプルな直進移動コンポーネント。
/// ShotgunSpawner から自動的にアタッチされることを想定。
/// </summary>
public class BulletMover : MonoBehaviour
{
    private Vector3 moveDirection;
    private float moveSpeed;

    /// <summary>
    /// 初期化用メソッド。ShotgunSpawner から呼び出される。
    /// </summary>
    public void Initialize(Vector3 direction, float speed)
    {
        // 念のため正規化
        moveDirection = direction.normalized;
        moveSpeed = speed;
    }

    private void Update()
    {
        // 毎フレーム、一定速度で前進
        transform.position += moveDirection * moveSpeed * Time.deltaTime;
    }
}

使い方の手順

ここからは、実際にシーンに組み込む手順を見ていきましょう。
例として「プレイヤーのショットガン攻撃」「敵の扇状弾」「動く砲台ギミック」の3パターンをイメージして解説します。

手順① 弾プレハブを用意する

  1. シーン上に小さな Sphere や Quad を配置して「Bullet」オブジェクトを作成します。
  2. 必要であれば、Rigidbody を追加します(物理挙動を使いたい場合)。
    ・2Dゲームなら Rigidbody2D ではなく、この記事のコードでは Rigidbody を想定しているので3D物理ベースにするか、
    ・2D用に改造する場合は後述の応用を参考にしてください。
  3. マテリアルやスプライトを設定して見た目を整えます。
  4. Hierarchy から Project ウィンドウへドラッグ&ドロップして、プレハブ化します(例:Bullet.prefab)。
  5. 元のシーン上の Bullet オブジェクトは削除してOKです。

手順② ShotgunSpawner コンポーネントをアタッチ

  1. プレイヤー、敵、砲台など「弾を撃つオブジェクト」をシーンに配置します。
  2. そのオブジェクトに ShotgunSpawner コンポーネントを追加します。
  3. インスペクターで以下を設定します:
    • Bullet Prefab : 先ほど作成した Bullet.prefab をドラッグ&ドロップ
    • Muzzle Transform : 弾の発射位置となる子オブジェクト(例:銃口)を指定。未指定なら本体の位置から発射
    • Bullet Count : 一度に発射する弾数(例:5, 7, 9など)
    • Spread Angle : 扇の角度(例:60〜120)。値を大きくすると広がりが大きくなります。
    • Base Direction : 通常は (0,0,1) のまま(= Transform.forward)。
      オブジェクトの forward が進行方向を向いている前提であれば、そのままでOKです。
    • Bullet Speed : 弾の初速(例:10〜30)
    • Bullet Life Time : 弾の寿命(例:2〜5秒)
    • Random Angle Offset : 少しバラけさせたい場合は 2〜5 度程度
    • Random Speed Rate : 0.1 で±10%の速度ゆらぎ

手順③ 発射のトリガーを作る(プレイヤー入力例)

ShotgunSpawner 自体は「いつ撃つか」を決めません。
「撃つべきタイミングで Fire() を呼ぶ」役割のコンポーネントを別に用意すると、責務が分かれてスッキリします。

例えば、簡易的なプレイヤー入力での発射トリガーはこんな感じです:


using UnityEngine;

/// <summary>
/// 非推奨な Godクラス化を避けるため、
/// 「入力を見て ShotgunSpawner.Fire() を呼ぶだけ」の薄いコンポーネント。
/// </summary>
[RequireComponent(typeof(ShotgunSpawner))]
public class PlayerShotgunInput : MonoBehaviour
{
    [SerializeField] private KeyCode fireKey = KeyCode.Space;

    private ShotgunSpawner shotgun;

    private void Awake()
    {
        shotgun = GetComponent<ShotgunSpawner>();
    }

    private void Update()
    {
        // スペースキーが押されたら散弾発射
        if (Input.GetKeyDown(fireKey))
        {
            shotgun.Fire();
        }
    }
}

このように、「入力を見るクラス」と「散弾を撃つクラス」を分けておくと、
後から「敵AIのタイミングで撃ちたい」「一定間隔で自動発射する砲台にしたい」といった変更にも柔軟に対応できます。

手順④ 敵や動く砲台への応用

  • 敵キャラの扇状ショット
    敵オブジェクトに ShotgunSpawner を付け、別コンポーネントで「プレイヤーを向く」「一定間隔で Fire() を呼ぶ」処理を書きます。
    敵ごとに Bullet Count や Spread Angle を変えれば、簡単にバリエーション豊かな弾幕を作れます。
  • 動く砲台ギミック
    回転する砲台オブジェクトに ShotgunSpawner を付けておき、回転アニメーションに合わせて Fire() を呼ぶだけで、
    ステージギミックとしての「扇状ショット砲台」が完成します。

メリットと応用

ShotgunSpawner をコンポーネントとして切り出すメリットを整理してみましょう。

  • プレハブの再利用性アップ
    プレイヤー用・敵用・ギミック用など、複数のオブジェクトに同じ散弾ロジックを簡単に使い回せます。
    「散弾の仕様変更(角度の計算方法など)」があっても、ShotgunSpawner だけ直せば全てに反映されます。
  • レベルデザインが楽になる
    Bullet Count と Spread Angle をインスペクターからいじるだけで、
    「狭いショット」「広いショット」「超密集ショット」などをすぐに試せます。
    プレハブ化しておけば、シーンにポンポン置くだけで様々なパターンの弾幕ギミックを配置できます。
  • Godクラスを避けられる
    「プレイヤー制御」「入力」「発射ロジック」「弾の挙動」をそれぞれ別コンポーネントに分けることで、
    各クラスがシンプルになり、デバッグもしやすくなります。

さらに、少し手を加えるだけで色々な応用が可能です。

  • 2Dゲーム向けに Z軸ではなく X-Y 平面で角度を回転させる
  • 弾ごとに色やサイズを変える(中央だけ太い弾にするなど)
  • 最初は狭い角度で、レベルが上がるごとに Spread Angle を広げていく

例えば、「中央の弾だけ少し大きくして強そうに見せる」改造はこんな感じで追加できます:


/// <summary>
/// 中央の弾だけスケールを大きくする改造例。
/// ShotgunSpawner.SpawnSingleBullet() 内で呼び出すイメージです。
/// </summary>
private void ScaleCenterBullet(GameObject bullet, int index, int totalCount)
{
    // 弾数が奇数で、かつ中央の弾であればスケールアップ
    if (totalCount % 2 == 1)
    {
        int centerIndex = totalCount / 2;
        if (index == centerIndex)
        {
            bullet.transform.localScale *= 1.5f;
        }
    }
}

このように、「ShotgunSpawner」はあくまで「散弾の骨格」として用意しておき、
ゲーム固有の演出やルールは小さな改造メソッドとして別に追加していくと、
プロジェクト全体がかなり見通しの良い構成になります。