【Cocos Creator】アタッチするだけ!StaminaBar (スタミナ管理)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】StaminaBar の実装:アタッチするだけで「スタミナの消費&自動回復」を実現する汎用スクリプト

アクションゲームやダッシュ機能のあるゲームでは、「スタミナ(行動力)」の管理はほぼ必須です。この記事では、任意のノードにアタッチするだけで、スタミナの減少・自動回復・閾値チェックまで完結する汎用コンポーネント「StaminaBar」を実装します。

外部の GameManager などには一切依存せず、すべてインスペクタで設定可能な設計にすることで、どのシーン・どのプロジェクトでもすぐに再利用できる形を目指します。


コンポーネントの設計方針

1. 機能要件の整理

StaminaBar コンポーネントで実現する機能は次の通りです。

  • スタミナ値の管理
    • 最大スタミナ値(maxStamina)
    • 現在スタミナ値(currentStamina)
    • 0〜最大値の範囲で自動クランプ
  • スタミナの自動回復
    • 毎秒回復量(recoveryPerSecond)
    • 一定時間アクションが行われなかった場合のみ回復開始(回復待機時間)
    • 自動回復の ON/OFF 切り替え
  • スタミナの消費
    • 外部から consume(amount: number) を呼ぶとスタミナを減少
    • スタミナが足りない場合は消費を拒否し false を返す
    • 消費成功時に「最後に消費した時間」を更新し、回復を一時停止
  • 各種しきい値・状態チェック
    • 「この割合を下回ったら低スタミナ」とみなす閾値(lowStaminaThreshold)
    • 現在値・割合・空かどうか・満タンかどうかなどを取得するメソッド
  • デバッグ・開発補助
    • エディタ上で現在スタミナを確認・リセットしやすいようにする
    • ログ出力の ON/OFF(動作確認用)

UI(ゲージ表示など)は一切含まず、「数値としてのスタミナ管理」だけに責務を絞ることで、どんなゲームにも組み込みやすい汎用コンポーネントにします。ゲージ表示は、必要に応じて別コンポーネントから getComponent(StaminaBar) で参照し、getNormalizedStamina() を使って更新する想定です。

2. 外部依存をなくす設計アプローチ

  • 他のスクリプト(GameManager など)には一切依存しない。
  • 必要な値はすべて @property でインスペクタから設定。
  • UI更新もこのコンポーネントでは行わず、「スタミナの数値管理」に責務を限定。
  • 防御的実装:
    • 数値は常に 0〜最大値にクランプ。
    • 不正な設定値(0以下の最大値など)を検知してログ出力。

3. インスペクタで設定可能なプロパティ設計

StaminaBar に用意する主なプロパティと役割は以下の通りです。

  • maxStamina: number
    • 最大スタミナ値。
    • 正の値で指定。0 以下が設定された場合は警告を出し、内部的に 1 に補正。
  • initialStaminaRatio: number
    • 初期スタミナ割合(0〜1)。
    • 1.0 なら満タン開始、0.5 なら半分から開始。
    • 0〜1 の範囲外が設定された場合は自動でクランプ。
  • autoRecover: boolean
    • 自動回復を行うかどうか。
    • ダッシュ中は OFF、通常は ON など、ゲーム仕様に合わせて切り替え可能。
  • recoveryPerSecond: number
    • 1秒あたりの回復量。
    • 例:10 を設定すると、毎秒スタミナが 10 回復。
    • 0 以下で自動回復は実質無効。
  • recoveryDelay: number
    • 最後にスタミナを消費してから、回復を開始するまでの待機時間(秒)。
    • 例:1.0 を設定すると、「最後の消費から 1 秒間は回復しない」。
  • lowStaminaThreshold: number
    • 「低スタミナ」とみなす割合(0〜1)。
    • 例:0.2 を設定すると、スタミナが 20% 未満で「低スタミナ」。
    • UI 側で「ゲージを赤くする」などの条件分岐に使える。
  • enableDebugLog: boolean
    • スタミナ消費や回復のログをコンソールに出すかどうか。
    • 開発中だけ ON、本番では OFF を推奨。
  • editorPreviewConsumeAmount: number
    • エディタから consume() を試すときに使う消費量。
    • Inspector のボタン(consumePreview メソッド)呼び出し用。

