【Cocos Creator 3.8】ManaRecharge の実装:アタッチするだけで MP(マナ)を時間経過で自動回復させる汎用スクリプト
このコンポーネントは、「キャラクターやオブジェクトが持つ MP(マナ)を、時間経過に応じて自動で回復させたい」という場面で使える汎用スクリプトです。
任意のノードに ManaRecharge をアタッチし、インスペクタで初期 MP・最大 MP・回復速度などを設定するだけで、外部の GameManager や別スクリプトに依存せず MP の自動回復ロジックを完結させられます。
コンポーネントの設計方針
このコンポーネントの目的は、
- ノードにアタッチするだけで「MP 値」を内部的に保持し、自動回復させる
- 外部のシングルトンやマネージャークラスに一切依存しない
- インスペクタから各種パラメータを調整できるようにし、さまざまなゲームに使い回せるようにする
- 必要であれば「回復一時停止」「即時回復」「MP 消費」の API を公開し、他スクリプトからも柔軟に利用できるようにする
を満たすことです。
MP の管理はこのコンポーネント単体で完結させ、UI への反映やキャラクター挙動との連携は、必要であれば他のスクリプトが getComponent(ManaRecharge) で取得して利用できるようにします。
ただし本記事で作る ManaRecharge 自体は、どのようなノードにアタッチしても動作する完全独立型です。
インスペクタで設定可能なプロパティ設計
以下のプロパティを用意します。
maxMP: number- 最大 MP 値。
- 0 より大きい値を推奨。
- 例: 100
startMP: number- 開始時点の MP 値。
- 0 ~
maxMPの範囲で指定。 - 例: 50(半分からスタート)
rechargePerSecond: number- 1 秒あたりに回復する MP 量。
- 0 の場合は自動回復しない。
- 例: 5(毎秒 5 MP 回復)
autoStart: booleantrueの場合、start()時点から自動回復を開始。falseの場合、start()時点では停止状態で、スクリプトからsetPaused(false)などを呼び出して開始する。
startPaused: booleanautoStartと組み合わせて使う補助フラグ。autoStart = trueかつstartPaused = trueなら、「MP 値は初期化するが、回復ロジックは一時停止した状態」で開始。- UI の準備が整うまで回復を止めておきたい場合などに使用。
canOvercharge: booleanfalseの場合、MP はmaxMPを超えない。trueの場合、外部からの回復やaddMP()によって一時的に最大値を超える「オーバーチャージ」を許可する。
minMP: number- MP の下限値。通常は 0 に固定で問題ないが、「常に 10 は残す」といったデザインにも対応できるようにしておく。
- デフォルトは 0。
debugLog: booleantrueにすると、MP の変化やエラーをconsole.logに出力してデバッグしやすくする。- リリースビルドでは
falseを推奨。
また、実行中に外部スクリプトから操作できるよう、以下のような公開メソッドを用意します(インスペクタには表示しない)。
getCurrentMP(): number– 現在の MP を取得getMaxMP(): number– 最大 MP を取得setCurrentMP(value: number): void– MP を直接設定(範囲は自動クランプ)addMP(amount: number): void– MP を増減(負数で消費)canConsume(cost: number): boolean– 指定コスト分の MP を消費できるか判定のみtryConsume(cost: number): boolean– 消費可能なら MP を減らしtrueを返すsetPaused(paused: boolean): void– 自動回復の一時停止/再開isPaused(): boolean– 一時停止状態かどうか
TypeScriptコードの実装
import { _decorator, Component, math } from 'cc';
const { ccclass, property } = _decorator;
/**
* ManaRecharge
* 任意のノードにアタッチするだけで MP(マナ)を時間経過で自動回復させるコンポーネント。
* 外部スクリプトやシングルトンには一切依存しません。
*/
@ccclass('ManaRecharge')
export class ManaRecharge extends Component {
@property({
tooltip: '最大MP値。0より大きい値を推奨します。'
})
public maxMP: number = 100;
@property({
tooltip: '開始時点のMP値。0〜maxMPの範囲で指定します。'
})
public startMP: number = 50;
@property({
tooltip: '1秒あたりに自動回復するMP量。0の場合は自動回復しません。'
})
public rechargePerSecond: number = 5;
@property({
tooltip: 'trueの場合、start()時に自動回復ロジックを有効化します。'
})
public autoStart: boolean = true;
@property({
tooltip: 'trueの場合、開始時に一時停止状態にします(MPは初期化されます)。'
})
public startPaused: boolean = false;
@property({
tooltip: 'trueの場合、外部からの回復でmaxMPを超えてオーバーチャージすることを許可します。'
})
public canOvercharge: boolean = false;
@property({
tooltip: 'MPの下限値。通常は0のままで問題ありません。'
})
public minMP: number = 0;
@property({
tooltip: 'trueにすると、MPの変化や状態をログ出力します(デバッグ用)。'
})
public debugLog: boolean = false;
// 実行時に内部で保持する現在のMP
private _currentMP: number = 0;
// 自動回復が一時停止中かどうか
private _paused: boolean = false;
onLoad() {
// 防御的な初期化とバリデーション
if (this.maxMP <= 0) {
console.warn('[ManaRecharge] maxMP が 0 以下です。1 に補正します。', this.node.name);
this.maxMP = 1;
}
if (this.minMP < 0) {
console.warn('[ManaRecharge] minMP が 0 未満です。0 に補正します。', this.node.name);
this.minMP = 0;
}
if (this.minMP >= this.maxMP) {
console.warn('[ManaRecharge] minMP が maxMP 以上です。minMP を 0 に、maxMP を 1 に補正します。', this.node.name);
this.minMP = 0;
this.maxMP = 1;
}
// startMP をクランプして _currentMP に反映
this._currentMP = math.clamp(this.startMP, this.minMP, this.maxMP);
// 開始時の一時停止状態を設定
this._paused = this.startPaused;
if (this.debugLog) {
console.log(`[ManaRecharge] onLoad: node=${this.node.name}, currentMP=${this._currentMP}, maxMP=${this.maxMP}`);
}
}
start() {
// autoStart が false の場合は、外部から setPaused(false) されるまで自動回復しない
if (!this.autoStart) {
this._paused = true;
}
if (this.debugLog) {
console.log(`[ManaRecharge] start: autoStart=${this.autoStart}, paused=${this._paused}`);
}
}
/**
* 毎フレーム呼ばれ、経過時間 dt に応じて MP を自動回復します。
* @param dt 前フレームからの経過時間(秒)
*/
update(dt: number) {
if (this._paused) {
return;
}
if (this.rechargePerSecond <= 0) {
return;
}
if (!this.canOvercharge && this._currentMP >= this.maxMP) {
// 既に最大値に達している場合は何もしない
return;
}
const before = this._currentMP;
// dt 秒分の回復量を加算
this._currentMP += this.rechargePerSecond * dt;
// オーバーチャージ不可の場合は maxMP でクランプ
if (!this.canOvercharge) {
this._currentMP = math.clamp(this._currentMP, this.minMP, this.maxMP);
} else {
// オーバーチャージ許可時も下限だけは守る
if (this._currentMP < this.minMP) {
this._currentMP = this.minMP;
}
}
if (this.debugLog && before !== this._currentMP) {
console.log(`[ManaRecharge] update: MP ${before.toFixed(2)} -> ${this._currentMP.toFixed(2)} (dt=${dt.toFixed(3)})`);
}
}
// ====== 公開API群(他スクリプトから利用可能) ======
/**
* 現在のMPを取得します。
*/
public getCurrentMP(): number {
return this._currentMP;
}
/**
* 最大MPを取得します。
*/
public getMaxMP(): number {
return this.maxMP;
}
/**
* MPを直接設定します。minMP〜maxMPの範囲にクランプされます(オーバーチャージはここでは無視)。
*/
public setCurrentMP(value: number): void {
const before = this._currentMP;
this._currentMP = math.clamp(value, this.minMP, this.maxMP);
if (this.debugLog) {
console.log(`[ManaRecharge] setCurrentMP: ${before} -> ${this._currentMP}`);
}
}
/**
* MPを増減します。負の値を渡すと消費になります。
* canOvercharge が true の場合、maxMP を超えて増加することを許可します。
*/
public addMP(amount: number): void {
if (amount === 0) {
return;
}
const before = this._currentMP;
this._currentMP += amount;
if (!this.canOvercharge) {
this._currentMP = math.clamp(this._currentMP, this.minMP, this.maxMP);
} else {
if (this._currentMP < this.minMP) {
this._currentMP = this.minMP;
}
}
if (this.debugLog) {
console.log(`[ManaRecharge] addMP: ${before} -> ${this._currentMP} (amount=${amount})`);
}
}
/**
* 指定したコストを消費できるかどうかを判定します(MPは変化しません)。
* @param cost 消費したいMP量(正の値を指定)
*/
public canConsume(cost: number): boolean {
if (cost <= 0) {
return true;
}
return this._currentMP - cost >= this.minMP;
}
/**
* 指定したコストを消費し、成功したらtrueを返します。
* @param cost 消費したいMP量(正の値を指定)
*/
public tryConsume(cost: number): boolean {
if (cost <= 0) {
return true;
}
if (!this.canConsume(cost)) {
if (this.debugLog) {
console.log(`[ManaRecharge] tryConsume: コスト ${cost} を消費するMPが足りません (current=${this._currentMP})`);
}
return false;
}
this.addMP(-cost);
if (this.debugLog) {
console.log(`[ManaRecharge] tryConsume: cost=${cost}, currentMP=${this._currentMP}`);
}
return true;
}
/**
* 自動回復を一時停止または再開します。
* @param paused trueで一時停止、falseで再開
*/
public setPaused(paused: boolean): void {
if (this._paused === paused) {
return;
}
this._paused = paused;
if (this.debugLog) {
console.log(`[ManaRecharge] setPaused: paused=${this._paused}`);
}
}
/**
* 自動回復が一時停止中かどうかを返します。
*/
public isPaused(): boolean {
return this._paused;
}
}
コードのポイント解説
onLoad()maxMP,minMPの妥当性チェックを行い、不正な値の場合は警告を出しつつ補正します。startMPをminMP〜maxMPの範囲にクランプして_currentMPに代入します。startPausedの値に基づき、一時停止フラグ_pausedを初期化します。
start()autoStartがfalseの場合は自動回復を開始しないように_paused = trueにします。- デバッグログが有効な場合、開始時の状態をログに出力します。
update(dt)_pausedがtrueのとき、あるいはrechargePerSecond <= 0のときは何もせずリターンします。canOverchargeがfalseかつ MP が既にmaxMPに達している場合も何もしません。- それ以外の場合、
rechargePerSecond * dtだけ MP を増加させ、必要に応じてクランプします。
- 公開 API 群
getCurrentMP,getMaxMP,setCurrentMP,addMPなどで外部スクリプトから MP を安全に操作できます。canConsumeとtryConsumeにより、「消費可能かどうかのチェック」と「実際の消費」を分けて利用できます。setPaused,isPausedで自動回復ロジックの一時停止/再開を制御できます。
使用手順と動作確認
ここでは、Cocos Creator 3.8.7 のエディタ上で ManaRecharge を導入し、簡単な動作確認を行う手順を説明します。
1. スクリプトファイルの作成
- エディタ左下の Assets パネルで、スクリプトを配置したいフォルダ(例:
assets/scripts)を選択します。 - そのフォルダ上で右クリック → Create → TypeScript を選択します。
- ファイル名を
ManaRecharge.tsに変更します。 - 作成した
ManaRecharge.tsをダブルクリックして開き、既存のテンプレートコードをすべて削除し、前述の TypeScript コードを丸ごと貼り付けて保存します。
2. テスト用ノードの作成
MP ロジック自体は見た目に依存しませんが、動作確認しやすいように Sprite ノードを作ってそこにアタッチしてみます。
- エディタ左上の Hierarchy パネルで、シーン(例:
Canvas)を選択します。 - 右クリック → Create → 2D Object → Sprite を選択し、新しい Sprite ノードを作成します。
- 作成されたノードの名前を
Playerなどに変更しておくと分かりやすいです。
3. ManaRecharge コンポーネントをアタッチ
- Hierarchy パネルで、先ほど作成した
Playerノードを選択します。 - 右側の Inspector パネルで、Add Component ボタンをクリックします。
- 表示されたメニューから Custom → ManaRecharge を選択します。
- もし一覧に出てこない場合は、スクリプトのコンパイルがまだ終わっていない可能性があるので、数秒待つかエディタ右上のビルド状況を確認してください。
4. インスペクタでパラメータを設定
Player ノードの Inspector に ManaRecharge コンポーネントが表示されていることを確認し、以下のように設定してみましょう。
Max MP: 100Start MP: 20Recharge Per Second: 10(毎秒 10 回復)Auto Start: ONStart Paused: OFFCan Overcharge: OFFMin MP: 0Debug Log: ON(挙動確認のため)
5. ゲームプレビューで動作を確認
- エディタ上部の Play ボタン(▶)を押して、プレビューを開始します。
- プレビューウィンドウが開いたら、Cocos Creator の Console パネルを表示します。
- メニューから Developer → Console(もしくはショートカット)で開けます。
- ゲームが再生されると、
Debug Logが ON なので、コンソールに[ManaRecharge] onLoad: ...[ManaRecharge] start: ...[ManaRecharge] update: MP 20.00 -> 20.17 ...のようなログが流れるはずです。
- 数秒経過すると、ログ上の MP 値が徐々に増えていき、
maxMP(100)に達したところで増加が止まることを確認できます。
6. MP 消費ロジックとの連携テスト(任意)
他のスクリプトから MP を消費する例として、簡単なテストスクリプトを作成してみます。
- Assets パネルで右クリック → Create → TypeScript を選択し、
TestConsumeMP.tsを作成します。 - 中身を次のようにします(このスクリプトはあくまでテスト用であり、
ManaRecharge自体はこれに依存しません)。
import { _decorator, Component } from 'cc';
import { ManaRecharge } from './ManaRecharge';
const { ccclass } = _decorator;
@ccclass('TestConsumeMP')
export class TestConsumeMP extends Component {
start() {
const mana = this.getComponent(ManaRecharge);
if (!mana) {
console.error('[TestConsumeMP] ManaRecharge コンポーネントが同じノードに見つかりません。');
return;
}
// 2秒ごとにMPを10消費してみる
this.schedule(() => {
const cost = 10;
const success = mana.tryConsume(cost);
console.log(`Try consume ${cost}: success=${success}, currentMP=${mana.getCurrentMP()}`);
}, 2);
}
}
- この
TestConsumeMPをPlayerノードに追加し、再度プレビューすると、- 2 秒ごとに MP を 10 消費しつつ、
- 同時に
ManaRechargeが自動回復していく
様子をログで確認できます。
このように、ManaRecharge は単体で MP の管理と自動回復を完結させつつ、必要に応じて他スクリプトから安全に操作できるようになっています。
まとめ
本記事では、Cocos Creator 3.8.7 と TypeScript で、
- 任意のノードにアタッチするだけで MP を自動回復させる
ManaRechargeコンポーネント - インスペクタから調整可能な各種パラメータ(最大 MP、開始 MP、回復速度、一時停止、オーバーチャージ可否など)
- 外部の GameManager やシングルトンに依存しない完全独立設計
- 他スクリプトから利用できる MP 操作用 API 群(取得、設定、加算、消費チェックなど)
を実装しました。
このコンポーネントをプロジェクトに組み込んでおけば、
- プレイヤーキャラクター
- 敵ユニット
- 召喚物やタワーなどのオブジェクト
など、MP を扱いたいすべてのノードに対して、スクリプトをアタッチしパラメータを変えるだけで同じロジックを再利用できます。
UI 側は getComponent(ManaRecharge) で現在 MP を取得してゲージ表示に使うだけなので、ゲームごとに MP 管理ロジックを作り直す必要がなくなり、開発効率が大きく向上します。
応用としては、
- レベルアップ時に
maxMPを増やしつつsetCurrentMP(maxMP)で全回復 - 特定のバフ中だけ
rechargePerSecondを一時的に増加させる - スキル発動時に
tryConsume(cost)を使って MP が足りるかどうかをチェック
といった使い方も簡単に行えます。
必要に応じて、イベントディスパッチ機能(MP 変化時にイベントを飛ばすなど)を追加することで、UI 連携もさらに楽にできますが、まずは今回のようなシンプルで独立した形から導入していくと扱いやすいでしょう。




