【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. スクリプトファイルの作成
- エディタ上部メニュー、または Assets パネルで任意のフォルダを選択します。
- そのフォルダ上で右クリック → Create → TypeScript を選択します。
- 新しく作成されたスクリプトの名前を 「StaminaBar.ts」 に変更します。
- ダブルクリックしてエディタ(VS Code など)で開き、既存のテンプレートコードをすべて削除し、前述の TypeScript コードを丸ごと貼り付けて保存します。
2. テスト用ノードの作成
スタミナは数値管理だけなので、どんなノードにアタッチしても動作します。ここでは分かりやすく Sprite ノードを例にします。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択します。
- 3D プロジェクトの場合は Create → 3D Object → Cube などでも構いません。
- 作成されたノードの名前を 「Player」 などに変更しておくと分かりやすいです。
3. StaminaBar コンポーネントのアタッチ
- Hierarchy で先ほど作成したノード(例:Player)を選択します。
- Inspector パネルの下部にある Add Component ボタンをクリックします。
- 表示されるメニューから Custom → StaminaBar を選択します。
- もし Custom 内に見当たらない場合:
- スクリプト名と
@ccclass('StaminaBar')の名前が一致しているか確認。 - スクリプトを保存した後、エディタがスクリプトをコンパイルし終えるまで少し待つ。
- スクリプト名と
- もし Custom 内に見当たらない場合:
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. エディタ上での簡易テスト(ログ確認)
自動回復や消費の挙動を、まずはログで確認してみます。
- 上部メニューから Project → Preview または画面上部の再生ボタンを押して、ゲームをプレビュー起動します。
- ブラウザの開発者ツール(F12)を開き、Console タブを表示します。
- ゲーム画面が表示されたら、Cocos 側の Scene タブで先ほどのノード(Player)を選択します。
- 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 を拡張してみてください。