これらに加えて、コード内でのみ使う内部状態(_currentStamina, _lastConsumeTime など)をプライベートフィールドとして持ちます。


TypeScriptコードの実装

以下が、Cocos Creator 3.8.7 用の完全な StaminaBar コンポーネント実装です。


import { _decorator, Component, CCFloat, CCBoolean, CCInteger, clamp01, clamp, game } from 'cc';
const { ccclass, property } = _decorator;

/**
 * StaminaBar
 * 任意のノードにアタッチするだけで「スタミナの数値管理(消費 & 自動回復)」を行う汎用コンポーネント。
 *
 * - 外部スクリプトに依存せず、このコンポーネント単体で完結します。
 * - UI やアクション側は、getComponent(StaminaBar) で取得してメソッドを呼び出してください。
 */
@ccclass('StaminaBar')
export class StaminaBar extends Component {

    @property({
        type: CCFloat,
        tooltip: '最大スタミナ値。\n正の値で指定してください。0 以下が設定された場合は 1 に補正されます。'
    })
    public maxStamina: number = 100;

    @property({
        type: CCFloat,
        range: [0, 1, 0.01],
        tooltip: '初期スタミナ割合(0〜1)。\n1.0 で満タン開始、0.5 で半分から開始します。'
    })
    public initialStaminaRatio: number = 1.0;

    @property({
        type: CCBoolean,
        tooltip: 'スタミナの自動回復を行うかどうか。'
    })
    public autoRecover: boolean = true;

    @property({
        type: CCFloat,
        tooltip: '1秒あたりのスタミナ回復量。\n例: 10 を設定すると毎秒 10 回復します。'
    })
    public recoveryPerSecond: number = 10;

    @property({
        type: CCFloat,
        tooltip: '最後にスタミナを消費してから、回復を開始するまでの待機時間(秒)。'
    })
    public recoveryDelay: number = 0.5;

    @property({
        type: CCFloat,
        range: [0, 1, 0.01],
        tooltip: '「低スタミナ」とみなす割合(0〜1)。\n例: 0.2 で 20% 未満を低スタミナと判定します。'
    })
    public lowStaminaThreshold: number = 0.2;

    @property({
        type: CCBoolean,
        tooltip: 'スタミナの消費/回復状況をログ出力するかどうか(開発用)。'
    })
    public enableDebugLog: boolean = false;

    @property({
        type: CCFloat,
        tooltip: 'エディタから consumePreview() を呼んでテストする際の消費量。'
    })
    public editorPreviewConsumeAmount: number = 10;

    // ---- 内部状態 ----

    /** 現在のスタミナ値(0〜maxStamina にクランプされます) */
    private _currentStamina: number = 0;

    /** 最後にスタミナを消費した time(game.totalTime / 1000 の値) */
    private _lastConsumeTime: number = 0;

    /** onLoad で設定値の検証と初期化を行う */
    protected onLoad() {
        this._validateAndInit();
    }

    /** start では特に処理せず、必要ならここでログなどを出す */
    protected start() {
        if (this.enableDebugLog) {
            console.log(`[StaminaBar] Initialized on node "${this.node.name}". max=${this.maxStamina}, current=${this._currentStamina}`);
        }
    }

    /**
     * 毎フレーム呼ばれ、自動回復処理を行います。
     * @param deltaTime 経過時間(秒)
     */
    protected update(deltaTime: number) {
        this._updateRecovery(deltaTime);
    }

    // =========================
    // 公開 API
    // =========================

