Unityを触り始めた頃って、つい Update() の中に「移動」「入力」「HP管理」「ダメージ計算」「エフェクト再生」…と全部詰め込みがちですよね。
とくにRPGやアクションで「炎は1.5倍ダメージ」「氷は半減」みたいな属性耐性ロジックを、巨大なプレイヤークラスや敵クラスにベタ書きしてしまうと、
- 敵の種類が増えるたびに
if (element == ...)が増殖する - バランス調整のたびにコードを書き換える必要がある
- 「属性耐性」だけを別プロジェクトで再利用しづらい
といった問題が出てきます。
この記事では、「属性ダメージの倍率計算」だけに責務を絞った ElementAffinity コンポーネント を作って、
プレイヤーや敵キャラ、トラップなどにポン付けするだけで属性耐性を扱えるようにしてみます。
【Unity】弱点も半減も全部おまかせ!「ElementAffinity」コンポーネント
今回作る ElementAffinity は、
- 炎・氷・雷・風・闇・光などの「属性」を列挙型で定義
- 属性ごとの倍率(例: 炎1.5倍、氷0.5倍など)をインスペクター上で設定
- 「この属性でこのダメージが来たら、最終的にいくらにするか?」を計算するAPIを提供
という、非常にシンプルなコンポーネントです。
ダメージ計算そのもの(HPを減らす処理)は別コンポーネントに任せて、ElementAffinity は倍率計算だけに集中させます。
フルコード:ElementAffinity.cs
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>扱う属性の種類を定義する列挙型</summary>
public enum ElementType
{
None = 0,
Fire = 1,
Ice = 2,
Thunder = 3,
Wind = 4,
Light = 5,
Dark = 6,
}
/// <summary>
/// 1つの属性に対する耐性設定(倍率)
/// ScriptableObject ではなく、シンプルな Serializable クラスとして定義して
/// インスペクターから配列/リストで編集できるようにします。
/// </summary>
[Serializable]
public class ElementAffinityEntry
{
[Tooltip("どの属性に対する耐性か")]
public ElementType element = ElementType.None;
[Tooltip("この属性のダメージに掛ける倍率(1.0 = 等倍, 2.0 = 2倍, 0.5 = 半減, 0 = 無効)")]
[Range(0f, 4f)]
public float multiplier = 1f;
}
/// <summary>
/// 属性ダメージに対して倍率補正(弱点・半減・無効など)を計算するコンポーネント。
/// このコンポーネントは「ダメージの最終値を計算する」責務だけを持ち、
/// 実際に HP を減らす処理は別コンポーネントに任せる想定です。
/// </summary>
public class ElementAffinity : MonoBehaviour
{
[Header("属性ごとの耐性(倍率)設定")]
[Tooltip("ここに設定した属性だけ個別倍率が適用されます。未設定の属性はデフォルト倍率が使われます。")]
[SerializeField]
private List<ElementAffinityEntry> affinityEntries = new List<ElementAffinityEntry>();
[Header("未設定属性のデフォルト倍率")]
[Tooltip("上のリストに存在しない属性に対して使用する倍率(通常は 1.0 = 等倍)")]
[SerializeField]
[Range(0f, 4f)]
private float defaultMultiplier = 1f;
// 高速アクセス用の辞書(ランタイム時に構築)
private readonly Dictionary<ElementType, float> _affinityTable = new Dictionary<ElementType, float>();
/// <summary>
/// 初期化処理。インスペクターで設定されたリストから辞書を構築します。
/// </summary>
private void Awake()
{
BuildAffinityTable();
}
/// <summary>
/// インスペクターで編集した内容を辞書に反映する。
/// エディタ上で値を変更したときに即反映したい場合は、OnValidate も利用します。
/// </summary>
private void BuildAffinityTable()
{
_affinityTable.Clear();
// リストから辞書へコピー
foreach (var entry in affinityEntries)
{
// None はスキップしても良いですが、あえて登録しておいても問題ありません
if (!_affinityTable.ContainsKey(entry.element))
{
_affinityTable.Add(entry.element, Mathf.Max(0f, entry.multiplier));
}
else
{
// 同じ属性が複数ある場合は最後の設定を優先
_affinityTable[entry.element] = Mathf.Max(0f, entry.multiplier);
}
}
}
#if UNITY_EDITOR
/// <summary>
/// エディタ上で値を変更したときにも辞書を再構築しておくと、
/// Play 直後に最新の設定が反映されます。
/// 実行時ビルドには影響しません(UNITY_EDITOR のみ)。
/// </summary>
private void OnValidate()
{
BuildAffinityTable();
}
#endif
/// <summary>
/// 指定された属性ダメージに対して、倍率を掛けた「最終ダメージ」を返します。
/// 例: baseDamage=100, element=Fire, Fire倍率=1.5 → 150
/// </summary>
/// <param name="baseDamage">元のダメージ値(クリティカル等の前でも後でも、どのタイミングでもOK)</param>
/// <param name="element">このダメージの属性</param>
/// <returns>属性倍率が適用された最終ダメージ値</returns>
public float CalculateDamage(float baseDamage, ElementType element)
{
float multiplier = GetMultiplier(element);
return baseDamage * multiplier;
}
/// <summary>
/// 指定された属性に対する倍率を返します。
/// リストに存在しない属性は defaultMultiplier を返します。
/// </summary>
/// <param name="element">倍率を知りたい属性</param>
/// <returns>倍率(0 以上)</returns>
public float GetMultiplier(ElementType element)
{
if (_affinityTable.TryGetValue(element, out float value))
{
return Mathf.Max(0f, value);
}
// 未設定属性はデフォルト倍率
return Mathf.Max(0f, defaultMultiplier);
}
/// <summary>
/// ランタイム中に属性倍率を変更したい場合に使えるヘルパー。
/// 例: バフ・デバフで一時的に倍率を変えるなど。
/// </summary>
/// <param name="element">変更したい属性</param>
/// <param name="multiplier">新しい倍率(0 以上)</param>
public void SetMultiplier(ElementType element, float multiplier)
{
multiplier = Mathf.Max(0f, multiplier);
if (_affinityTable.ContainsKey(element))
{
_affinityTable[element] = multiplier;
}
else
{
_affinityTable.Add(element, multiplier);
}
// インスペクター上のリストも同期しておくと、デバッグしやすい
var entry = affinityEntries.Find(e => e.element == element);
if (entry != null)
{
entry.multiplier = multiplier;
}
else
{
affinityEntries.Add(new ElementAffinityEntry
{
element = element,
multiplier = multiplier
});
}
}
/// <summary>
/// 現在の倍率設定をデバッグ用に文字列化するヘルパー。
/// ログ出力や UI 表示などに利用できます。
/// </summary>
public string GetDebugDescription()
{
System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.AppendLine($"[ElementAffinity] Default: x{defaultMultiplier:F2}");
foreach (var kv in _affinityTable)
{
sb.AppendLine($" - {kv.Key}: x{kv.Value:F2}");
}
return sb.ToString();
}
}
使い方の手順
ここからは、実際に「プレイヤー」「敵」「炎トラップ」などで使う例を交えながら、手順①〜④で見ていきましょう。
手順①:スクリプトを用意する
- 上記の
ElementAffinity.csをプロジェクトのScriptsフォルダなどに保存します。 - Unity に戻ると自動でコンパイルされ、コンポーネントとして追加できるようになります。
手順②:プレイヤーや敵に ElementAffinity をアタッチ
例として、プレイヤーと敵キャラに属性耐性を持たせます。
- プレイヤーの GameObject(例:
Player)を選択 Add ComponentボタンからElementAffinityを追加- 同様に、敵キャラのプレハブ(例:
Enemy_Slime)にもElementAffinityを追加
インスペクター上で、たとえば次のように設定してみましょう。
- Player の ElementAffinity
- Default Multiplier:
1.0 - Affinity Entries:
- Fire:
0.5(炎に強い) - Ice:
1.5(氷が弱点)
- Fire:
- Default Multiplier:
- Enemy_Slime の ElementAffinity
- Default Multiplier:
1.0 - Affinity Entries:
- Fire:
2.0(炎に超弱い) - Ice:
0.25(氷にめちゃくちゃ強い)
- Fire:
- Default Multiplier:
これで、あとは「ダメージを与える側」が ElementAffinity.CalculateDamage() を呼ぶだけで、弱点・半減を自動計算してくれます。
手順③:ダメージ処理側から呼び出す(プレイヤー攻撃 → 敵がダメージを受ける例)
ここでは、敵側に Health コンポーネントを用意して、そこから ElementAffinity を使う例を示します。
「HP管理」と「属性計算」を別コンポーネントに分けることで、責務を分離しています。
using UnityEngine;
/// <summary>シンプルな HP 管理コンポーネント</summary>
[RequireComponent(typeof(ElementAffinity))]
public class Health : MonoBehaviour
{
[SerializeField]
private float maxHp = 100f;
[SerializeField]
private float currentHp = 100f;
// 同じ GameObject 上の ElementAffinity をキャッシュ
private ElementAffinity _elementAffinity;
private void Awake()
{
_elementAffinity = GetComponent<ElementAffinity>();
currentHp = maxHp;
}
/// <summary>
/// 属性付きダメージを受ける。
/// 外部(プレイヤーの攻撃スクリプトなど)から呼び出される想定。
/// </summary>
/// <param name="baseDamage">属性補正前のダメージ</param>
/// <param name="element">この攻撃の属性</param>
public void TakeDamage(float baseDamage, ElementType element)
{
// ElementAffinity に計算を委譲
float finalDamage = _elementAffinity.CalculateDamage(baseDamage, element);
currentHp -= finalDamage;
currentHp = Mathf.Max(0f, currentHp);
Debug.Log($"{gameObject.name} は {element} 属性で {finalDamage} ダメージを受けた!(残りHP: {currentHp})");
if (currentHp <= 0f)
{
Die();
}
}
private void Die()
{
Debug.Log($"{gameObject.name} は倒れた!");
// 実際のゲームではアニメーション再生や破壊処理などをここに書く
// Destroy(gameObject);
}
}
この Health コンポーネントを敵プレハブにアタッチしておけば、プレイヤー側の攻撃スクリプトからは
public class PlayerAttack : MonoBehaviour
{
[SerializeField]
private float attackPower = 20f;
[SerializeField]
private ElementType attackElement = ElementType.Fire;
private void OnTriggerEnter(Collider other)
{
// 攻撃判定に入った相手にダメージを与える例
var health = other.GetComponent<Health>();
if (health != null)
{
// 属性付きダメージを送る
health.TakeDamage(attackPower, attackElement);
}
}
}
のように、非常にシンプルな呼び出しで済みます。
プレイヤー側は「攻撃力」と「攻撃属性」だけを意識し、弱点・半減のロジックは一切知りません。
手順④:環境ギミックにも使う(炎の床トラップなど)
同じコンポーネントを、動く床やトラップにもそのまま使い回せます。
たとえば「炎の床に乗ると炎属性ダメージを継続的に受ける」ギミックは、次のように書けます。
using UnityEngine;
/// <summary>
/// 炎の床トラップ。
/// 触れている間、一定間隔で炎属性ダメージを与える。
/// </summary>
public class FireFloorTrap : MonoBehaviour
{
[SerializeField]
private float damagePerTick = 5f;
[SerializeField]
private float interval = 1f;
[SerializeField]
private ElementType element = ElementType.Fire;
private float _timer = 0f;
private void OnTriggerStay(Collider other)
{
_timer += Time.deltaTime;
if (_timer < interval) return;
_timer = 0f;
var health = other.GetComponent<Health>();
if (health != null)
{
health.TakeDamage(damagePerTick, element);
}
}
private void OnTriggerExit(Collider other)
{
// 対象が離れたらタイマーをリセットしておく
_timer = 0f;
}
}
プレイヤー・敵・トラップのどれもが、ElementAffinity と Health を共有しているので、
「炎に弱い敵だけめちゃくちゃ減る」「氷に強いプレイヤーは氷トラップが怖くない」といった表現が、レベルデザイン側の設定だけで簡単に実現できます。
メリットと応用
メリット①:プレハブ単位でバランス調整ができる
敵やプレイヤーの属性耐性を コードではなくインスペクターで設定 できるので、
- ゲームデザイナーが自分で倍率をいじれる
- 「このボスは炎に 1.2 倍だけど、雷には 0.8 倍」みたいな細かい調整も簡単
- 同じスクリプトを使い回しつつ、プレハブごとに違うキャラクター性を出せる
といったメリットがあります。
「属性耐性」という概念がコンポーネントとして独立しているので、プレハブを複製して倍率だけ変えるといった運用もやりやすいですね。
メリット②:Godクラス化を防ぎ、責務を分離できる
HP管理は Health、属性倍率は ElementAffinity、攻撃処理は PlayerAttack や FireFloorTrap といった具合に、
それぞれのコンポーネントが「1つのことだけ」を担当しています。
- 「属性ロジックだけ差し替えたい」ときに
ElementAffinityだけを編集すればよい - HPの仕様変更(例: シールド追加)は
Healthだけを変更すればよい - 攻撃の当たり判定の調整は攻撃スクリプト側だけを触ればよい
といった形で、影響範囲が小さくなり、開発がだいぶ楽になります。
メリット③:デバッグとログ出力がしやすい
GetDebugDescription() のようなヘルパーを用意しておけば、
var affinity = GetComponent<ElementAffinity>();
Debug.Log(affinity.GetDebugDescription());
とするだけで、現在の倍率設定をログに吐き出せます。
「なんかこの敵、炎ダメージ入りすぎじゃない?」というときにすぐ確認できるのは、運用上かなりありがたいです。
改造案:一時的なバフ・デバフを付与する
応用として、「一定時間だけ炎ダメージを半減するバフ」などを付与する処理を追加してみましょう。
以下は、ElementAffinity を持つオブジェクトに対して、一時的に倍率を変更するシンプルなサンプルです。
using System.Collections;
using UnityEngine;
/// <summary>
/// 一時的に特定の属性倍率を変更する簡易バフコンポーネント。
/// ElementAffinity と同じ GameObject に付けて使う想定。
/// </summary>
[RequireComponent(typeof(ElementAffinity))]
public class TemporaryElementBuff : MonoBehaviour
{
[SerializeField]
private ElementType targetElement = ElementType.Fire;
[SerializeField]
private float buffMultiplier = 0.5f; // 例: 炎ダメージ半減
[SerializeField]
private float duration = 5f; // バフの持続時間
private ElementAffinity _affinity;
private float _originalMultiplier;
private void Awake()
{
_affinity = GetComponent<ElementAffinity>();
}
public void ApplyBuff()
{
StopAllCoroutines();
StartCoroutine(BuffRoutine());
}
private IEnumerator BuffRoutine()
{
// 元の倍率を保存
_originalMultiplier = _affinity.GetMultiplier(targetElement);
// 新しい倍率を適用
_affinity.SetMultiplier(targetElement, buffMultiplier);
yield return new WaitForSeconds(duration);
// 元に戻す
_affinity.SetMultiplier(targetElement, _originalMultiplier);
}
}
このコンポーネントをプレイヤーに付けておけば、
var buff = player.GetComponent<TemporaryElementBuff>();
buff.ApplyBuff();
と呼ぶだけで、「5秒間だけ炎ダメージ半減」といった効果を実現できます。
このように、ElementAffinity を土台にして小さなコンポーネントを積み上げていくと、ゲームの拡張性と保守性がぐっと上がります。
属性システムは複雑になりがちですが、まずは「倍率計算」をコンポーネントとして切り出すところから始めてみると、
だいぶコードがスッキリしてくるはずです。ぜひ自分のプロジェクトでも試してみてください。
