Unityの学習を始めたばかりだと、つい何でもかんでも Update() に書いてしまいがちですよね。
「入力のチェック」「弾の発射」「クールダウンの管理」「エフェクト再生」「サウンド再生」…全部1つのスクリプトに押し込んでしまうと、すぐに数百行のGodクラスが出来上がってしまいます。
そうなると、
- ちょっとした仕様変更でも他の処理に影響が出やすい
- プレイヤーと敵で微妙に違う弾発射ロジックを使い回しづらい
- プレハブ化しづらく、レベルデザインのたびにコードをいじる羽目になる
といった問題が出てきます。
そこで今回は「弾を撃つ」という機能だけに責務を絞ったコンポーネント
ProjectileShooter(弾発射) を作って、
- 入力で撃つプレイヤー
- 一定間隔で撃つ敵タレット
- 仕掛けとして定期的に弾を飛ばすトラップ
などに簡単に使い回せる形にしていきましょう。
【Unity】入力でも自動でも撃てる!「ProjectileShooter」コンポーネント
このコンポーネントは、
- 親オブジェクトの位置・向きから弾プレハブをインスタンス化
- 入力(ボタン)またはタイマー(自動射撃)で発射トリガー
- 弾の初速や発射レートをインスペクターから調整可能
という役割だけに責務を限定しています。
弾そのものの挙動(移動・ダメージ処理など)は別コンポーネントに任せる前提で、「撃つこと」に集中させます。
フルコード:ProjectileShooter.cs
using UnityEngine;
using UnityEngine.InputSystem; // 新Input Systemを使う場合
/// <summary>弾を前方に発射するコンポーネント</summary>
public class ProjectileShooter : MonoBehaviour
{
// --- 射撃モード設定 ---
/// <summary>射撃のトリガー方法</summary>
private enum FireMode
{
Input, // 入力で撃つ
Timer, // 一定間隔で自動で撃つ
Both // 入力でもタイマーでも撃つ
}
[Header("射撃モード")]
[SerializeField] private FireMode fireMode = FireMode.Input;
// --- 弾プレハブと発射設定 ---
[Header("弾プレハブ")]
[Tooltip("発射する弾のプレハブ。Rigidbody 付き推奨")]
[SerializeField] private GameObject projectilePrefab;
[Header("発射位置・向き")]
[Tooltip("弾を生成する位置。未指定ならこのオブジェクトのTransformを使用")]
[SerializeField] private Transform muzzleTransform;
[Tooltip("弾の初速(m/s)。Rigidbody が付いている場合は AddForce で前方へ加速")]
[SerializeField] private float initialSpeed = 20f;
[Tooltip("弾の生成時に追加するランダムな角度ブレ(度)。0 でブレなし")]
[SerializeField] private float randomSpreadAngle = 0f;
// --- 入力関連(Input System)---
[Header("入力設定(FireMode が Input / Both のとき有効)")]
[Tooltip("Input System の Action を使う場合に指定(例: PlayerInput から参照)")]
[SerializeField] private InputActionReference fireActionReference;
[Tooltip("旧 Input Manager を使う場合のボタン名(Input.GetButtonDown)。空なら無効")]
[SerializeField] private string legacyFireButtonName = "Fire1";
// --- タイマー射撃関連 ---
[Header("タイマー射撃設定(FireMode が Timer / Both のとき有効)")]
[Tooltip("自動射撃の間隔(秒)")]
[SerializeField] private float fireInterval = 1.0f;
[Tooltip("開始から何秒後に自動射撃を開始するか")]
[SerializeField] private float autoFireStartDelay = 0f;
// --- 共通クールダウン ---
[Header("クールダウン")]
[Tooltip("最小射撃間隔(秒)。0 なら連射制限なし")]
[SerializeField] private float fireCooldown = 0.1f;
[Tooltip("true の場合、クールダウン中は入力を無視する")]
[SerializeField] private bool ignoreInputDuringCooldown = true;
// --- サウンド / エフェクト(任意)---
[Header("演出(任意)")]
[Tooltip("発射時に再生するサウンド")]
[SerializeField] private AudioSource fireAudioSource;
[Tooltip("発射時に生成するマズルフラッシュなどのエフェクト")]
[SerializeField] private GameObject muzzleEffectPrefab;
[Tooltip("マズルエフェクトの寿命(秒)。0 以下なら Destroy しない")]
[SerializeField] private float muzzleEffectLifetime = 1.0f;
// --- 内部状態 ---
private float _timeSinceLastShot = 0f;
private float _autoFireTimer = 0f;
private bool _autoFireEnabled = false;
private void Awake()
{
// muzzleTransform が未設定なら自分自身を使う
if (muzzleTransform == null)
{
muzzleTransform = transform;
}
}
private void OnEnable()
{
// InputAction を有効化
if (fireActionReference != null && fireActionReference.action != null)
{
fireActionReference.action.Enable();
}
// タイマー射撃の初期化
_autoFireEnabled = (fireMode == FireMode.Timer || fireMode == FireMode.Both);
_autoFireTimer = -autoFireStartDelay; // マイナスからスタートして、0 を超えたら発射開始
_timeSinceLastShot = fireCooldown; // 起動直後から撃てるようにしておく
}
private void OnDisable()
{
// InputAction を無効化
if (fireActionReference != null && fireActionReference.action != null)
{
fireActionReference.action.Disable();
}
}
private void Update()
{
float deltaTime = Time.deltaTime;
_timeSinceLastShot += deltaTime;
HandleInputFire();
HandleTimerFire(deltaTime);
}
/// <summary>
/// 入力による射撃処理
/// </summary>
private void HandleInputFire()
{
if (fireMode != FireMode.Input && fireMode != FireMode.Both)
{
return;
}
bool firePressed = false;
// 新 Input System
if (fireActionReference != null && fireActionReference.action != null)
{
// ボタンが押された瞬間を検出
if (fireActionReference.action.triggered)
{
firePressed = true;
}
}
// 旧 Input Manager
else if (!string.IsNullOrEmpty(legacyFireButtonName))
{
if (Input.GetButtonDown(legacyFireButtonName))
{
firePressed = true;
}
}
if (!firePressed)
{
return;
}
// クールダウンチェック
if (!CanFire())
{
if (ignoreInputDuringCooldown)
{
return;
}
// クールダウン中でも最後の入力を受け付けたい場合は、
// ここでキューイング処理などを追加してもよい
}
Fire();
}
/// <summary>
/// タイマーによる自動射撃処理
/// </summary>
private void HandleTimerFire(float deltaTime)
{
if (!_autoFireEnabled)
{
return;
}
_autoFireTimer += deltaTime;
// autoFireStartDelay が経過するまで待つ
if (_autoFireTimer < 0f)
{
return;
}
// fireInterval ごとに発射
if (_autoFireTimer >= fireInterval)
{
// 間隔を保つために余剰時間を引く
_autoFireTimer -= fireInterval;
if (CanFire())
{
Fire();
}
}
}
/// <summary>
/// 今撃てる状態かどうか(クールダウンのみ判定)
/// </summary>
private bool CanFire()
{
if (fireCooldown <= 0f)
{
return true;
}
return _timeSinceLastShot >= fireCooldown;
}
/// <summary>
/// 実際に弾を生成して飛ばす処理
/// </summary>
private void Fire()
{
if (projectilePrefab == null)
{
Debug.LogWarning($"[ProjectileShooter] projectilePrefab が設定されていません: {name}");
return;
}
_timeSinceLastShot = 0f;
// 発射位置と向き
Vector3 spawnPosition = muzzleTransform.position;
Quaternion spawnRotation = muzzleTransform.rotation;
// ランダムなブレを追加(必要な場合)
if (randomSpreadAngle > 0f)
{
float yaw = Random.Range(-randomSpreadAngle, randomSpreadAngle);
float pitch = Random.Range(-randomSpreadAngle, randomSpreadAngle);
Quaternion spread = Quaternion.Euler(pitch, yaw, 0f);
spawnRotation *= spread;
}
// 弾プレハブをインスタンス化
GameObject projectileInstance = Instantiate(projectilePrefab, spawnPosition, spawnRotation);
// Rigidbody が付いていれば前方へ力を加える
Rigidbody rb = projectileInstance.GetComponent<Rigidbody>();
if (rb != null && initialSpeed > 0f)
{
rb.velocity = Vector3.zero; // 念のため初期化
rb.angularVelocity = Vector3.zero;
// forward 方向に速度を与える
rb.AddForce(muzzleTransform.forward * initialSpeed, ForceMode.VelocityChange);
}
// 演出:サウンド再生
if (fireAudioSource != null)
{
fireAudioSource.Play();
}
// 演出:マズルエフェクト
if (muzzleEffectPrefab != null)
{
GameObject effect = Instantiate(muzzleEffectPrefab, spawnPosition, spawnRotation);
if (muzzleEffectLifetime > 0f)
{
Destroy(effect, muzzleEffectLifetime);
}
}
}
// --- 外部から明示的に撃たせたい場合用の公開メソッド ---
/// <summary>
/// スクリプトから明示的に発射させたいときに使用(クールダウンは適用される)
/// </summary>
public void TryFire()
{
if (CanFire())
{
Fire();
}
}
/// <summary>
/// クールダウンを無視して強制的に発射させる(特殊演出用)
/// </summary>
public void ForceFire()
{
Fire();
}
}
使い方の手順
ここでは代表的な3パターンを例に、使い方を手順で見ていきます。
① プレイヤーがクリック/ボタンで弾を撃つ
- シーン内のプレイヤーオブジェクト(例:
Player)を選択。 ProjectileShooterコンポーネントを追加。-
弾プレハブを用意して
projectilePrefabに割り当てます。
例:- 3Dの場合: 球体に
Rigidbodyを付けて「Bullet」プレハブ化 - 2Dの場合: スプライトに
Rigidbody2Dを付けたプレハブを使い、
このコンポーネント側のRigidbody取得部分をRigidbody2D用に改造してもOK
- 3Dの場合: 球体に
-
fireModeを Input に設定。- 新 Input System を使っている場合:
InputActionAssetから「Shoot」「Fire」などのアクションを作成PlayerInputコンポーネント経由でInputActionReferenceを作り、
それをfireActionReferenceにドラッグ&ドロップ
- 旧 Input Manager を使う場合:
legacyFireButtonNameにFire1などのボタン名を入力
- 新 Input System を使っている場合:
-
発射位置を銃口にしたい場合、プレイヤーの子オブジェクトに「Muzzle」などの空オブジェクトを作り、
それをmuzzleTransformに割り当てます。 -
initialSpeedやfireCooldownを好みに合わせて調整。
再生してボタン(またはクリック)で弾が飛べばOKです。
② 敵タレットが一定間隔で自動射撃する
- シーンにタレット用のオブジェクトを配置(例:
EnemyTurret)。 ProjectileShooterコンポーネントを追加。- タレットが撃ちたい弾プレハブを
projectilePrefabに設定。 fireModeを Timer に設定。-
fireIntervalを 0.5〜2.0 秒あたりに設定して連射ペースを調整。
例えば 1.5 にすると、1.5秒ごとに弾を1発撃ちます。 -
ゲーム開始からしばらくしてから撃たせたい場合は
autoFireStartDelayに数秒入れておきましょう。 -
タレットの砲身の先端に「Muzzle」オブジェクトを置き、
muzzleTransformに設定しておくと、
見た目通りの位置から弾が出ます。
③ 仕掛けトラップ:壁から定期的に弾が飛び出す
- 壁やトラップ用のオブジェクト(例:
SpikeShooter)をシーンに配置。 ProjectileShooterコンポーネントを追加。- トゲ弾や炎の玉などのプレハブを
projectilePrefabに設定。 fireModeを Timer か Both に設定。- Timer: 常に一定間隔で飛び出すトラップ
- Both: タイマーに加えて、スイッチなどからスクリプトで
TryFire()を呼び出すことも可能
-
トラップの向き(
transform.forward)を調整して、弾が正しい方向に飛ぶようにします。
必要に応じてrandomSpreadAngleを少しだけ(例: 5〜10度)付けると、
ばらけた感じが出て自然になります。
メリットと応用
ProjectileShooter を「弾を撃つだけ」のコンポーネントとして分離しておくと、
- プレハブ化しやすい:プレイヤー・敵・ギミックなど、どこにでも貼るだけで弾発射機能を付与できる
- レベルデザインが楽:シーン上で
fireIntervalやautoFireStartDelayを調整するだけで
難易度やリズムを調整できる - 責務が明確:入力処理・AI・移動・ダメージ処理などと分離されているので、
バグの切り分けや仕様変更がしやすい - 拡張しやすい:弾の種類を変えたいときはプレハブを差し替えるだけで済む
例えば、敵AIコンポーネントは「いつ撃つか」だけを決めて、実際の発射は ProjectileShooter の TryFire() を呼ぶだけ、という構成にしておくと、
AIのロジックと発射ロジックがきれいに分離できます。
改造案:弾の発射方向をターゲットに向ける
標準の実装では muzzleTransform.forward 方向に弾を飛ばしていますが、
敵がプレイヤーを追いかけるようなタレットにしたい場合、以下のような関数を追加してターゲット方向に撃てるようにしても便利です。
/// <summary>
/// 指定したターゲットに向かって発射する(クールダウンあり)
/// </summary>
public void TryFireAtTarget(Transform target)
{
if (target == null)
{
return;
}
if (!CanFire())
{
return;
}
// 一時的に muzzleTransform をターゲット方向に向ける
Vector3 originalForward = muzzleTransform.forward;
Vector3 direction = (target.position - muzzleTransform.position).normalized;
muzzleTransform.forward = direction;
Fire();
// 向きを元に戻す
muzzleTransform.forward = originalForward;
}
このように、小さな責務ごとにコンポーネントを分けておくと、
「撃ち方を変える」「狙い方を変える」といった改造も、シンプルな関数追加だけで済ませやすくなります。
ぜひ自分のプロジェクト用にカスタマイズしてみてください。