    /**
     * スタミナを消費します。
     * @param amount 消費量(正の値)
     * @returns 消費に成功したかどうか(スタミナ不足なら false)
     */
    public consume(amount: number): boolean {
        if (amount <= 0) {
            if (this.enableDebugLog) {
                console.warn('[StaminaBar] consume() called with non-positive amount:', amount);
            }
            return false;
        }

        if (this._currentStamina < amount) {
            // 足りないので消費できない
            if (this.enableDebugLog) {
                console.log(`[StaminaBar] Not enough stamina to consume. requested=${amount}, current=${this._currentStamina}`);
            }
            return false;
        }

        this._currentStamina -= amount;
        this._currentStamina = this._clampStamina(this._currentStamina);
        this._lastConsumeTime = this._getTimeSeconds();

        if (this.enableDebugLog) {
            console.log(`[StaminaBar] Consumed ${amount}. current=${this._currentStamina}/${this.maxStamina}`);
        }

        return true;
    }

    /**
     * スタミナを強制的に回復させます(自動回復とは別に即時回復)。
     * @param amount 回復量(正の値)
     */
    public restore(amount: number): void {
        if (amount <= 0) {
            if (this.enableDebugLog) {
                console.warn('[StaminaBar] restore() called with non-positive amount:', amount);
            }
            return;
        }
        this._currentStamina += amount;
        this._currentStamina = this._clampStamina(this._currentStamina);

        if (this.enableDebugLog) {
            console.log(`[StaminaBar] Restored ${amount}. current=${this._currentStamina}/${this.maxStamina}`);
        }
    }

    /**
     * スタミナを最大値まで完全回復させます。
     */
    public restoreFull(): void {
        this._currentStamina = this.maxStamina;
        if (this.enableDebugLog) {
            console.log(`[StaminaBar] Fully restored. current=${this._currentStamina}/${this.maxStamina}`);
        }
    }

    /**
     * 現在のスタミナ値を取得します。
     */
    public getCurrentStamina(): number {
        return this._currentStamina;
    }

    /**
     * 最大スタミナ値を取得します。
     */
    public getMaxStamina(): number {
        return this.maxStamina;
    }

    /**
     * スタミナの割合(0〜1)を取得します。
     * UI のゲージ更新などに使用してください。
     */
    public getNormalizedStamina(): number {
        if (this.maxStamina <= 0) {
            return 0;
        }
        return clamp01(this._currentStamina / this.maxStamina);
    }

    /**
     * スタミナが空(0)かどうか。
     */
    public isEmpty(): boolean {
        return this._currentStamina <= 0;
    }

    /**
     * スタミナが満タン(maxStamina)かどうか。
     */
    public isFull(): boolean {
        return this._currentStamina >= this.maxStamina;
    }

    /**
     * 低スタミナ状態かどうか(normalized < lowStaminaThreshold)。
     */
    public isLow(): boolean {
        return this.getNormalizedStamina() < this.lowStaminaThreshold;
    }

    /**
     * 現在スタミナが指定量以上あるかどうか(行動可能かのチェック用途)。
     * @param required 必要スタミナ量
     */
    public canConsume(required: number): boolean {
        return this._currentStamina >= required;
    }

    // =========================
    // エディタ用の簡易テストメソッド
    // =========================

    /**
     * エディタ上から手動で呼び出して、スタミナ消費をテストするためのメソッドです。
     * - 実行: Inspector でこのコンポーネントを選択 → 属性インスペクタの「▼」メニューから呼び出し
     */
    public consumePreview(): void {
        const success = this.consume(this.editorPreviewConsumeAmount);
        if (this.enableDebugLog) {
            console.log(`[StaminaBar] consumePreview called. success=${success}`);
        }
    }

    // =========================
    // 内部処理
    // =========================

    /** 設定値の検証と初期化を行う */
    private _validateAndInit(): void {
        if (this.maxStamina <= 0) {
            console.warn(`[StaminaBar] maxStamina is non-positive on node "${this.node.name}". It will be set to 1.`);
            this.maxStamina = 1;
        }

        // 初期スタミナ割合を 0〜1 にクランプ
        this.initialStaminaRatio = clamp01(this.initialStaminaRatio);

        this._currentStamina = this.maxStamina * this.initialStaminaRatio;
        this._currentStamina = this._clampStamina(this._currentStamina);

        // ゲーム開始時点を最後の消費時間として記録(すぐに回復させたくない場合の基準)
        this._lastConsumeTime = this._getTimeSeconds();
    }

