Unityを触り始めた頃って、つい Update() に「移動」「入力」「HP管理」「スタミナ管理」など、全部まとめて書いてしまいがちですよね。最初は動くので満足できますが、機能が増えるほど
- どこで何をしているのか分からない
- ちょっとした仕様変更で大量の修正が必要になる
- 別キャラや別シーンで同じ処理を使い回しにくい
といった問題が一気に噴き出してきます。
そこでおすすめなのが、「スタミナ管理だけを担当するコンポーネント」を切り出してしまうやり方です。
今回紹介する 「StaminaBar」コンポーネント は、
- ダッシュや回避などのアクションでスタミナを消費する
- 一定時間アクションをしなければ自動回復する
- スタミナが足りないときはアクションをブロックする
といった「スタミナ管理」の責務だけに絞ったシンプルなスクリプトです。
プレイヤーでも敵でも、スタミナを使うキャラにポン付けして再利用できるようにしていきましょう。
【Unity】アクションが気持ちよく回るスタミナ制御!「StaminaBar」コンポーネント
フルコード(StaminaBar.cs)
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// スタミナの増減と自動回復を管理するコンポーネント。
/// - アクション側から「消費したい」ときに RequestConsume を呼ぶ
/// - スタミナが足りなければ false を返す(アクション不可)
/// - 一定時間入力がなければ自動で回復を開始
/// - UIなどはイベント(OnStaminaChanged)を購読して更新する
///
/// 責務を「スタミナの数値管理」に限定することで、
/// 入力・移動・攻撃などの処理ときれいに分離できます。
/// </summary>
public class StaminaBar : MonoBehaviour
{
[Header("スタミナ基本設定")]
[SerializeField]
private float maxStamina = 100f; // スタミナの最大値
[SerializeField]
private float initialStamina = 100f; // 開始時のスタミナ
[SerializeField]
private bool startFull = true; // 開始時に自動で最大にするか
[Header("自動回復設定")]
[SerializeField]
private float regenRate = 15f; // 1秒あたりの回復量
[SerializeField]
private float regenDelay = 1.0f; // 最後の消費から回復を始めるまでの待ち時間(秒)
[SerializeField]
private bool canRegenWhileMoving = true; // 移動中も回復してよいか(例:ダッシュ中は false など)
[Header("制限設定")]
[SerializeField]
private bool clampToZero = true; // 0未満にしない
[SerializeField]
private bool clampToMax = true; // 最大値を超えない
[Header("イベント")]
[SerializeField]
private UnityEvent<float, float> onStaminaChanged;
// 引数: current, max。UIスライダーなどから購読する
[SerializeField]
private UnityEvent onStaminaDepleted; // スタミナがゼロになった瞬間に発火
[SerializeField]
private UnityEvent onStaminaRecoveredFromZero; // ゼロ状態から回復し始めた瞬間に発火
/// <summary>現在のスタミナ値(読み取り専用プロパティ)</summary>
public float CurrentStamina => currentStamina;
/// <summary>最大スタミナ値(読み取り専用プロパティ)</summary>
public float MaxStamina => maxStamina;
/// <summary>現在のスタミナ割合(0〜1)</summary>
public float NormalizedStamina => maxStamina > 0f ? currentStamina / maxStamina : 0f;
// 実際のスタミナ値
private float currentStamina;
// 最後にスタミナを消費した時間
private float lastConsumeTime;
// 直前のフレームでゼロだったかどうかを記録(イベント用)
private bool wasStaminaZero = false;
// 現在「移動中」とみなすかどうか(外部から設定)
private bool isMoving = false;
private void Awake()
{
// 初期スタミナの決定
if (startFull)
{
currentStamina = maxStamina;
}
else
{
currentStamina = Mathf.Clamp(initialStamina, 0f, maxStamina);
}
lastConsumeTime = -999f; // かなり過去に消費したことにしておく
wasStaminaZero = currentStamina <= 0f;
// 初期値をイベントで通知(UI初期化用)
RaiseStaminaChangedEvent();
}
private void Update()
{
HandleRegeneration(Time.deltaTime);
}
/// <summary>
/// 外部から「スタミナを消費したい」ときに呼ぶ関数。
/// 例:ダッシュ開始時、回避入力時など。
///
/// <returns>
/// 消費に成功したら true(アクション実行OK)、
/// 足りなければ false(アクション禁止)を返します。
/// </returns>
public bool RequestConsume(float amount)
{
if (amount <= 0f)
{
// 0以下の値を渡されたら何もしない
return true;
}
// スタミナが足りるかチェック
if (currentStamina < amount)
{
// 足りないのでアクション不可
return false;
}
// 消費処理
currentStamina -= amount;
// 必要に応じてクランプ
if (clampToZero && currentStamina < 0f)
{
currentStamina = 0f;
}
// 「最後に消費した時間」を更新(自動回復の遅延に使用)
lastConsumeTime = Time.time;
// ゼロになったタイミングでイベント発火
if (!wasStaminaZero && currentStamina <= 0f)
{
wasStaminaZero = true;
onStaminaDepleted?.Invoke();
}
// 変更をイベントで通知
RaiseStaminaChangedEvent();
return true;
}
/// <summary>
/// 外部から強制的にスタミナを回復させたいときに使う関数。
/// 例:アイテム使用、チェックポイント到達など。
/// </summary>
public void AddStamina(float amount)
{
if (amount <= 0f)
{
return;
}
float previous = currentStamina;
currentStamina += amount;
if (clampToMax)
{
currentStamina = Mathf.Min(currentStamina, maxStamina);
}
// ゼロ状態から回復した瞬間を検知
if (previous <= 0f && currentStamina > 0f)
{
wasStaminaZero = false;
onStaminaRecoveredFromZero?.Invoke();
}
RaiseStaminaChangedEvent();
}
/// <summary>
/// スタミナを完全回復させるヘルパー関数。
/// </summary>
public void Refill()
{
float previous = currentStamina;
currentStamina = maxStamina;
if (previous <= 0f && currentStamina > 0f)
{
wasStaminaZero = false;
onStaminaRecoveredFromZero?.Invoke();
}
RaiseStaminaChangedEvent();
}
/// <summary>
/// プレイヤーやAI側から「今は移動中かどうか」を通知するための関数。
/// 自動回復のON/OFFに使います。
/// 例:ダッシュ中は true、止まっているときは false など。
/// </summary>
public void SetMovingState(bool moving)
{
isMoving = moving;
}
/// <summary>
/// 現在のスタミナが指定量以上あるかをチェックする。
/// 実際に消費はしないので、UIの点滅やボタンの有効/無効の判定に便利です。
/// </summary>
public bool HasEnough(float amount)
{
return currentStamina >= amount;
}
/// <summary>
/// 自動回復の処理本体。
/// Update から毎フレーム呼び出されます。
/// </summary>
private void HandleRegeneration(float deltaTime)
{
if (regenRate <= 0f)
{
return; // 回復量0なら何もしない
}
// 移動中は回復させたくない場合
if (!canRegenWhileMoving && isMoving)
{
return;
}
// 最後の消費から一定時間経過していなければ回復しない
if (Time.time - lastConsumeTime < regenDelay)
{
return;
}
if (currentStamina >= maxStamina)
{
return; // すでに満タン
}
float previous = currentStamina;
// 経過時間に応じて回復
currentStamina += regenRate * deltaTime;
if (clampToMax && currentStamina > maxStamina)
{
currentStamina = maxStamina;
}
// ゼロ状態から回復した瞬間を検知
if (previous <= 0f && currentStamina > 0f)
{
wasStaminaZero = false;
onStaminaRecoveredFromZero?.Invoke();
}
// 値が変わっていればイベント通知
if (!Mathf.Approximately(previous, currentStamina))
{
RaiseStaminaChangedEvent();
}
}
/// <summary>
/// イベントを通じてスタミナ値の変化を通知する。
/// UIや他コンポーネントはここを購読するだけでよい。
/// </summary>
private void RaiseStaminaChangedEvent()
{
onStaminaChanged?.Invoke(currentStamina, maxStamina);
}
#if UNITY_EDITOR
// エディタ上で値を変更したときに制約をかける
private void OnValidate()
{
if (maxStamina < 0f) maxStamina = 0f;
if (regenRate < 0f) regenRate = 0f;
if (regenDelay < 0f) regenDelay = 0f;
// 初期値も最大値の範囲に収める
initialStamina = Mathf.Clamp(initialStamina, 0f, maxStamina);
}
#endif
}
使い方の手順
ここでは「プレイヤーのダッシュにスタミナを使う」例で説明します。敵AIやロール回避、強攻撃などにもほぼ同じ手順で使えます。
-
コンポーネントを用意する
- プロジェクトの
ScriptsフォルダなどにStaminaBar.csを作成し、上記コードをコピペして保存します。 - Unityエディタに戻ると自動でコンパイルされます。
- プロジェクトの
-
プレイヤーにアタッチする
- シーン内のプレイヤーオブジェクト(例:
Player)を選択。 Add ComponentボタンからStaminaBarを追加します。- インスペクタで以下を調整します:
Max Stamina:例 100Regen Rate:例 15(1秒あたり15回復)Regen Delay:例 1(1秒間何もしなければ回復開始)Can Regen While Moving:ダッシュ中も回復したくなければオフにして、後述のSetMovingStateを使います。
- シーン内のプレイヤーオブジェクト(例:
-
アクション側からスタミナを消費する
例として、プレイヤーのダッシュスクリプトからスタミナを消費してみます。
プレイヤーにこんなコンポーネントを追加してみてください(簡易版です):using UnityEngine; /// <summary> /// 非常にシンプルなダッシュ例。 /// StaminaBar にスタミナ消費を依頼し、 /// 足りなければダッシュしない。 /// </summary> [RequireComponent(typeof(CharacterController))] [RequireComponent(typeof(StaminaBar))] public class SimpleDashController : MonoBehaviour { [SerializeField] private float walkSpeed = 3f; [SerializeField] private float dashSpeed = 8f; [SerializeField] private float dashStaminaCost = 25f; // ダッシュ1回あたりの消費量 private CharacterController characterController; private StaminaBar staminaBar; private void Awake() { characterController = GetComponent<CharacterController>(); staminaBar = GetComponent<StaminaBar>(); } private void Update() { // 簡易な移動入力(WASD) float h = Input.GetAxisRaw("Horizontal"); float v = Input.GetAxisRaw("Vertical"); Vector3 inputDir = new Vector3(h, 0f, v).normalized; bool isMoving = inputDir.sqrMagnitude > 0.01f; // StaminaBar に「移動中かどうか」を通知 staminaBar.SetMovingState(isMoving); // 左Shiftでダッシュを試みる bool dashRequested = Input.GetKeyDown(KeyCode.LeftShift); float speed = walkSpeed; if (dashRequested) { // スタミナ消費をリクエスト bool canDash = staminaBar.RequestConsume(dashStaminaCost); if (canDash) { // スタミナが足りたので、このフレームだけダッシュ速度にする speed = dashSpeed; } } // 通常移動(重力などは簡略化) Vector3 velocity = inputDir * speed; characterController.SimpleMove(velocity); } }- この例では、左Shiftを押した瞬間だけスタミナを25消費し、ダッシュ速度になります。
- スタミナが足りないと
RequestConsumeがfalseを返すので、その場合はダッシュしません。
-
UIスライダーでスタミナを可視化する
スタミナバーを画面に表示したい場合は、UIスライダーとイベントをつなぐだけでOKです。GameObject > UI > Sliderでスライダーを作成し、名前をStaminaSliderに変更。- 空のオブジェクト
StaminaUIを作成し、次のスクリプトをアタッチします。
using UnityEngine; using UnityEngine.UI; /// <summary> /// StaminaBar のイベントを受けて UI.Slider を更新するだけのコンポーネント。 /// UI専用の責務に絞ることで、ロジックと見た目を分離しています。 /// </summary> public class StaminaSliderView : MonoBehaviour { [SerializeField] private StaminaBar targetStaminaBar; [SerializeField] private Slider slider; private void Awake() { if (slider == null) { slider = GetComponentInChildren<Slider>(); } } private void OnEnable() { if (targetStaminaBar != null) { // イベント購読を設定(インスペクタからも設定可能) // ここではスクリプトから登録する例を示します。 // targetStaminaBar の OnStaminaChanged に、 // この UpdateSlider メソッドを紐付けてください(インスペクタでも可)。 } } /// <summary> /// StaminaBar の OnStaminaChanged(current, max)から呼び出される想定の関数。 /// </summary> public void UpdateSlider(float current, float max) { if (slider == null) return; slider.maxValue = max; slider.value = current; } }あとはインスペクタで以下を設定します。
StaminaUIのTarget Stamina BarにプレイヤーのStaminaBarをドラッグ。SliderフィールドにStaminaSliderをドラッグ。- プレイヤーの
StaminaBarコンポーネントのOn Stamina ChangedイベントにStaminaUIを追加し、
関数としてStaminaSliderView.UpdateSliderを指定します。
これでスタミナが変化するたびにスライダーが自動更新されます。
メリットと応用
StaminaBar を「スタミナ数値の管理だけ」に責務を絞っておくと、プレハブ管理やレベルデザインがかなり楽になります。
- プレイヤー、敵、ギミックで使い回せる
プレイヤーだけでなく、スタミナ制限付きの敵(連続攻撃すると一時的に疲れるボスなど)にも同じコンポーネントをポン付けできます。
行動ロジックは別コンポーネントに書き、スタミナが足りるかどうかだけHasEnough()で問い合わせればOKです。 - 難易度調整がインスペクタだけで完結する
Max StaminaやRegen Rateを変えるだけで、「短距離全力ダッシュ型」「長距離ゆるやかダッシュ型」などのキャラバリエーションを簡単に作れます。
プレハブを複製してパラメータだけ変える運用もしやすいです。 - UIとロジックを完全に分離できる
数値管理はStaminaBar、描画はStaminaSliderViewのように役割を分けておくと、
「スライダーを円形ゲージに変えたい」「色を変えたい」といったUI変更時にロジック側を一切触らなくて済みます。
応用として、スタミナがゼロのときだけ画面を暗くしたり、キャラのアニメーションレイヤーを切り替えたりするのも簡単です。
OnStaminaDepleted / OnStaminaRecoveredFromZero イベントに別コンポーネントを紐付けるだけで、「疲労状態」の表現を追加できます。
改造案:スタミナが足りないときにUIを点滅させる
例えば、「スタミナが足りなくてアクションできなかったときに、スタミナバーを赤く点滅させる」関数を追加するのもアリですね。
以下は UI 側コンポーネントに追加するイメージの関数です。
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// スタミナ不足を視覚的にフィードバックする簡易エフェクト。
/// </summary>
public class StaminaBlinkEffect : MonoBehaviour
{
[SerializeField]
private Image barImage;
[SerializeField]
private Color blinkColor = Color.red;
[SerializeField]
private float blinkDuration = 0.2f;
private Color originalColor;
private float blinkTimer;
private void Awake()
{
if (barImage == null)
{
barImage = GetComponentInChildren<Image>();
}
if (barImage != null)
{
originalColor = barImage.color;
}
}
private void Update()
{
if (blinkTimer > 0f && barImage != null)
{
blinkTimer -= Time.deltaTime;
float t = Mathf.Clamp01(blinkTimer / blinkDuration);
// 点滅から元の色に戻る
barImage.color = Color.Lerp(originalColor, blinkColor, t);
if (blinkTimer <= 0f)
{
barImage.color = originalColor;
}
}
}
/// <summary>
/// スタミナ不足時に呼び出すことで、バーを一瞬点滅させる。
/// 例:アクション側で RequestConsume が false を返したときに呼ぶ。
/// </summary>
public void PlayBlink()
{
blinkTimer = blinkDuration;
if (barImage != null)
{
barImage.color = blinkColor;
}
}
}
アクション側(例:SimpleDashController)で RequestConsume が false を返したときに PlayBlink() を呼べば、「スタミナが足りない」というフィードバックを簡単に追加できます。
このように、スタミナの「数値管理」と「演出」を小さなコンポーネントに分けていくと、プロジェクトが大きくなっても破綻しにくくなります。




