Unityを触り始めた頃、「とりあえず攻撃処理は全部 Update() に書いておこう」とやりがちですよね。
入力判定も、ダメージ計算も、エフェクト再生も、クリティカル判定も、全部ひとつのスクリプトにベタ書き…。
最初は動きますが、あとから「クリティカルの仕様を変えたい」「敵だけクリティカル倍率を変えたい」となった瞬間、巨大なGodクラスの中から該当コードを探して地獄を見ることになります。
そこで今回は、「クリティカル判定」という単一の責務だけを切り出したコンポーネント
CriticalHitter を用意して、攻撃ダメージ計算の中にきれいに組み込める形にしてみましょう。
【Unity】クリティカルでド派手ダメージ!「CriticalHitter」コンポーネント
CriticalHitter は、
- クリティカル発生確率
- クリティカル時のダメージ倍率
- 乱数シードの管理(デバッグ再現用)
といった「クリティカル判定」に関する責務だけを担当させるコンポーネントです。
プレイヤーでも敵でも、「攻撃する側」にアタッチしておいて、
CalculateDamage() を呼び出すだけでクリティカル込みのダメージを簡単に計算できます。
フルソースコード:CriticalHitter.cs
using UnityEngine;
namespace Combat
{
/// <summary>
/// クリティカル判定とダメージ倍率計算だけを担当するコンポーネント。
/// - クリティカル発生確率
/// - クリティカル時のダメージ倍率
/// - 乱数シード(任意)
/// を管理し、「攻撃ダメージ計算」の中から呼び出して使います。
///
/// 責務を「クリティカル関連のロジック」に限定することで、
/// 攻撃処理のGodクラス化を防ぎます。
/// </summary>
public class CriticalHitter : MonoBehaviour
{
[Header("クリティカル基本設定")]
[Tooltip("0〜1の範囲で指定。0.25 なら 25% の確率でクリティカル。")]
[SerializeField, Range(0f, 1f)]
private float criticalChance = 0.2f;
[Tooltip("クリティカル時のダメージ倍率。2.0 なら 2倍ダメージ。")]
[SerializeField, Min(1f)]
private float criticalMultiplier = 2.0f;
[Header("デバッグ用オプション")]
[Tooltip("true の場合、固定シードで乱数を生成して挙動を再現しやすくします。")]
[SerializeField]
private bool useFixedSeed = false;
[Tooltip("useFixedSeed が true のときに使用するシード値。")]
[SerializeField]
private int fixedSeed = 12345;
// 内部で使用する乱数生成器
private System.Random random;
/// <summary>
/// 直近の判定がクリティカルだったかどうか。
/// UI演出などから参照するための読み取り専用プロパティ。
/// </summary>
public bool LastHitWasCritical { get; private set; }
/// <summary>
/// 直近のクリティカル倍率(クリティカルでなければ 1.0)。
/// ダメージ表示の色分けやログ出力に使えます。
/// </summary>
public float LastCriticalMultiplier { get; private set; } = 1f;
private void Awake()
{
// 乱数生成器の初期化
if (useFixedSeed)
{
random = new System.Random(fixedSeed);
}
else
{
// シード未指定の場合は時間ベースで適当に
random = new System.Random();
}
}
/// <summary>
/// 基礎ダメージに対して、クリティカル判定込みの最終ダメージを計算します。
///
/// 例:
/// baseDamage = 10, criticalChance = 0.3, criticalMultiplier = 2.0
/// - 70% の確率で 10 ダメージ
/// - 30% の確率で 20 ダメージ
/// </summary>
/// <param name="baseDamage">クリティカル前の基礎ダメージ</param>
/// <returns>クリティカル判定を反映した最終ダメージ</returns>
public int CalculateDamage(int baseDamage)
{
if (baseDamage <= 0)
{
// マイナスやゼロダメージはそのまま返す(クリティカルさせない)
LastHitWasCritical = false;
LastCriticalMultiplier = 1f;
return baseDamage;
}
bool isCritical = RollCritical();
LastHitWasCritical = isCritical;
LastCriticalMultiplier = isCritical ? criticalMultiplier : 1f;
// 実数で倍率をかけてから四捨五入して整数に戻す
float rawDamage = baseDamage * LastCriticalMultiplier;
int finalDamage = Mathf.RoundToInt(rawDamage);
return finalDamage;
}
/// <summary>
/// クリティカルが発生したかどうかを判定します。
/// このメソッド単体を使って、
/// 「クリティカル時だけエフェクトを出す」といった用途にも利用できます。
/// </summary>
/// <returns>クリティカルなら true</returns>
public bool RollCritical()
{
// 0.0 〜 1.0 未満の乱数を取得
double value = random.NextDouble();
// 乱数が「確率」より小さければクリティカル
bool isCritical = value <= criticalChance;
return isCritical;
}
/// <summary>
/// ランタイム中にクリティカル確率を変更します。
/// スキルやバフでの一時的な上昇などに利用できます。
/// </summary>
/// <param name="newChance">0〜1の範囲にクランプされます</param>
public void SetCriticalChance(float newChance)
{
criticalChance = Mathf.Clamp01(newChance);
}
/// <summary>
/// ランタイム中にクリティカル倍率を変更します。
/// クリティカルダメージアップ系のバフなどに利用できます。
/// </summary>
/// <param name="newMultiplier">1以上を想定。1未満が指定された場合は1にクランプ。</param>
public void SetCriticalMultiplier(float newMultiplier)
{
criticalMultiplier = Mathf.Max(1f, newMultiplier);
}
/// <summary>
/// デバッグ用:インスペクターから現在の設定を確認しやすくするための
/// プロパティアクセサ(読み取り専用)。
/// </summary>
public float CriticalChance => criticalChance;
public float CriticalMultiplier => criticalMultiplier;
}
}
使い方の手順
ここでは、プレイヤーが敵に攻撃するシンプルな例で使い方を説明します。
プレイヤーだけでなく、敵やトラップ、動く床の「接触ダメージ」などにも同じ要領で使えます。
手順①:攻撃者側に CriticalHitter をアタッチ
- 空の GameObject でも、プレイヤーのルートオブジェクトでもOKです。
- Inspector で「Add Component」→
CriticalHitterを追加します。 - Critical Chance(例:0.25 = 25%)と Critical Multiplier(例:2.5倍)を好みに設定します。
手順②:シンプルな攻撃スクリプトから呼び出す
攻撃ロジックは別コンポーネントに分けて、CriticalHitter を「ダメージ計算の道具」として使います。
以下は、プレイヤーがスペースキーで敵にダメージを与える非常にシンプルな例です。
using UnityEngine;
using Combat; // CriticalHitter の namespace
/// <summary>
/// 非常にシンプルな攻撃サンプル。
/// 入力と当たり判定とダメージ計算を最低限に抑え、
/// クリティカル計算だけを CriticalHitter に委譲します。
/// </summary>
public class SimpleAttack : MonoBehaviour
{
[Header("攻撃設定")]
[SerializeField]
private int baseDamage = 10;
[Tooltip("攻撃対象とするレイヤー(例:Enemy)")]
[SerializeField]
private LayerMask targetLayer;
[Tooltip("攻撃が届く最大距離")]
[SerializeField]
private float attackRange = 2f;
[Header("参照コンポーネント")]
[SerializeField]
private CriticalHitter criticalHitter;
private void Reset()
{
// 同じ GameObject 上に CriticalHitter があれば自動参照
criticalHitter = GetComponent<CriticalHitter>();
}
private void Update()
{
// 超シンプルな入力例:スペースキーで攻撃
if (Input.GetKeyDown(KeyCode.Space))
{
TryAttack();
}
}
private void TryAttack()
{
if (criticalHitter == null)
{
Debug.LogWarning("CriticalHitter が設定されていません。ノンクリティカルで計算します。", this);
}
// 正面方向に Ray を飛ばして敵を判定する簡易版
Ray ray = new Ray(transform.position, transform.forward);
if (Physics.Raycast(ray, out RaycastHit hit, attackRange, targetLayer))
{
int finalDamage = baseDamage;
if (criticalHitter != null)
{
// クリティカル判定込みでダメージを計算
finalDamage = criticalHitter.CalculateDamage(baseDamage);
}
// ダメージを与える相手は IDamageable などのインターフェースにしておくと綺麗
var damageable = hit.collider.GetComponent<IDamageable>();
if (damageable != null)
{
damageable.TakeDamage(finalDamage);
// ログにクリティカル情報を出してみる
if (criticalHitter != null && criticalHitter.LastHitWasCritical)
{
Debug.Log($"CRITICAL! {finalDamage} ダメージ!(倍率: {criticalHitter.LastCriticalMultiplier:F1})");
}
else
{
Debug.Log($"{finalDamage} ダメージ");
}
}
}
}
}
/// <summary>
/// ダメージを受ける側が実装するインターフェースの例。
/// </summary>
public interface IDamageable
{
void TakeDamage(int damage);
}
手順③:敵側にダメージ受け取りスクリプトを用意
敵側は「ダメージを受ける」という責務だけを持つコンポーネントにしておくと、プレイヤー以外にも使い回せて便利です。
using UnityEngine;
/// <summary>
/// 非常にシンプルな敵のHP管理。
/// IDamageable を実装しているので SimpleAttack からダメージを受け取れます。
/// </summary>
public class SimpleEnemy : MonoBehaviour, IDamageable
{
[SerializeField]
private int maxHp = 50;
private int currentHp;
private void Awake()
{
currentHp = maxHp;
}
public void TakeDamage(int damage)
{
currentHp -= damage;
Debug.Log($"{gameObject.name} は {damage} ダメージを受けた!(残りHP: {currentHp})");
if (currentHp <= 0)
{
Die();
}
}
private void Die()
{
Debug.Log($"{gameObject.name} は倒れた!");
// 実際のゲームではアニメーション再生やドロップ処理などを書く
Destroy(gameObject);
}
}
手順④:シーンに配置してテスト
- シーンに「Player」オブジェクトを作成し、
SimpleAttackとCriticalHitterをアタッチ。 - 「Enemy」オブジェクトを作り、
SimpleEnemyをアタッチし、レイヤーを「Enemy」などに設定。 SimpleAttack.targetLayerに「Enemy」レイヤーを設定。- 再生して、プレイヤーを敵の方向に向けてスペースキーを押すと、ランダムにクリティカルが発生し、ログに倍率付きで表示されます。
動く床やトラップに使う場合は、OnTriggerEnter などで CalculateDamage を呼び出すだけで、同じクリティカルロジックを再利用できます。
メリットと応用
CriticalHitter を分離しておくメリットはかなり大きいです。
- プレハブごとにクリティカル仕様を分けられる
プレイヤーは「25% で 2倍」、ボスは「10% で 5倍」、雑魚敵は「5% で 1.5倍」など、
プレハブのインスペクターから数値をいじるだけで簡単に差別化できます。 - レベルデザイン時に数値調整がしやすい
「このステージはクリティカル出やすくして爽快感マシマシにしよう」など、
シーン上のオブジェクトにアタッチされたCriticalHitterの値をGUIから直接いじるだけでテストできます。 - 演出との連携がシンプル
LastHitWasCriticalやLastCriticalMultiplierを参照して、
ダメージテキストを赤くしたり、カメラを揺らしたり、といった「派手な数字演出」を簡単に追加できます。 - Godクラス化を防げる
クリティカル判定ロジックを攻撃スクリプトから切り離すことで、
攻撃処理は「いつ・誰に・いくらダメージを与えるか」だけに集中でき、
クリティカルは専用コンポーネントに任せる、というきれいな責務分割ができます。
改造案:クリティカル時だけ派手な数字を出す
例えば、ダメージテキストを出すコンポーネントから CriticalHitter を参照して、
クリティカル時だけ色やサイズを変えることができます。
using UnityEngine;
using TMPro;
using Combat;
/// <summary>
/// ダメージ表示用の簡易コンポーネント例。
/// CriticalHitter の結果を参照して、クリティカル時だけ演出を派手にします。
/// </summary>
public class DamageTextSpawner : MonoBehaviour
{
[SerializeField]
private TextMeshPro damageTextPrefab;
[SerializeField]
private Color normalColor = Color.white;
[SerializeField]
private Color criticalColor = Color.yellow;
[SerializeField]
private float normalScale = 1f;
[SerializeField]
private float criticalScale = 1.5f;
[SerializeField]
private CriticalHitter criticalHitter;
public void SpawnDamageText(int damage, Vector3 worldPosition)
{
TextMeshPro text = Instantiate(damageTextPrefab, worldPosition, Quaternion.identity);
text.text = damage.ToString();
bool isCritical = criticalHitter != null && criticalHitter.LastHitWasCritical;
text.color = isCritical ? criticalColor : normalColor;
text.transform.localScale = Vector3.one * (isCritical ? criticalScale : normalScale);
}
}
このように、CriticalHitter を「クリティカルの真偽と倍率を教えてくれるコンポーネント」として扱うことで、
ダメージ表示・サウンド・カメラシェイクなどの演出コンポーネントときれいに連携できます。
クリティカル判定というひとつの機能を小さなコンポーネントに切り出しておくと、
ゲーム全体の設計もすっきりして、プレハブ管理やレベルデザインがかなり楽になります。
「大きな攻撃スクリプトに全部詰め込む」のではなく、「小さな責務ごとにコンポーネントを分ける」方向で組み立てていきましょう。