    /** 自動回復処理 */
    private _updateRecovery(deltaTime: number): void {
        if (!this.autoRecover) {
            return;
        }
        if (this.recoveryPerSecond <= 0) {
            return;
        }
        if (this.isFull()) {
            return;
        }

        const now = this._getTimeSeconds();
        const elapsedSinceConsume = now - this._lastConsumeTime;

        // 回復待機時間に達していなければ回復しない
        if (elapsedSinceConsume < this.recoveryDelay) {
            return;
        }

        // 回復量を計算
        const recoverAmount = this.recoveryPerSecond * deltaTime;
        if (recoverAmount <= 0) {
            return;
        }

        const before = this._currentStamina;
        this._currentStamina += recoverAmount;
        this._currentStamina = this._clampStamina(this._currentStamina);

        if (this.enableDebugLog && this._currentStamina !== before) {
            console.log(`[StaminaBar] Recovered ${this._currentStamina - before}. current=${this._currentStamina}/${this.maxStamina}`);
        }
    }

    /** スタミナ値を 0〜maxStamina にクランプ */
    private _clampStamina(value: number): number {
        return clamp(value, 0, this.maxStamina);
    }

    /** 経過時間(秒)を取得(game.totalTime はミリ秒) */
    private _getTimeSeconds(): number {
        return game.totalTime / 1000;
    }
}

コードの主要ポイント解説

  • onLoad()
    • _validateAndInit() を呼び出し、maxStamina の補正や初期スタミナ値の計算を行います。
    • これにより、シーン開始時点で常に一貫した状態からスタートできます。
  • update(deltaTime)
    • 毎フレーム、自動回復処理 _updateRecovery() を呼びます。
    • autoRecover, recoveryPerSecond, recoveryDelay, _lastConsumeTime を元に、回復すべきかどうかを判定します。
  • consume(amount)
    • スタミナが足りない場合は false を返し、消費しません
    • 消費が成功すると、_lastConsumeTime を更新し、以降 recoveryDelay の間は回復を停止します。
  • getNormalizedStamina()
    • UI のゲージ更新で便利な 0〜1 の割合を返します。
    • 最大値が 0 以下の場合は 0 を返し、安全に扱えるようにしています。
  • isLow()
    • 内部で getNormalizedStamina() を使い、lowStaminaThreshold と比較して低スタミナかどうかを返します。
    • UI やアクション側で「低スタミナ時にエフェクトを出す」などの条件判定に使えます。
  • consumePreview()
    • エディタ上で簡単に動作確認するためのテスト用メソッドです。
    • Inspector からメソッドを呼び出すことで、実際にスタミナが減る様子をログで確認できます。

使用手順と動作確認

1. スクリプトファイルの作成

  1. エディタ上部メニュー、または Assets パネルで任意のフォルダを選択します。
  2. そのフォルダ上で右クリック → Create → TypeScript を選択します。
  3. 新しく作成されたスクリプトの名前を 「StaminaBar.ts」 に変更します。
  4. ダブルクリックしてエディタ(VS Code など)で開き、既存のテンプレートコードをすべて削除し、前述の TypeScript コードを丸ごと貼り付けて保存します。

2. テスト用ノードの作成

スタミナは数値管理だけなので、どんなノードにアタッチしても動作します。ここでは分かりやすく Sprite ノードを例にします。

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択します。
    • 3D プロジェクトの場合は Create → 3D Object → Cube などでも構いません。
  2. 作成されたノードの名前を 「Player」 などに変更しておくと分かりやすいです。

3. StaminaBar コンポーネントのアタッチ

  1. Hierarchy で先ほど作成したノード(例:Player)を選択します。
  2. Inspector パネルの下部にある Add Component ボタンをクリックします。
  3. 表示されるメニューから Custom → StaminaBar を選択します。
    • もし Custom 内に見当たらない場合:
      • スクリプト名と @ccclass('StaminaBar') の名前が一致しているか確認。
      • スクリプトを保存した後、エディタがスクリプトをコンパイルし終えるまで少し待つ。

4. インスペクタでのプロパティ設定例

