Unityを触り始めると、つい「とりあえず全部Updateに書く」実装になりがちですよね。
プレイヤー操作、ダメージ計算、UI更新、エフェクト制御…全部ひとつのスクリプトに押し込んでしまうと、次のような問題が出てきます。
- 処理の見通しが悪くなり、バグ修正が怖くなる
- 敵やボスにも同じロジックを使いたいのに、コピペ地獄になる
- 「ちょっと吸血効果を入れたい」だけなのに、巨大クラスを編集する必要がある
そこでおすすめなのが、「1つのスクリプト = 1つの責務」を意識したコンポーネント指向の設計です。
今回は「攻撃ヒット時に与えたダメージの一部を回復する」吸血攻撃を、単独で付け外しできるコンポーネントとして実装してみましょう。
【Unity】殴るほど回復する吸血ビルド!「Lifesteal」コンポーネント
この記事では、攻撃ヒット時に与えたダメージの一定割合を自分のHPとして回復する「Lifesteal」コンポーネントを実装します。
ポイントは以下です。
- HP管理は専用コンポーネント
Healthに分離 - ダメージ通知は
Damageableコンポーネント経由のイベントでやり取り Lifestealは「ダメージを与えたときのフック」だけに責務を絞る
これにより、
- プレイヤーだけでなく、敵やボスにも簡単に吸血効果を付与できる
- 吸血の有無を「コンポーネントを付けるかどうか」で切り替えられる
- ダメージ処理の中心ロジックを汚さずに済む
それでは、必要なコンポーネント一式のコードから見ていきましょう。
フルコード一式
以下の3コンポーネントを用意します。
- Health … HPの管理(ダメージ・回復・死亡イベント)
- Damageable … ダメージを受ける対象に付ける「被ダメージ窓口」
- Lifesteal … 与えたダメージの一部を回復する吸血コンポーネント
1. Health.cs(HP管理)
using UnityEngine;
using UnityEngine.Events;
/// <summary>シンプルなHP管理コンポーネント</summary>
public class Health : MonoBehaviour
{
[Header("HP 設定")]
[SerializeField] private float maxHealth = 100f;
[SerializeField] private float currentHealth = 100f;
[Header("イベント")]
[SerializeField] private UnityEvent onDeath;
[SerializeField] private UnityEvent onDamaged; // 受けたダメージ量
[SerializeField] private UnityEvent onHealed; // 回復量
public float MaxHealth => maxHealth;
public float CurrentHealth => currentHealth;
public bool IsDead => currentHealth <= 0f;
private void Awake()
{
// エディタ上で currentHealth を変更していない場合は maxHealth で初期化
if (Mathf.Approximately(currentHealth, 0f))
{
currentHealth = maxHealth;
}
}
/// <summary>ダメージを受ける(マイナス値を渡さないこと)</summary>
public void TakeDamage(float amount)
{
if (IsDead) return;
if (amount <= 0f) return;
currentHealth = Mathf.Max(0f, currentHealth - amount);
onDamaged?.Invoke(amount);
if (IsDead)
{
onDeath?.Invoke();
}
}
/// <summary>回復する(マイナス値を渡さないこと)</summary>
public void Heal(float amount)
{
if (IsDead) return;
if (amount <= 0f) return;
float oldHealth = currentHealth;
currentHealth = Mathf.Min(maxHealth, currentHealth + amount);
float healedAmount = currentHealth - oldHealth;
if (healedAmount > 0f)
{
onHealed?.Invoke(healedAmount);
}
}
/// <summary>HPを最大値まで全回復</summary>
public void RestoreFull()
{
float amount = maxHealth - currentHealth;
Heal(amount);
}
}
2. Damageable.cs(ダメージ窓口)
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 「このオブジェクトはダメージを受けられる」ことを表すコンポーネント。
/// 攻撃側はこのコンポーネントに対して Damage() を呼ぶだけにすると、
/// ダメージ処理の責務を分離しやすくなります。
/// </summary>
[RequireComponent(typeof(Health))]
public class Damageable : MonoBehaviour
{
[SerializeField] private Health health;
// 「誰から」「どれだけ」ダメージを受けたかのイベント
[System.Serializable]
public class DamageEvent : UnityEvent<GameObject, float> { }
[Header("ダメージイベント")]
[SerializeField] private DamageEvent onDamagedBy;
public DamageEvent OnDamagedBy => onDamagedBy;
private void Reset()
{
health = GetComponent<Health>();
}
private void Awake()
{
if (health == null)
{
health = GetComponent<Health>();
}
}
/// <summary>
/// ダメージを適用するメイン入口。
/// attacker: 攻撃者(null でも可)
/// amount : ダメージ量(0以上)
/// </summary>
public void Damage(GameObject attacker, float amount)
{
if (amount <= 0f) return;
// まずHPを減らす
health.TakeDamage(amount);
// その後、誰からどれだけダメージを受けたかを通知
onDamagedBy?.Invoke(attacker, amount);
}
}
3. Lifesteal.cs(吸血攻撃)
using UnityEngine;
/// <summary>
/// 攻撃ヒット時に与えたダメージの一定割合を自分のHPとして回復するコンポーネント。
///
/// 使い方の想定:
/// - 攻撃を行うオブジェクト(プレイヤー、敵など)にアタッチ
/// - 攻撃がヒットしたタイミングで OnAttackHit() を呼ぶ
/// (例:近接武器のトリガー、弾の衝突判定など)
///
/// SRP的には:
/// - 「いつダメージを与えるか」は別コンポーネント
/// - 「与えたダメージから何%吸血するか」だけをこのコンポーネントが担当
/// </summary>
[RequireComponent(typeof(Health))]
public class Lifesteal : MonoBehaviour
{
[Header("吸血設定")]
[Tooltip("与えたダメージの何%を回復するか(0〜1)。例: 0.2 で 20%")]
[SerializeField, Range(0f, 1f)]
private float lifestealRate = 0.2f;
[Tooltip("1ヒットで回復できる最大値。0以下なら制限なし")]
[SerializeField]
private float maxHealPerHit = 0f;
[Header("参照")]
[SerializeField] private Health selfHealth;
private void Reset()
{
selfHealth = GetComponent<Health>();
}
private void Awake()
{
if (selfHealth == null)
{
selfHealth = GetComponent<Health>();
}
}
/// <summary>
/// 攻撃がヒットしてダメージを与えたときに呼ぶ関数。
///
/// damageDealt: 実際に相手に与えたダメージ量
/// target : 攻撃対象(使わない場合は null でOK。将来の拡張用)
/// </summary>
public void OnAttackHit(float damageDealt, GameObject target)
{
if (damageDealt <= 0f) return;
if (selfHealth == null) return;
if (selfHealth.IsDead) return;
// 吸血量を計算
float healAmount = damageDealt * lifestealRate;
// 1ヒットあたりの最大回復量が設定されていれば制限
if (maxHealPerHit > 0f)
{
healAmount = Mathf.Min(healAmount, maxHealPerHit);
}
if (healAmount <= 0f) return;
// 自分自身を回復
selfHealth.Heal(healAmount);
// デバッグ用ログ(必要なければ削除)
// Debug.Log($"{gameObject.name} は吸血で {healAmount} 回復しました。");
}
}
4. おまけ:シンプルな近接攻撃例(任意)
「いつ OnAttackHit を呼ぶか」の一例として、
レイキャストで前方の敵にダメージを与える簡易近接攻撃コンポーネントを載せておきます。
using UnityEngine;
/// <summary>
/// 非常にシンプルな近接攻撃例。
/// - 左クリックで前方にレイを飛ばし、
/// Damageable を持つ対象にダメージを与える。
/// - Lifesteal を持っていれば、与えたダメージに応じて回復する。
///
/// 実運用では InputSystem やアニメーションイベントと連携させると良いです。
/// </summary>
[RequireComponent(typeof(Lifesteal))]
public class SimpleMeleeAttack : MonoBehaviour
{
[Header("攻撃設定")]
[SerializeField] private float damage = 20f;
[SerializeField] private float range = 2f;
[SerializeField] private LayerMask hitLayers = ~0; // 全レイヤー対象
[Header("参照")]
[SerializeField] private Lifesteal lifesteal;
private void Reset()
{
lifesteal = GetComponent<Lifesteal>();
}
private void Awake()
{
if (lifesteal == null)
{
lifesteal = GetComponent<Lifesteal>();
}
}
private void Update()
{
// デモ用にマウス左クリックで攻撃
if (Input.GetMouseButtonDown(0))
{
PerformAttack();
}
}
private void PerformAttack()
{
Ray ray = new Ray(transform.position, transform.forward);
if (Physics.Raycast(ray, out RaycastHit hit, range, hitLayers, QueryTriggerInteraction.Ignore))
{
Damageable damageable = hit.collider.GetComponentInParent<Damageable>();
if (damageable != null)
{
// 実際にダメージを与える
damageable.Damage(gameObject, damage);
// 吸血コンポーネントに「このダメージ量でヒットしたよ」と通知
lifesteal.OnAttackHit(damage, damageable.gameObject);
}
}
}
}
使い方の手順
手順①:スクリプトをプロジェクトに追加
- Unity の Project ウィンドウで、
Scriptsフォルダなどを作成。 - そこに以下の4つのC#ファイルを作成して、上記コードをそれぞれ貼り付けます。
Health.csDamageable.csLifesteal.csSimpleMeleeAttack.cs(デモ用・任意)
手順②:ダメージを受ける側(敵など)にコンポーネントを付ける
例として「Enemy」プレハブを作る場合:
- Hierarchy で Enemy 用の GameObject を作成(3Dなら Capsule など)。
- Enemy に以下のコンポーネントを追加。
Health… HP上限や初期値を設定(例:Max 100)Damageable… 自動でHealthが参照されます
- 必要に応じて
HealthのonDeathイベントに「Destroy(this.gameObject)」などを登録すると、HP0で消える敵が作れます。
手順③:攻撃する側(プレイヤーなど)に Lifesteal を付ける
例として「Player」オブジェクトに吸血近接攻撃を持たせる場合:
- Hierarchy で Player オブジェクトを用意。
- Player に以下のコンポーネントを追加。
Health… プレイヤーのHP管理Lifesteal… 吸血設定- Lifesteal Rate:0.2(= 20%)など
- Max Heal Per Hit:0(制限なし)か、1ヒットでの最大回復量を任意で設定
SimpleMeleeAttack… デモ用近接攻撃(攻撃ロジックを自作している場合は不要)
SimpleMeleeAttackの Damage や Range をお好みで調整。
これで「Player が左クリックで近接攻撃 → Enemy にダメージ → 与えたダメージの一部を Player が回復」という流れが動くはずです。
手順④:別の例(動く床やボスにも使う)
このコンポーネント設計の良いところは、攻撃する側に Lifesteal を付けるだけで吸血ビルドにできる点です。
- 敵のボスに Lifesteal を付ける
- ボスの GameObject に
HealthとLifestealを追加。 - ボスの攻撃ロジック(近接・遠距離問わず)で、ヒットしたときに
Lifesteal.OnAttackHit(実ダメージ, 対象)を呼ぶ。 - これだけで「殴ると回復するいやらしいボス」が作れます。
- ボスの GameObject に
- トゲ付き動く床
- 床オブジェクトに「プレイヤーにダメージを与える」スクリプトを付ける。
- その床に
HealthとLifestealを付ければ、「踏むとダメージ+床が回復する」ギミックにもできます(ギミックHPが減るゲーム設計の場合)。
メリットと応用
このように Lifesteal を「与えたダメージに応じて回復するだけの小さなコンポーネント」として切り出しておくと、プレハブ管理やレベルデザインがかなり楽になります。
メリット
- プレハブの組み合わせでビルドを表現できる
プレイヤーの「ビルド変更」を、巨大な Player スクリプトの if 文ではなく、
Lifestealを付けたり外したりするだけで表現できます。
吸血ビルド・クリティカルビルド・毒ビルドなどを、コンポーネント単位で差し替えられます。 - 敵AIとダメージ処理を分離できる
敵AIスクリプトは「いつ攻撃するか」だけに集中させ、
ダメージと吸血はDamageable/Lifestealに任せられます。
これにより、AIロジックの見通しが良くなります。 - テストしやすい
Lifesteal単体をテストしたい場合、
シーンにダミーのHealthを持つオブジェクトを置いて、
インスペクターからOnAttackHit(50, null)を呼ぶだけで挙動確認ができます。
応用アイデア
- 「HPが一定以下のときだけ吸血量を増やす」狂戦士ビルド
- 「特定の属性の敵にだけ吸血が有効」な属性システムとの連携
- 「最後に与えたダメージ量をUIに表示」して、吸血感を演出
例えば、「HPが50%未満のときは吸血量を2倍にする」改造案はこんな感じです。
private void ApplyBerserkBonus(ref float healAmount)
{
// HPが50%未満なら吸血量2倍のバーサーカーモード
if (selfHealth != null && selfHealth.CurrentHealth < selfHealth.MaxHealth * 0.5f)
{
healAmount *= 2f;
}
}
この関数を Lifesteal.OnAttackHit 内で、healAmount を確定する前に呼び出すだけで、
「瀕死になるほど吸血力が上がる」ビルドに簡単に拡張できます。
こういった小さなコンポーネントを積み重ねていくと、
巨大な God クラスに頼らない、保守しやすい Unity プロジェクトになっていきます。
ぜひ自分のプロジェクトでも、「攻撃ロジック」と「吸血ロジック」を分ける設計を試してみてください。
