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パターンをイメージして解説します。
手順① 弾プレハブを用意する
- シーン上に小さな Sphere や Quad を配置して「Bullet」オブジェクトを作成します。
- 必要であれば、
Rigidbodyを追加します(物理挙動を使いたい場合)。
・2DゲームならRigidbody2Dではなく、この記事のコードではRigidbodyを想定しているので3D物理ベースにするか、
・2D用に改造する場合は後述の応用を参考にしてください。 - マテリアルやスプライトを設定して見た目を整えます。
- Hierarchy から Project ウィンドウへドラッグ&ドロップして、プレハブ化します(例:
Bullet.prefab)。 - 元のシーン上の Bullet オブジェクトは削除してOKです。
手順② ShotgunSpawner コンポーネントをアタッチ
- プレイヤー、敵、砲台など「弾を撃つオブジェクト」をシーンに配置します。
- そのオブジェクトに
ShotgunSpawnerコンポーネントを追加します。 - インスペクターで以下を設定します:
- 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%の速度ゆらぎ
- Bullet Prefab : 先ほど作成した
手順③ 発射のトリガーを作る(プレイヤー入力例)
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」はあくまで「散弾の骨格」として用意しておき、
ゲーム固有の演出やルールは小さな改造メソッドとして別に追加していくと、
プロジェクト全体がかなり見通しの良い構成になります。
