Unityの初心者~中級者でありがちなのが、Update 関数に「移動処理」「入力処理」「HP管理」「MP管理」「アニメーション制御」など、ありとあらゆる処理を書いてしまうパターンです。
動き始めは良くても、時間が経つにつれて1つのスクリプトが肥大化し、バグの原因がどこなのか分からなくなったり、別のキャラクターに流用しづらくなったりしますよね。
この記事では「MPの自動回復」というよくある仕組みを、単機能のコンポーネントとして独立させた 「ManaRecharge」コンポーネント を作ってみます。
MP管理を1つのコンポーネントに閉じ込めておけば、プレイヤーにも敵にも、ボスにも、NPCにも、同じ仕組みをポン付けで使い回せて便利です。
【Unity】放っておくだけでMPが戻る!「ManaRecharge」コンポーネント
ここでは、
- 最大MPと現在MPを管理する
- 指定した秒数ごとに、指定量だけMPを自動回復する
- MPの増減を外部から安全に操作できるAPIを用意する
- UI(スライダーなど)と簡単に連携できるイベントを用意する
といった要件を満たす ManaRecharge コンポーネントを作成します。
フルコード:ManaRecharge.cs
using System;
using UnityEngine;
using UnityEngine.Events;
namespace Sample.Mana
{
/// <summary>
/// MP(マナ)を時間経過で自動回復させるコンポーネント。
/// ・最大MPと現在MPを管理
/// ・一定間隔または毎秒で自動回復
/// ・外部から安全に消費/回復するためのメソッドを提供
/// ・UI更新用のイベントを発行
///
/// ※このコンポーネント単体で動作します。
/// </summary>
public class ManaRecharge : MonoBehaviour
{
// ==== 設定値(インスペクターから調整) ====
[Header("MP 基本設定")]
[Tooltip("最大MP")]
[SerializeField] private float maxMana = 100f;
[Tooltip("開始時のMP(-1以下なら最大値で開始)")]
[SerializeField] private float startMana = -1f;
[Header("自動回復設定")]
[Tooltip("自動回復を有効にするか")]
[SerializeField] private bool autoRecharge = true;
[Tooltip("1秒あたりの自動回復量(連続回復モード)")]
[SerializeField] private float rechargePerSecond = 5f;
[Tooltip("一定間隔で回復する場合の回復量(intervalごと)")]
[SerializeField] private float rechargeAmountPerTick = 10f;
[Tooltip("一定間隔で回復する場合の間隔秒数")]
[SerializeField] private float rechargeInterval = 1f;
[Tooltip("true の場合:毎フレーム少しずつ回復 / false の場合:intervalごとにまとめて回復")]
[SerializeField] private bool continuousRecharge = true;
[Header("回復開始のディレイ")]
[Tooltip("最後にMPを消費してから、何秒後に自動回復を再開するか")]
[SerializeField] private float rechargeDelayAfterUse = 1.5f;
[Header("制限設定")]
[Tooltip("自動回復を一時停止するか(外部から制御用)")]
[SerializeField] private bool pauseAutoRecharge = false;
// ==== イベント ====
[Header("イベント")]
[Tooltip("MPが変化したときに呼ばれる(現在値, 最大値)")]
[SerializeField] private UnityEvent<float, float> onManaChanged;
[Tooltip("MPが枯渇したときに呼ばれる")]
[SerializeField] private UnityEvent onManaDepleted;
[Tooltip("MPが満タンになったときに呼ばれる")]
[SerializeField] private UnityEvent onManaFull;
// ==== 内部状態 ====
/// <summary>現在のMP(外部からは読み取り専用)</summary>
public float CurrentMana => currentMana;
/// <summary>最大MP(読み取り専用)</summary>
public float MaxMana => maxMana;
// 実際のMP値
private float currentMana;
// 最後にMPを消費した時間
private float lastUseTime;
// ==== ライフサイクル ====
private void Awake()
{
// 開始時のMPを決定
if (startMana < 0f)
{
currentMana = maxMana; // デフォルトは最大値スタート
}
else
{
currentMana = Mathf.Clamp(startMana, 0f, maxMana);
}
lastUseTime = -9999f; // かなり昔に消費した扱いにしておく
}
private void Start()
{
// 初期値をイベントで通知(UIに反映したい場合など)
InvokeManaChangedEvent();
}
private void Update()
{
if (!autoRecharge) return;
if (pauseAutoRecharge) return;
// MPがすでに満タンなら何もしない
if (currentMana >= maxMana) return;
// 最後に消費してからディレイ時間が経過しているかチェック
if (Time.time - lastUseTime < rechargeDelayAfterUse) return;
if (continuousRecharge)
{
// 毎秒一定量回復(フレームごとに少しずつ)
float delta = rechargePerSecond * Time.deltaTime;
if (delta > 0f)
{
AddMana(delta);
}
}
else
{
// 一定間隔ごとにまとめて回復
// intervalの倍数をまたいだタイミングで回復させる
// (簡易実装:Time.time を使った剰余判定)
// ※より厳密にしたい場合は、内部にタイマーを持つ方式に差し替えてください。
if (rechargeInterval > 0f)
{
float t = Time.time - lastUseTime - rechargeDelayAfterUse;
// intervalの境目(誤差許容)で回復
float mod = t % rechargeInterval;
// modが前フレームより小さくなった=intervalをまたいだとみなす
// 前フレームのmodを保持する必要があるため、内部変数を追加
}
}
// 上記の「一定間隔ごとの回復」をもう少し分かりやすく書くために、
// 別のタイマー方式で実装し直します。
}
// --- ここから下で interval タイマーを実装し直す ---
// 一定間隔回復用のタイマー
private float rechargeTimer = 0f;
private void LateUpdate()
{
if (!autoRecharge) return;
if (pauseAutoRecharge) return;
if (!continuousRecharge)
{
if (currentMana >= maxMana) return;
// 最後の消費からの経過時間がディレイを超えているか
float elapsedSinceUse = Time.time - lastUseTime;
if (elapsedSinceUse < rechargeDelayAfterUse) return;
// ディレイを過ぎたら interval タイマーを進める
rechargeTimer += Time.deltaTime;
if (rechargeTimer >= rechargeInterval && rechargeInterval > 0f)
{
rechargeTimer -= rechargeInterval;
if (rechargeAmountPerTick > 0f)
{
AddMana(rechargeAmountPerTick);
}
}
}
}
// ==== 公開メソッド(外部からの操作用API) ====
/// <summary>
/// MPを消費する。消費に成功したかどうかを返す。
/// </summary>
/// <param name="amount">消費したいMP量(正の値)</param>
/// <param name="allowOverConsume">
/// true の場合:現在MPが足りなくても0まで消費する(例:必ず撃てるが、足りない分は消費されない)。<br/>
/// false の場合:現在MPが足りないときは何もせず false を返す。
/// </param>
public bool ConsumeMana(float amount, bool allowOverConsume = false)
{
if (amount <= 0f) return false;
// 現在MPが足りない場合
if (currentMana < amount)
{
if (!allowOverConsume)
{
// 消費失敗
return false;
}
}
float before = currentMana;
// allowOverConsume = true の場合は、0 まで下げる
currentMana = Mathf.Max(0f, currentMana - amount);
// 消費した時刻を記録(自動回復ディレイのため)
lastUseTime = Time.time;
// interval用タイマーもリセット
rechargeTimer = 0f;
// イベント通知
InvokeManaChangedEvent();
// 枯渇したらイベント発火
if (before > 0f && currentMana <= 0f)
{
onManaDepleted?.Invoke();
}
return true;
}
/// <summary>
/// MPを回復させる。自動回復からも呼ばれる。
/// </summary>
/// <param name="amount">回復量(正の値)</param>
public void AddMana(float amount)
{
if (amount <= 0f) return;
float before = currentMana;
currentMana = Mathf.Clamp(currentMana + amount, 0f, maxMana);
// 変化があったときのみイベント通知
if (!Mathf.Approximately(before, currentMana))
{
InvokeManaChangedEvent();
}
// 満タンになった瞬間にイベント発火
if (before < maxMana && currentMana >= maxMana)
{
onManaFull?.Invoke();
}
}
/// <summary>
/// MPを最大値にリセットする。
/// </summary>
public void RestoreFull()
{
float before = currentMana;
currentMana = maxMana;
InvokeManaChangedEvent();
if (before < maxMana)
{
onManaFull?.Invoke();
}
}
/// <summary>
/// 自動回復を一時停止する。
/// </summary>
public void PauseAutoRecharge()
{
pauseAutoRecharge = true;
}
/// <summary>
/// 自動回復を再開する。
/// </summary>
public void ResumeAutoRecharge()
{
pauseAutoRecharge = false;
}
/// <summary>
/// 最大MPを変更し、現在MPの上限も調整する。
/// </summary>
public void SetMaxMana(float newMax, bool keepRatio = true)
{
if (newMax <= 0f) return;
if (keepRatio && maxMana > 0f)
{
// 現在の割合を維持したまま最大値を変更
float ratio = currentMana / maxMana;
maxMana = newMax;
currentMana = Mathf.Clamp(newMax * ratio, 0f, maxMana);
}
else
{
// 現在値はそのまま or はみ出た分はClamp
maxMana = newMax;
currentMana = Mathf.Clamp(currentMana, 0f, maxMana);
}
InvokeManaChangedEvent();
}
// ==== 内部メソッド ====
/// <summary>
/// MP変化イベントを呼び出すヘルパー
/// </summary>
private void InvokeManaChangedEvent()
{
onManaChanged?.Invoke(currentMana, maxMana);
}
}
}
上記はそのまま ManaRecharge.cs としてプロジェクトに追加すれば動作します。
UnityEvent を使っているので、UIスライダーやテキストと簡単に連携できます。
使い方の手順
ここでは、プレイヤーキャラにMP自動回復を付ける例をメインに、敵やギミックへの応用も含めて手順を説明します。
① スクリプトをプロジェクトに追加する
Assetsフォルダ内にScriptsフォルダを作成(任意)。- その中に
ManaRecharge.csという名前で新規C#スクリプトを作成。 - エディタで開き、上のコードをまるごとコピペして保存。
② プレイヤーに ManaRecharge コンポーネントをアタッチ
- ヒエラルキーでプレイヤーの GameObject を選択。
Add ComponentボタンからManaRechargeを検索して追加。- インスペクターで以下の値を設定してみましょう:
- Max Mana:100
- Start Mana:-1(最大値で開始)
- Auto Recharge:チェックON
- Continuous Recharge:チェックON(毎秒回復モード)
- Recharge Per Second:5(1秒あたり5回復)
- Recharge Delay After Use:1.5(消費後1.5秒経過すると回復開始)
③ スキル発動スクリプトから MP を消費する
プレイヤーのスキル発動処理などから、ManaRecharge.ConsumeMana を呼び出してMPを減らします。
Unity6の新Input Systemなどから呼び出すイメージで、シンプルなサンプルを示します。
using UnityEngine;
using Sample.Mana; // ManaRecharge の namespace
/// <summary>
/// 非常にシンプルなスキル発動サンプル。
/// キー入力でスキルを撃ち、ManaRecharge からMPを消費する。
/// </summary>
public class SimpleSkillCaster : MonoBehaviour
{
[SerializeField] private ManaRecharge mana; // 同じオブジェクトか、別オブジェクトの ManaRecharge を参照
[SerializeField] private float skillCost = 20f;
private void Reset()
{
// 同じオブジェクト上から自動取得してみる
if (mana == null)
{
mana = GetComponent<ManaRecharge>();
}
}
private void Update()
{
// 例としてスペースキーでスキル発動
if (Input.GetKeyDown(KeyCode.Space))
{
TryCastSkill();
}
}
private void TryCastSkill()
{
// MPが足りなければスキル不発
bool success = mana.ConsumeMana(skillCost, allowOverConsume: false);
if (success)
{
Debug.Log("スキル発動! MP消費: " + skillCost);
// 実際のスキル処理(エフェクト生成など)をここに書く
}
else
{
Debug.Log("MPが足りません!");
}
}
}
このように、スキル発動側は「MPの値」を直接いじらず、コンポーネントのAPIだけを呼ぶ形にすると、責務が分離されてスッキリします。
④ UIスライダーと連携してMPバーを表示する
MPバーを Slider で表示する例です。
まずは簡単なスクリプトを用意します。
using UnityEngine;
using UnityEngine.UI;
using Sample.Mana;
/// <summary>
/// ManaRecharge の onManaChanged イベントを受けて、
/// Slider UI を更新するコンポーネント。
/// </summary>
public class ManaSliderView : MonoBehaviour
{
[SerializeField] private ManaRecharge mana;
[SerializeField] private Slider slider;
private void Reset()
{
// 同じオブジェクト上の Slider を自動取得
if (slider == null)
{
slider = GetComponent<Slider>();
}
}
/// <summary>
/// ManaRecharge から呼ばれるイベント用メソッド。
/// インスペクターの UnityEvent から紐づけて使う。
/// </summary>
public void OnManaChanged(float current, float max)
{
if (slider == null) return;
slider.maxValue = max;
slider.value = current;
}
}
セットアップ手順は以下の通りです。
- Canvas 上に Slider を配置。
- Slider オブジェクトに
ManaSliderViewをアタッチ。 ManaSliderViewの Mana に、プレイヤーのManaRechargeをドラッグ&ドロップ。- プレイヤーの
ManaRechargeコンポーネントの On Mana Changed イベント欄で、+ボタンを押す。- Slider オブジェクト(ManaSliderView が付いている)をドラッグ。
- 関数に
ManaSliderView.OnManaChangedを指定。
これで、MPが変化するたびにスライダーが自動で更新されるようになります。
プレイヤーだけでなく、ボスのMPバーや、チャージ中のギミックのエネルギー残量バーなどにも流用できます。
メリットと応用
ManaRecharge をコンポーネントとして切り出すメリットはかなり大きいです。
- プレハブ化がしやすい
プレイヤー、敵、召喚獣、タレット、トラップなど、
「リソースを使って何かをするオブジェクト」にそのままアタッチして使い回せます。
MPの最大値や回復速度だけインスペクターで変えれば、簡単に個性を出せます。 - レベルデザインが楽になる
ステージごとに「このエリアはMPが回復しやすい」「ボス戦は回復が遅い」など、
rechargePerSecondやrechargeDelayAfterUseを調整するだけでバランス調整が可能です。
ステージ専用のスクリプトを増やさず、パラメータ調整だけで雰囲気を変えられます。 - Godクラス化を防げる
「プレイヤー制御スクリプト」がMPの増減ロジックまで抱え込まず、
ただmana.ConsumeMana()を呼ぶだけで済むため、責務が明確になります。 - UIとの連携が簡単
UnityEventで現在値と最大値を投げているので、スライダーでもテキストでも、
さらにはアニメーション用のスクリプトでも、イベント受信側を差し替えるだけでOKです。
また、MP以外の用途にも応用できます。
- スタミナ(ダッシュや回避に使用)
- 銃の弾数(時間で弾が補充される武器)
- シールドエネルギー(ダメージを受けなければ自動回復するバリア)
どれも「現在値」「最大値」「自動回復」という概念は同じなので、
クラス名と表示名だけ変えればそのまま使い回せるはずです。
改造案:HPと連動してMP上限を変える
応用として、「HPが減ると最大MPも減る」ようなローグライク風の仕様を追加したい場合、ManaRecharge の外側に小さなコンポーネントを作るのがおすすめです。
例えばこんな感じです。
using UnityEngine;
using Sample.Mana;
/// <summary>
/// HP割合に応じて ManaRecharge の最大MPを変動させる例。
/// HPコンポーネント(仮)から現在HPと最大HPを受け取る想定。
/// </summary>
public class ManaMaxByHpRatio : MonoBehaviour
{
[SerializeField] private ManaRecharge mana;
[SerializeField] private float baseMaxMana = 100f;
/// <summary>
/// HPコンポーネント側の「HPが変わった」イベントから呼ばれる想定のメソッド。
/// </summary>
public void OnHpChanged(float currentHp, float maxHp)
{
if (mana == null || maxHp <= 0f) return;
float ratio = Mathf.Clamp01(currentHp / maxHp);
float newMax = baseMaxMana * ratio;
// 比率維持オプションは false にして、現在MPをそのままにする
mana.SetMaxMana(newMax, keepRatio: false);
}
}
このように、「MP自動回復」という1つの責務を ManaRecharge に閉じ込めておくことで、
周辺の仕様(HPとの連動、ステージギミックとの連動など)は別コンポーネントとして小さく積み上げていけます。
結果として、保守しやすく拡張しやすいプロジェクトになっていきますね。