ノードに StaminaBar をアタッチすると、Inspector に以下のようなプロパティが表示されます。まずは次のように設定してみてください。

  • Max Stamina: 100
  • Initial Stamina Ratio: 1.0(満タンスタート)
  • Auto Recover: チェック ON
  • Recovery Per Second: 15(毎秒 15 回復)
  • Recovery Delay: 1.0(最後の消費から 1 秒後に回復開始)
  • Low Stamina Threshold: 0.2(20% 未満を低スタミナと判定)
  • Enable Debug Log: チェック ON(動作確認のために一旦 ON にする)
  • Editor Preview Consume Amount: 25(テストで 25 消費)

5. エディタ上での簡易テスト(ログ確認)

自動回復や消費の挙動を、まずはログで確認してみます。

  1. 上部メニューから Project → Preview または画面上部の再生ボタンを押して、ゲームをプレビュー起動します。
  2. ブラウザの開発者ツール(F12)を開き、Console タブを表示します。
  3. ゲーム画面が表示されたら、Cocos 側の Scene タブで先ほどのノード(Player)を選択します。
  4. Inspector の StaminaBar コンポーネント右上にある 「…」メニュー(またはメソッド呼び出し一覧) から、consumePreview() を実行します。
    • これにより editorPreviewConsumeAmount 分だけスタミナが消費されます。
    • Console に以下のようなログが表示されます:
      • [StaminaBar] Consumed 25. current=75/100
    • その後、recoveryDelay 秒経過すると、自動回復ログが出始めます:
      • [StaminaBar] Recovered 1.5. current=76.5/100 など

この時点で、スタミナが減って一定時間後に自動回復する 挙動が確認できれば成功です。

6. 実際のゲームロジックとの連携例

StaminaBar 自体は他のスクリプトに依存しませんが、ゲーム側からは以下のように呼び出して使えます。


// 例: プレイヤーのダッシュ処理スクリプト内

import { _decorator, Component, input, Input, EventKeyboard, KeyCode } from 'cc';
import { StaminaBar } from './StaminaBar';
const { ccclass, property } = _decorator;

@ccclass('PlayerDash')
export class PlayerDash extends Component {

    @property(StaminaBar)
    public staminaBar: StaminaBar | null = null;

    @property
    public dashStaminaCost: number = 20;

    protected onLoad() {
        // Inspector で設定されていなければ、自動で同一ノードから取得を試みる
        if (!this.staminaBar) {
            this.staminaBar = this.getComponent(StaminaBar);
        }
    }

    protected start() {
        input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
    }

    protected onDestroy() {
        input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
    }

    private onKeyDown(event: EventKeyboard) {
        if (event.keyCode === KeyCode.SHIFT_LEFT) {
            if (!this.staminaBar) {
                console.warn('[PlayerDash] StaminaBar is not assigned.');
                return;
            }

            // スタミナが足りるか確認してからダッシュ
            if (this.staminaBar.consume(this.dashStaminaCost)) {
                // ここにダッシュ処理を書く
                console.log('Dash!');
            } else {
                console.log('Not enough stamina to dash.');
            }
        }
    }
}

このように、StaminaBar は「スタミナの数値管理」だけに専念し、アクション側のスクリプトは consume()canConsume() を呼び出すだけで済むようになります。


まとめ

  • StaminaBar コンポーネントは、任意のノードにアタッチするだけでスタミナの減少・自動回復・閾値チェックを完結できる汎用スクリプトです。
  • 外部の GameManager やシングルトンには一切依存せず、インスペクタで設定可能なプロパティだけで挙動を制御できます。
  • UI やアクションロジックとは疎結合になっているため、別プロジェクトでもそのまま再利用しやすい構成になっています。
  • スタミナ以外にも、「MP(マジックポイント)」「シールド耐久値」「エネルギーゲージ」など、同じ仕組みを流用して別リソース管理コンポーネントを作ることも簡単です。

この StaminaBar.ts をひとつ用意しておくだけで、アクションゲームの実装時に「スタミナ管理ロジックを毎回書き直す」必要がなくなり、ゲーム開発のスピードと保守性が大きく向上します。ぜひ自分のプロジェクトに組み込んで、必要に応じてプロパティや API を拡張してみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!