Unityを触り始めた頃って、つい「とりあえず Update() に全部書いてしまう」ことが多いですよね。
プレイヤーの移動、攻撃、ダメージ処理、UI更新……すべてを1つの巨大スクリプトに詰め込むと、次のような問題が出てきます。
- どこで何が起きているか追えなくなる(デバッグ地獄)
- 別のキャラや敵にロジックを流用しづらい
- ちょっと仕様変更するだけで他の機能が壊れがち
とくに「ダメージ判定」まわりは、攻撃側・防御側・ステータス・エフェクトなどが絡み合いやすく、
気づくと1つのクラスが「HP管理+ダメージ計算+回避判定+UI更新」みたいなGodクラスになりがちです。
そこでこの記事では、「回避判定」だけに責務を絞ったコンポーネントとして、
EvasionCalculator を用意して、攻撃を受けたときに Agility(敏捷)ステータスに基づいて確率でダメージを無効化(Miss) する仕組みを作ってみましょう。
【Unity】ステータス連動でスマートに回避判定!「EvasionCalculator」コンポーネント
ここでは、以下のようなシンプルな構成を目指します。
Damageable…… 「ダメージを受けられる存在」を表すコンポーネントSimpleStats…… HP と Agility を持つだけの軽量ステータスEvasionCalculator…… Agility に応じて「回避(Miss)するかどうか」を判定するコンポーネント
攻撃が当たったとき、Damageable が EvasionCalculator に「今回の攻撃、回避する?」と問い合わせ、
回避成功ならダメージ0、失敗なら普通にダメージ適用、という流れにします。
フルコード
using UnityEngine;
namespace Sample.Evasion
{
/// <summary>
/// シンプルなステータス保持クラス。
/// HP と Agility(敏捷)だけを持ちます。
/// 本番プロジェクトでは、ここを拡張してもOKですが、
/// 責務を増やしすぎないように注意しましょう。
/// </summary>
public class SimpleStats : MonoBehaviour
{
[Header("基本ステータス")]
[SerializeField] private int maxHp = 100;
[SerializeField] private int currentHp = 100;
[Tooltip("回避率の計算に使う敏捷ステータス")]
[SerializeField] private int agility = 10;
public int MaxHp => maxHp;
public int CurrentHp => currentHp;
public int Agility => agility;
/// <summary>
/// ダメージを直接HPに適用するシンプルな関数。
/// 回避判定などはここでは行わず、
/// Damageable や EvasionCalculator に任せます。
/// </summary>
public void ApplyDamage(int damage)
{
// マイナスダメージが来ても安全に処理
if (damage < 0)
{
Debug.LogWarning($"[SimpleStats] マイナスダメージが渡されました: {damage}");
return;
}
currentHp -= damage;
currentHp = Mathf.Clamp(currentHp, 0, maxHp);
Debug.Log($"[SimpleStats] ダメージ {damage} を受けました。残りHP: {currentHp}");
if (currentHp <= 0)
{
HandleDeath();
}
}
private void HandleDeath()
{
// とりあえずログだけ。実際のゲームではここで死亡アニメやリスポーン処理などを呼び出します。
Debug.Log($"[SimpleStats] {gameObject.name} は倒れました。");
}
}
/// <summary>
/// 回避判定専用のコンポーネント。
/// Agility ステータスに基づき、攻撃を受けた際に
/// 一定確率でダメージを無効化(Miss)します。
///
/// 責務は「回避の確率計算と判定」に限定し、
/// 実際のダメージ適用は Damageable に任せます。
/// </summary>
[RequireComponent(typeof(SimpleStats))]
public class EvasionCalculator : MonoBehaviour
{
[Header("回避率設定")]
[Tooltip("Agility 1 あたりの基本回避率(0〜1)。例: 0.01f なら Agility 50 で 50% 回避。")]
[SerializeField] private float baseEvasionPerAgility = 0.01f;
[Tooltip("回避率の最大値(0〜1)。これ以上は上がらない上限値です。")]
[SerializeField] private float maxEvasionRate = 0.8f;
[Tooltip("ランダムシードを固定したい場合は true。デバッグ用。")]
[SerializeField] private bool useFixedRandomSeed = false;
[Tooltip("固定シード値。useFixedRandomSeed が true のときのみ使用。")]
[SerializeField] private int fixedRandomSeed = 12345;
private SimpleStats stats;
private System.Random random; // System.Random を使うことでテストしやすくしています。
private void Awake()
{
stats = GetComponent<SimpleStats>();
if (useFixedRandomSeed)
{
random = new System.Random(fixedRandomSeed);
}
else
{
// 時間ベースのシードでランダム性を確保
random = new System.Random(System.Environment.TickCount);
}
}
/// <summary>
/// 現在の Agility から「理論上の回避率(0〜1)」を計算して返します。
/// 実際の判定は PerformEvasionCheck() で行います。
/// </summary>
public float GetCurrentEvasionRate()
{
// マイナス値は0として扱う
int agility = Mathf.Max(0, stats.Agility);
// 線形で回避率を計算
float rate = agility * baseEvasionPerAgility;
// 上限を適用
rate = Mathf.Clamp01(rate);
rate = Mathf.Min(rate, maxEvasionRate);
return rate;
}
/// <summary>
/// 回避判定を実行し、「今回の攻撃を回避したかどうか」を返します。
/// true のときは Miss(ダメージ無効)、false のときは被弾です。
/// </summary>
public bool PerformEvasionCheck()
{
float evasionRate = GetCurrentEvasionRate();
// 0〜1 の乱数を生成
float roll = (float)random.NextDouble();
bool isEvaded = roll <= evasionRate;
Debug.Log(
$"[EvasionCalculator] 回避判定: " +
$"Agility={stats.Agility}, Rate={evasionRate:P1}, Roll={roll:F3}, " +
$"Result={(isEvaded ? "MISS" : "HIT")}"
);
return isEvaded;
}
}
/// <summary>
/// 「ダメージを受けられる存在」を表すコンポーネント。
/// EvasionCalculator を使用して回避判定を行い、
/// 実際のダメージ適用を SimpleStats に委譲します。
///
/// 攻撃側からは「TakeDamage(int) を呼べばOK」という
/// シンプルなインターフェースを提供します。
/// </summary>
[RequireComponent(typeof(SimpleStats))]
public class Damageable : MonoBehaviour
{
[Header("ダメージ設定")]
[Tooltip("回避判定を行う場合は true。false にすると常に被弾します。")]
[SerializeField] private bool useEvasion = true;
[Tooltip("回避に成功したときにログを出すかどうか。")]
[SerializeField] private bool logOnMiss = true;
private SimpleStats stats;
private EvasionCalculator evasionCalculator;
private void Awake()
{
stats = GetComponent<SimpleStats>();
evasionCalculator = GetComponent<EvasionCalculator>();
}
/// <summary>
/// 攻撃側から呼び出される想定のダメージ適用関数。
/// ここで回避判定を行い、必要に応じてダメージを無効化します。
/// </summary>
/// <param name="rawDamage">攻撃側が計算した生のダメージ値</param>
public void TakeDamage(int rawDamage)
{
if (rawDamage <= 0)
{
Debug.LogWarning($"[Damageable] 0以下のダメージが指定されました: {rawDamage}");
return;
}
// 回避を使う設定で、かつ EvasionCalculator が存在する場合のみ判定
if (useEvasion && evasionCalculator != null)
{
bool isEvaded = evasionCalculator.PerformEvasionCheck();
if (isEvaded)
{
if (logOnMiss)
{
Debug.Log($"[Damageable] {gameObject.name} は攻撃を回避しました!(MISS)");
}
// ダメージは適用しない
return;
}
}
// 回避しなかった場合のみダメージ適用
stats.ApplyDamage(rawDamage);
}
}
/// <summary>
/// テスト用の簡易攻撃スクリプト。
/// キー入力に応じて、指定した Damageable にダメージを与えます。
/// 実際のゲームでは、攻撃アニメーションやヒット判定から
/// TakeDamage を呼び出す想定です。
/// </summary>
public class SimpleAttacker : MonoBehaviour
{
[Header("攻撃対象")]
[SerializeField] private Damageable target;
[Header("攻撃設定")]
[SerializeField] private int attackDamage = 10;
[Tooltip("テスト用に1秒間に何回攻撃するか(自動攻撃用)。0以下で自動攻撃オフ。")]
[SerializeField] private float autoAttackPerSecond = 0f;
private float autoAttackTimer;
private void Update()
{
// スペースキーで1回攻撃(簡易テスト用)
if (Input.GetKeyDown(KeyCode.Space))
{
PerformAttack();
}
// 自動攻撃(DPSテストなどに便利)
if (autoAttackPerSecond > 0f && target != null)
{
autoAttackTimer += Time.deltaTime;
float interval = 1f / autoAttackPerSecond;
while (autoAttackTimer >= interval)
{
autoAttackTimer -= interval;
PerformAttack();
}
}
}
private void PerformAttack()
{
if (target == null)
{
Debug.LogWarning("[SimpleAttacker] 攻撃対象が設定されていません。");
return;
}
Debug.Log($"[SimpleAttacker] {target.gameObject.name} に {attackDamage} ダメージの攻撃!");
target.TakeDamage(attackDamage);
}
}
}
使い方の手順
ここでは、プレイヤーが敵からの攻撃を受けるケースを例に、セットアップ手順を見ていきます。
-
プレイヤー用 GameObject を用意する
- Hierarchy で
Right Click > 3D Object > Capsuleなどでプレイヤーの見た目を作成。 - 名前を
Playerに変更しておきましょう。
- Hierarchy で
-
プレイヤーにステータスと回避コンポーネントを付ける
Playerを選択し、Inspector でAdd Component。SimpleStatsを追加し、Max HpやAgilityを好みの値に設定(例: HP=100, Agility=30)。- 続けて
EvasionCalculatorを追加。 Base Evasion Per Agilityを0.01にすると、Agility=30 で約30%回避になります。- 最後に
Damageableを追加。Use Evasionがオンになっていることを確認。
-
敵(攻撃側)を用意して攻撃させる
- Hierarchy で
Right Click > 3D Object > Cubeなどで敵の見た目を作成。 - 名前を
Enemyに変更。 EnemyにSimpleAttackerを追加。TargetにPlayerのDamageableをドラッグ&ドロップして設定。Attack Damageを 10 などに設定。- テスト用途で
Auto Attack Per Secondを1にすると、毎秒1回自動攻撃してくれます。
- Hierarchy で
-
再生して回避判定を確認する
- Play ボタンを押してゲームを再生。
- Space キーを押すか、自動攻撃をオンにしておくと、Console にログが出ます。
[EvasionCalculator]ログで、Rate(回避率)とRoll(乱数)が確認できます。Result=MISSのときは回避成功、HITのときは被弾です。[SimpleStats]のログで、実際にHPが減ったかどうかも確認できます。
同じ仕組みは、敵キャラ や 動く床(トラップ) にもそのまま流用できます。
- 敵がプレイヤー攻撃を「回避」する敵専用ステータス
- トラップのダメージを、プレイヤーの敏捷で「すり抜けられる」かどうか判定
といった形で、「ダメージを受ける側にだけ EvasionCalculator を付ける」という統一ルールにしておくと管理がとても楽になります。
メリットと応用
EvasionCalculator をコンポーネントとして切り出すことで、次のようなメリットがあります。
-
プレハブの再利用性が高い
プレイヤー、雑魚敵、ボスなど、「回避できるキャラ」にはすべて同じコンポーネントを付けるだけで済みます。
回避ロジックを変えたくなった場合も、このコンポーネント1つを修正すれば全キャラに反映されます。 -
レベルデザインがインスペクタで完結する
AgilityやBase Evasion Per Agility、Max Evasion Rateをいじるだけで、
「この敵は回避高め」「このボスはほとんど回避しない」などを簡単に調整できます。
シーンごとにプレハブを複製してパラメータだけ変える、といった運用もしやすいです。 -
責務が分離されているのでテストしやすい
回避判定はEvasionCalculator、HP管理はSimpleStats、
「ダメージを受ける窓口」はDamageableと分割しているので、
どこかがおかしくなっても原因を特定しやすくなります。
さらに、「改造案」として、たとえば 連続で攻撃を受けると一時的に回避率が上がる ような仕組みを追加してみるのも面白いです。
以下は、EvasionCalculator に「直近数秒間の被弾数に応じてボーナス回避率を付与する」関数を追加する例です。
/// <summary>
/// 被弾回数に応じて一時的な回避ボーナスを計算する例。
/// 例えば、「直近で多く攻撃されているほど、回避しやすくなる」ような
/// ラバー・バンディング的な仕組みに応用できます。
///
/// 実際には、被弾回数をどこかでカウントして渡してあげる必要があります。
/// </summary>
/// <param name="recentHitCount">直近数秒間の被弾回数など</param>
public float CalculateTemporaryEvasionBonus(int recentHitCount)
{
// 1回の被弾につき +2% のボーナス(例)
float bonusPerHit = 0.02f;
float bonus = recentHitCount * bonusPerHit;
// ボーナスは最大 20% まで
bonus = Mathf.Clamp(bonus, 0f, 0.2f);
return bonus;
}
このように、回避ロジックに関するアイデアはすべて EvasionCalculator に集約しておくと、
ゲーム全体の設計がスッキリして、後からの調整や仕様追加もやりやすくなります。
ぜひ、自分のプロジェクト用にカスタマイズしつつ、「小さな責務のコンポーネント」を積み重ねる設計を意識してみてください。
