Unityを触り始めた頃は、つい「とりあえず Update に全部書くか…」となりがちですよね。
移動処理、入力処理、弾の発射、UIの更新…すべてを1つのスクリプトの Update に突っ込んでしまうと、次のような問題が起きます。
- 機能追加のたびに1ファイルがどんどん巨大化して、スクロール地獄になる
- 「どこがバグってるのか」が追いにくく、デバッグがつらい
- 別のキャラやオブジェクトで同じ処理を再利用しづらい
そこで今回は、「溜め撃ち」の機能だけに責務を絞ったコンポーネント
「ChargeShot」 を作ってみましょう。
ボタンの長押し時間を計測し、ボタンを離した瞬間に、その時間に応じて
威力やサイズが変化する弾 を発射する仕組みです。
プレイヤーキャラにアタッチするだけで「溜め撃ち」が生えるような、小さくて再利用しやすいコンポーネントを目指します。
【Unity】長押しで威力アップする溜め撃ち!「ChargeShot」コンポーネント
フルコード(ChargeShot.cs)
using UnityEngine;
using UnityEngine.InputSystem; // 新Input System用
/// <summary>溜め撃ちの弾の挙動(移動とダメージ値保持)</summary>
[RequireComponent(typeof(Rigidbody))]
public class ChargeBullet : MonoBehaviour
{
[SerializeField] private float speed = 10f; // 弾の移動速度
[SerializeField] private float lifeTime = 5f; // 何秒後に自動で消えるか
private Rigidbody _rb;
/// <summary>外部から参照したい攻撃力(ChargeShot側で設定)</summary>
public float Power { get; private set; }
private void Awake()
{
_rb = GetComponent<Rigidbody>();
}
/// <summary>
/// 弾の初期設定を行う(方向と威力を渡す)
/// </summary>
public void Initialize(Vector3 direction, float power)
{
Power = power;
// 指定方向へ速度を与える
_rb.velocity = direction.normalized * speed;
// 一定時間後に自動破棄
Destroy(gameObject, lifeTime);
}
// ここでは当たり判定の実装は省略。
// 実際のゲームでは OnCollisionEnter などで敵にダメージを与える処理を追加しましょう。
}
/// <summary>
/// 溜め撃ちを管理するコンポーネント。
/// 入力の長押し時間を計測し、ボタンを離したときに溜め時間に応じた弾を発射する。
/// </summary>
public class ChargeShot : MonoBehaviour
{
[Header("Input 設定")]
[SerializeField]
private string fireActionName = "Fire";
// Input Actions の Action 名(PlayerInput の Action Map 内の名前)
[Header("チャージ設定")]
[SerializeField] private float minChargeTime = 0.1f; // この時間未満は「ショット無し」にしたい場合
[SerializeField] private float maxChargeTime = 2.0f; // これ以上はチャージ時間を伸ばさない
[SerializeField] private float minPower = 10f; // 最小威力
[SerializeField] private float maxPower = 50f; // 最大威力
[SerializeField] private float minScale = 0.5f; // 弾の最小スケール
[SerializeField] private float maxScale = 2.0f; // 弾の最大スケール
[Header("発射設定")]
[SerializeField] private ChargeBullet bulletPrefab; // 発射する弾のプレハブ
[SerializeField] private Transform muzzleTransform; // 発射位置(銃口など)
[SerializeField] private Vector3 shotDirection = Vector3.forward; // 発射方向(ローカル空間)
[Header("デバッグ表示")]
[SerializeField] private bool showDebugLog = false;
private PlayerInput _playerInput;
private bool _isCharging = false;
private float _chargeStartTime = 0f;
private void Awake()
{
// PlayerInput は同じオブジェクトに付けておく想定
_playerInput = GetComponent<PlayerInput>();
if (_playerInput == null)
{
Debug.LogWarning(
"ChargeShot: 同じ GameObject に PlayerInput が見つかりません。" +
"新Input Systemを使う場合は PlayerInput を追加し、Action 名を設定してください。"
);
}
}
private void OnEnable()
{
// PlayerInput がある場合はイベントに登録(Action 名で検索)
if (_playerInput != null && !string.IsNullOrEmpty(fireActionName))
{
var action = _playerInput.actions[fireActionName];
if (action != null)
{
action.started += OnFireStarted;
action.canceled += OnFireCanceled;
}
}
}
private void OnDisable()
{
if (_playerInput != null && !string.IsNullOrEmpty(fireActionName))
{
var action = _playerInput.actions[fireActionName];
if (action != null)
{
action.started -= OnFireStarted;
action.canceled -= OnFireCanceled;
}
}
}
/// <summary>
/// 発射ボタンが押され始めた時に呼ばれる
/// </summary>
private void OnFireStarted(InputAction.CallbackContext context)
{
_isCharging = true;
_chargeStartTime = Time.time;
if (showDebugLog)
{
Debug.Log("ChargeShot: チャージ開始");
}
}
/// <summary>
/// 発射ボタンが離された時に呼ばれる
/// </summary>
private void OnFireCanceled(InputAction.CallbackContext context)
{
if (!_isCharging)
{
return;
}
_isCharging = false;
float chargeDuration = Time.time - _chargeStartTime;
// 最小チャージ時間未満ならキャンセル(「暴発防止」用)
if (chargeDuration < minChargeTime)
{
if (showDebugLog)
{
Debug.Log($"ChargeShot: チャージ時間が短すぎたため発射しません ({chargeDuration:F2}秒)");
}
return;
}
// 0.0 ~ 1.0 に正規化したチャージ率
float t = Mathf.Clamp01(chargeDuration / maxChargeTime);
// チャージ率に応じて威力とスケールを補間
float power = Mathf.Lerp(minPower, maxPower, t);
float scale = Mathf.Lerp(minScale, maxScale, t);
FireChargedShot(power, scale);
if (showDebugLog)
{
Debug.Log($"ChargeShot: 発射! チャージ時間={chargeDuration:F2}秒, 威力={power:F1}, スケール={scale:F2}");
}
}
/// <summary>
/// 実際に弾プレハブを生成して、威力とサイズを設定して発射する
/// </summary>
private void FireChargedShot(float power, float scale)
{
if (bulletPrefab == null)
{
Debug.LogWarning("ChargeShot: bulletPrefab が設定されていません。");
return;
}
// 発射位置の決定(指定がなければ自分の位置)
Transform spawnPoint = muzzleTransform != null ? muzzleTransform : transform;
// 弾を生成
ChargeBullet bulletInstance = Instantiate(
bulletPrefab,
spawnPoint.position,
spawnPoint.rotation
);
// スケールを設定
bulletInstance.transform.localScale = Vector3.one * scale;
// 発射方向(ローカル→ワールド変換)
Vector3 worldDirection = spawnPoint.TransformDirection(shotDirection.normalized);
// 弾に初期設定を渡す
bulletInstance.Initialize(worldDirection, power);
}
// もし旧Input Manager を使いたい場合は、Update で Input.GetButton 系を読む別メソッドを用意してもOK。
// その場合も「チャージの開始/終了」をこのクラス内に閉じ込めると、責務がはっきりして保守しやすくなります。
}
使い方の手順
-
弾プレハブ(ChargeBullet)を作る
- 空の GameObject を作成し、名前を
ChargeBulletにする。 Rigidbodyコンポーネントを追加し、Use Gravityをオフ(弾をまっすぐ飛ばしたい場合)。SphereやCylinderなどの Mesh を子オブジェクトとして付けて、見た目を作る。- 上記コードの
ChargeBulletコンポーネントを追加する。 - この GameObject を Project ビューにドラッグしてプレハブ化する。
- 空の GameObject を作成し、名前を
-
プレイヤーオブジェクトに ChargeShot を付ける
例として、3Dアクションのプレイヤーキャラに溜め撃ちを付けたい場合:- プレイヤーの GameObject(例:
Player)を選択。 PlayerInputコンポーネントを追加し、Input Actions アセットを設定。- Input Actions 内に「Fire」などの Action を作成し、Button タイプでマウス左ボタンやゲームパッドのボタンにバインドする。
- 同じオブジェクトに
ChargeShotコンポーネントを追加。 Bullet Prefabに先ほど作ったChargeBulletプレハブをドラッグ&ドロップ。- 銃口になる子オブジェクト(例:
Muzzle)を作り、その Transform をMuzzle Transformに指定。 - 発射方向
Shot Directionはデフォルトの(0,0,1)(Z+)のままでOK(Muzzle の forward 方向に飛びます)。
- プレイヤーの GameObject(例:
-
敵や動く床にも簡単に応用できる
- 敵のチャージ攻撃:敵キャラに
ChargeShotを付けて、AIスクリプトから
「一定時間ごとに OnFireStarted → OnFireCanceled を模倣」することで、ため攻撃を実装できます。
プレイヤー入力ではなく、スクリプト制御でチャージ時間を決める形ですね。 - 動く砲台(タレット):動く床や壁に砲台モデルを載せ、その砲台に
ChargeShotを付ければ、
ステージギミックとして定期的に溜め撃ちを撃つトラップも作れます。
- 敵のチャージ攻撃:敵キャラに
-
パラメータを調整してゲーム性を作る
Max Charge Timeを短め(例: 1秒)にするとテンポの良いアクションに。Min Charge Timeを0に近づけると、タップでも弱弾が出て、長押しで強弾になる操作感に。Min/Max PowerとMin/Max Scaleをいじって、見た目とダメージのバランスを取っていきましょう。
メリットと応用
ChargeShot コンポーネントを分離しておくことで、次のようなメリットがあります。
- プレハブ化しやすい:プレイヤー用、敵用、ボス用など、同じ溜め撃ちロジックを持つオブジェクトを
プレハブとして量産できます。調整したいのはパラメータだけなので、コードをコピペする必要がありません。 - レベルデザインが楽:ステージ中のギミック砲台やトラップに、そのまま
ChargeShotを付けて
弾プレハブと向きを変えるだけで「溜めて撃つギミック」が作れます。レベルデザイナーは Inspector で数値を調整するだけ。 - 責務が明確:入力の長押し時間を計測し、チャージ量に応じて弾を生成する、という責務だけに絞っているので、
移動やHP管理などと混ざらず、コードの見通しが良くなります。
さらに、溜め中の演出(エフェクトやサウンド)を追加すると、気持ちよさが一気に上がります。
例えば、チャージ中はプレイヤーの手元を光らせたり、チャージ量に応じてピッチが変わるSEを鳴らしたり、といった拡張が考えられます。
最後に、改造案として「現在のチャージ率を返す」ヘルパー関数を追加する例を載せておきます。
これを使えば、UIゲージやエフェクト側からチャージ量を簡単に参照できます。
/// <summary>
/// 現在のチャージ率(0.0 ~ 1.0)を取得する。
/// 溜め中でない場合は 0 を返す。
/// </summary>
public float GetCurrentChargeRatio()
{
if (!_isCharging)
{
return 0f;
}
float chargeDuration = Time.time - _chargeStartTime;
return Mathf.Clamp01(chargeDuration / maxChargeTime);
}
この関数を UI ゲージ更新用のコンポーネントから呼び出して、
Image.fillAmount = chargeShot.GetCurrentChargeRatio(); のように使えば、
視覚的に「今どれくらい貯まっているか」をプレイヤーに伝えられます。
溜め撃ちロジックを1つのコンポーネントに閉じ込めておくと、
演出や入力方法を変えたくなったときも、ここを中心に改造するだけで済むのでおすすめです。
