Unityでアクションゲームやコンボ制のバトルを作り始めると、つい Update() に「入力判定」「ダメージ処理」「コンボ計測」「UI更新」など、全部を押し込んでしまいがちですよね。動きはするものの、数日後にはどこを触ればいいのか分からない「神クラス(Godクラス)」が誕生してしまいます。
そんなときに意識したいのが「コンポーネント指向」と「単一責任」です。
コンボのロジックは「コンボのロジックだけ」を担当する小さなコンポーネントにしておくと、プレイヤーにも敵にも簡単に使い回せて、テストやデバッグもぐっと楽になります。
この記事では、
- 連続ヒット数をカウントする
- 一定時間攻撃が途切れたらコンボを自動リセットする
という役割だけに責務を絞った 「ComboCounter」コンポーネント を作っていきます。
【Unity】シンプルに盛り上がるコンボシステム!「ComboCounter」コンポーネント
フルコード:ComboCounter.cs
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 連続ヒット数(コンボ)を管理するコンポーネント。
/// - RegisterHit() を呼ぶたびにコンボ数を加算
/// - 一定時間ヒットが無いと自動でコンボをリセット
/// - イベントで UI やエフェクト側に通知
///
/// 責務を「コンボの数え上げとリセット」に限定しているので、
/// 実際の攻撃判定やダメージ処理は別コンポーネントに任せる設計です。
/// </summary>
public class ComboCounter : MonoBehaviour
{
[Header("コンボ設定")]
[SerializeField]
[Tooltip("コンボが維持される最大時間(秒)。この時間ヒットが無いとコンボリセット。")]
private float comboTimeoutSeconds = 2.0f;
[SerializeField]
[Tooltip("この数以上のコンボになったら、OnComboThresholdReached イベントを発火します。0以下なら無効。")]
private int highlightThreshold = 5;
[Header("デバッグ表示")]
[SerializeField]
[Tooltip("現在のコンボ数をインスペクターに表示するためのフィールド(読み取り専用のつもりで使う)。")]
private int currentCombo = 0;
[SerializeField]
[Tooltip("最後にヒットが発生してからの経過時間(秒)。デバッグ用。")]
private float elapsedSinceLastHit = 0f;
[Header("イベント")]
[SerializeField]
[Tooltip("コンボ数が変化したときに呼ばれるイベント。int: 現在のコンボ数")]
private UnityEvent<int> onComboChanged;
[SerializeField]
[Tooltip("コンボがリセットされたときに呼ばれるイベント。")]
private UnityEvent onComboReset;
[SerializeField]
[Tooltip("設定した閾値以上のコンボに到達したときに呼ばれるイベント。int: 現在のコンボ数")]
private UnityEvent<int> onComboThresholdReached;
/// <summary>現在のコンボ数を外部から読み取るためのプロパティ(読み取り専用)。</summary>
public int CurrentCombo => currentCombo;
/// <summary>コンボが有効かどうか(1以上なら true)。</summary>
public bool HasCombo => currentCombo > 0;
/// <summary>タイムアウト時間の変更用プロパティ。実行中に動的に変えたい場合に使用。</summary>
public float ComboTimeoutSeconds
{
get => comboTimeoutSeconds;
set => comboTimeoutSeconds = Mathf.Max(0f, value);
}
private void Update()
{
// コンボが0ならタイムアウトを気にする必要がない
if (currentCombo <= 0)
{
return;
}
// 最後のヒットからの経過時間を加算
elapsedSinceLastHit += Time.deltaTime;
// 一定時間ヒットが無かったらコンボをリセット
if (elapsedSinceLastHit >= comboTimeoutSeconds)
{
ResetCombo();
}
}
/// <summary>
/// ヒットが発生したときに呼び出すメソッド。
/// 攻撃判定側のスクリプトから呼びましょう。
/// </summary>
public void RegisterHit()
{
// 経過時間をリセット
elapsedSinceLastHit = 0f;
// コンボ数を加算
currentCombo++;
// コンボ数変更イベントを通知
onComboChanged?.Invoke(currentCombo);
// 閾値に到達したらイベントを通知
if (highlightThreshold > 0 && currentCombo == highlightThreshold)
{
onComboThresholdReached?.Invoke(currentCombo);
}
}
/// <summary>
/// 外部から明示的にコンボをリセットしたい場合に呼ぶメソッド。
/// 例: プレイヤーがダメージを受けたときなど。
/// </summary>
public void ResetCombo()
{
if (currentCombo == 0)
{
// 既に0なら何もしない
return;
}
currentCombo = 0;
elapsedSinceLastHit = 0f;
// リセットイベント
onComboReset?.Invoke();
// 0になったことも通知したい場合は onComboChanged も呼ぶ
onComboChanged?.Invoke(currentCombo);
}
/// <summary>
/// コンボ数を強制的に設定したい場合(デバッグ・チート用)。
/// 通常のゲームプレイでは RegisterHit() / ResetCombo() を使うのが推奨です。
/// </summary>
/// <param name="value">設定したいコンボ数。0未満は0に補正。</param>
public void SetCombo(int value)
{
currentCombo = Mathf.Max(0, value);
elapsedSinceLastHit = 0f;
onComboChanged?.Invoke(currentCombo);
}
}
使い方の手順
ここからは、プレイヤーの攻撃コンボ を例に、実際の使い方をステップ形式で見ていきます。
手順①:コンポーネントをプロジェクトに追加
ComboCounter.csをプロジェクトのScriptsフォルダなどに保存します。- Unityに戻ると自動でコンパイルされ、コンポーネントとして使えるようになります。
手順②:プレイヤー(または敵)にアタッチ
- シーン内の Player オブジェクトを選択。
- Add Component から
ComboCounterを追加。 - インスペクターで以下を設定:
- Combo Timeout Seconds: 例) 2.0(2秒間ヒットが途切れるとコンボ終了)
- Highlight Threshold: 例) 5(5コンボ達成時にイベント発火)
手順③:攻撃スクリプトから RegisterHit() を呼ぶ
「敵に当たった瞬間」に RegisterHit() を1回呼べばOKです。
例として、シンプルな近接攻撃スクリプトを用意してみます。
using UnityEngine;
/// <summary>
/// 非常にシンプルな近接攻撃サンプル。
/// - 左クリックで攻撃
/// - Raycast で前方に敵がいるかチェック
/// - 当たったら ComboCounter に RegisterHit() を通知
/// 実運用ではダメージ処理やアニメーションなどを別コンポーネントに分離すると綺麗です。
/// </summary>
[RequireComponent(typeof(ComboCounter))]
public class SimpleMeleeAttack : MonoBehaviour
{
[SerializeField]
[Tooltip("攻撃が届く最大距離")]
private float attackRange = 2.0f;
[SerializeField]
[Tooltip("攻撃が当たるレイヤー(敵など)")]
private LayerMask hitLayerMask;
private ComboCounter comboCounter;
private void Awake()
{
// 必須コンポーネントとして ComboCounter を取得
comboCounter = GetComponent<ComboCounter>();
}
private void Update()
{
// 左クリックで攻撃(InputSystem を使うなら別途アクションから呼ぶ形にしましょう)
if (Input.GetMouseButtonDown(0))
{
TryAttack();
}
}
private void TryAttack()
{
Ray ray = new Ray(transform.position, transform.forward);
if (Physics.Raycast(ray, out RaycastHit hitInfo, attackRange, hitLayerMask))
{
// ここで本来は敵の HP を減らしたりする
Debug.Log($"Hit: {hitInfo.collider.name}");
// ヒットが成立したのでコンボを加算
comboCounter.RegisterHit();
}
}
}
このように、「攻撃が当たったかどうか」だけを攻撃スクリプトが判断し、「コンボの管理」は ComboCounter に丸投げする形にすると、責務が綺麗に分かれます。
手順④:UIテキストやエフェクトと連携する
コンボが溜まっているときは画面に「x3」「x10」と表示したくなりますよね。
そのために ComboCounter には UnityEvent を用意してあります。
- コンボ数表示用の TextMeshProUGUI や UI を用意。
- 以下のようなシンプルな UI 更新スクリプトを作成して、Canvas 上のオブジェクトにアタッチします。
using UnityEngine;
using TMPro;
/// <summary>
/// ComboCounter と連携して、コンボ数を TextMeshPro で表示するサンプル。
/// </summary>
public class ComboTextUI : MonoBehaviour
{
[SerializeField]
private ComboCounter comboCounter;
[SerializeField]
private TextMeshProUGUI comboText;
[SerializeField]
[Tooltip("コンボが0のときに表示するテキスト(空なら非表示)。")]
private string zeroComboText = "";
private void Start()
{
if (comboCounter == null)
{
Debug.LogError("ComboCounter の参照が設定されていません。", this);
enabled = false;
return;
}
if (comboText == null)
{
Debug.LogError("TextMeshProUGUI の参照が設定されていません。", this);
enabled = false;
return;
}
// 初期表示を更新
UpdateComboText(comboCounter.CurrentCombo);
}
/// <summary>
/// ComboCounter の OnComboChanged イベントから呼ばれる想定のメソッド。
/// </summary>
public void OnComboChanged(int combo)
{
UpdateComboText(combo);
}
private void UpdateComboText(int combo)
{
if (combo <= 0)
{
if (string.IsNullOrEmpty(zeroComboText))
{
comboText.gameObject.SetActive(false);
}
else
{
comboText.gameObject.SetActive(true);
comboText.text = zeroComboText;
}
}
else
{
comboText.gameObject.SetActive(true);
comboText.text = $"{combo} Combo!";
}
}
}
次に、イベントの紐付け を行います。
- Player の
ComboCounterコンポーネントを選択。 - インスペクターの On Combo Changed イベントに要素を追加。
- ヒエラルキーから ComboTextUI を持つオブジェクトをドラッグ&ドロップ。
- 関数選択ドロップダウンから
ComboTextUI -> OnComboChanged(int)を選択。
これで、コンボ数が変わるたびに UI が自動更新されます。
メリットと応用
メリット①:プレイヤーにも敵にも簡単に流用できる
ComboCounter は「ヒットが起きたことを報告する」ための RegisterHit() と「リセットする」ための ResetCombo() しか公開していません。
そのため、
- プレイヤーの攻撃コンボ
- ボスの多段ヒット攻撃(一定時間途切れるとコンボ終了)
- トレーニングモードでの「連続ヒット数」カウント
など、どのキャラクターにもそのまま使い回せます。
プレハブに ComboCounter を事前に仕込んでおけば、レベルデザイン時には「コンボを持つ敵」としてドラッグ&ドロップするだけで済みます。
メリット②:UIやエフェクト側はイベント駆動でスッキリ
コンボのUI表示やエフェクト再生は、Update() で「今のコンボ数はいくつだろう?」と毎フレーム監視する必要はありません。
onComboChanged や onComboThresholdReached のイベントにぶら下げておけば、
- コンボが増えた瞬間だけ UI を更新
- 5コンボ達成時にだけ専用エフェクトを再生
といった処理が自然に書けます。
これにより、UI側のスクリプトは「イベントを受けて表示を変えるだけ」という単純な責務になります。
メリット③:テストやデバッグがしやすい
ComboCounter は単体で振る舞いが完結しているので、
- エディタ上で
SetCombo()を呼んで UI の見え方を確認 - タイムアウト時間を変えてゲームバランスを調整
- ログを仕込んで「いつリセットされたか」を追う
といった検証がしやすくなります。
巨大な Player クラスの中にコンボ処理が埋もれている場合と比べると、圧倒的に見通しが良いです。
改造案:コンボに応じてダメージ倍率を返す
応用として、「コンボ数に応じてダメージ倍率を変えたい」というケースを考えてみましょう。
以下のようなメソッドを ComboCounter に追加するだけで、攻撃側から簡単に利用できます。
/// <summary>
/// 現在のコンボ数に応じてダメージ倍率を計算するサンプル。
/// 例:
/// - 0コンボ: 1.0倍
/// - 1〜4コンボ: 1.0〜1.4倍
/// - 5コンボ以上: 1.5倍で頭打ち
/// </summary>
public float GetDamageMultiplier()
{
if (currentCombo <= 0)
{
return 1.0f;
}
// コンボ数に応じて少しずつ倍率アップ(最大1.5倍)
float multiplier = 1.0f + currentCombo * 0.1f;
return Mathf.Min(multiplier, 1.5f);
}
攻撃スクリプト側では、
float baseDamage = 10f;
float finalDamage = baseDamage * comboCounter.GetDamageMultiplier();
のように呼び出すだけで、「コンボが続くほど強くなる攻撃」を簡単に実装できます。
このように、コンボのロジックを1つの小さなコンポーネントに閉じ込めることで、ゲーム全体の設計がすっきりしていきます。
「Updateに全部書いてしまう」状態から一歩抜け出して、役割ごとにコンポーネントを分けていく習慣をつけていきましょう。
