【Cocos Creator 3.8】ShieldGeneratorの実装:アタッチするだけで「HPより先に減るシールド」を管理できる汎用スクリプト
このガイドでは、キャラクターや敵などにアタッチするだけで「ダメージを受けたときに、HPの前にシールド値が先に消費される」挙動を実現できる汎用コンポーネント ShieldGenerator を実装します。
HP管理そのものは別のコンポーネントやゲームロジックに任せつつ、「シールドの計算ロジックだけを独立させて再利用したい」という場面で非常に便利です。シールド値や自動回復、クールダウンなどはすべてインスペクタから調整できるように設計します。
コンポーネントの設計方針
1. 機能要件の整理
ShieldGenerator は以下のような機能を持ちます。
- 「現在シールド値」を内部で管理する。
- 外部から「ダメージ値」を渡すと、HPの前にシールドで受け止める。
- シールドで吸収しきれなかったダメージのみを「残りダメージ」として返す。
- シールドが自動回復する機能(オフにもできる)。
- ダメージを受けてから一定時間はシールドが回復しない「回復待機時間」を設定可能。
- 現在シールド値/最大シールド値などを取得できる「汎用API」として設計。
- 外部スクリプトやシングルトンに依存せず、このコンポーネント単体で完結。
このコンポーネントは「HPを減らす責務」を持ちません。代わりに、
applyDamage(damage: number): numberを呼び出すと、- シールドで吸収しきれなかった「残りダメージ」を返す。
- 呼び出し側は、その残りダメージを HP から減算すればよい。
これにより、HP管理の実装や構造に一切依存しない「完全に独立したシールド計算モジュール」として再利用できます。
2. インスペクタで設定可能なプロパティ設計
すべての設定は @property を通してインスペクタから行えるようにします。
- maxShield (最大シールド値)
- 型:
number - 説明: シールドの最大値。ゲーム開始時の初期値にもなります。
- 例: 50, 100, 200 など。
- 型:
- startWithFullShield (開始時フルシールド)
- 型:
boolean - 説明:
trueの場合、開始時にcurrentShield = maxShieldに設定。 falseの場合、開始時のシールドはinitialShieldプロパティの値を使用。
- 型:
- initialShield (開始時シールド値)
- 型:
number - 説明:
startWithFullShield = falseのときに使用される開始時のシールド量。 - 例: 0 〜 maxShield の範囲で設定。
- 型:
- enableAutoRegen (自動回復を有効にするか)
- 型:
boolean - 説明:
trueで、時間経過によるシールド自動回復を有効化。
- 型:
- regenRatePerSecond (1秒あたりの回復量)
- 型:
number - 説明: 自動回復時、1秒あたりに回復するシールド量。
- 例: 5 なら、1秒で 5 シールド回復。
- 型:
- regenDelayAfterHit (被弾後の回復待機時間)
- 型:
number - 説明: ダメージを受けてから、自動回復が再開されるまでの待ち時間(秒)。
- 例: 3.0 なら、被弾後3秒間は回復しない。
- 型:
- clampToMax (最大値にクランプ)
- 型:
boolean - 説明:
trueの場合、シールド値がmaxShieldを超えないように制限。
- 型:
- debugLog (デバッグログ出力)
- 型:
boolean - 説明:
trueの場合、ダメージ適用や回復処理の詳細をconsole.logに出力。
- 型:
また、コード内だけで管理する読み取り専用の状態として、
- currentShield (現在シールド値)
- lastHitTime (最後にダメージを受けた時間)
を持ちますが、これらはインスペクタから直接編集できないようにしつつ、getCurrentShield() などのメソッド経由で参照できるようにします。
TypeScriptコードの実装
以下が完成した ShieldGenerator コンポーネントの全コードです。
import { _decorator, Component, math } from 'cc';
const { ccclass, property } = _decorator;
/**
* ShieldGenerator
*
* - ダメージを受けた際、HPより先に消費される「シールド値」を管理する汎用コンポーネント。
* - 他のスクリプトに依存せず、このコンポーネント単体で完結します。
* - 外部から applyDamage(damage) を呼び出すことで、
* シールドで吸収しきれなかった残りダメージだけを返します。
*/
@ccclass('ShieldGenerator')
export class ShieldGenerator extends Component {
@property({
tooltip: 'シールドの最大値。ゲーム開始時の上限値です。'
})
public maxShield: number = 100;
@property({
tooltip: 'true の場合、開始時にシールドが最大値まで回復した状態になります。'
})
public startWithFullShield: boolean = true;
@property({
tooltip: '開始時のシールド値。startWithFullShield が false のときに使用されます。'
})
public initialShield: number = 0;
@property({
tooltip: 'シールドの自動回復を有効にするかどうか。'
})
public enableAutoRegen: boolean = true;
@property({
tooltip: '1秒あたりのシールド自動回復量。enableAutoRegen が true のときに使用されます。'
})
public regenRatePerSecond: number = 5;
@property({
tooltip: 'ダメージを受けてから自動回復が再開するまでの待機時間(秒)。'
})
public regenDelayAfterHit: number = 3;
@property({
tooltip: 'true の場合、シールド値が最大値を超えないように制限します。'
})
public clampToMax: boolean = true;
@property({
tooltip: 'true の場合、ダメージ適用や回復処理をコンソールに出力します。'
})
public debugLog: boolean = false;
// 現在のシールド値(インスペクタからは直接編集させない)
private _currentShield: number = 0;
// 最後にダメージを受けた時刻(秒)。-1 は「まだ被弾していない」ことを表す。
private _lastHitTime: number = -1;
// 経過時間を追跡するための累積タイマー
private _elapsedTime: number = 0;
onLoad() {
// 防御的実装: maxShield が負の値にならないように保証
if (this.maxShield < 0) {
if (this.debugLog) {
console.warn('[ShieldGenerator] maxShield が負の値だったため 0 に補正しました。');
}
this.maxShield = 0;
}
if (this.startWithFullShield) {
this._currentShield = this.maxShield;
} else {
// initialShield を 0〜maxShield の範囲にクランプ
const clampedInitial = math.clamp(this.initialShield, 0, this.maxShield);
if (clampedInitial !== this.initialShield && this.debugLog) {
console.warn('[ShieldGenerator] initialShield が範囲外だったためクランプしました。', this.initialShield, '→', clampedInitial);
}
this._currentShield = clampedInitial;
}
if (this.debugLog) {
console.log('[ShieldGenerator] onLoad: currentShield =', this._currentShield, ' / maxShield =', this.maxShield);
}
}
/**
* start
* 現状では特別な処理は行いませんが、
* 将来的に初期化ロジックを追加する余地を残しています。
*/
start() {
// ここでは特に何もしない
}
/**
* update
* 毎フレーム呼ばれ、自動回復ロジックを処理します。
*/
update(deltaTime: number) {
// 経過時間を累積
this._elapsedTime += deltaTime;
// 自動回復が無効なら何もしない
if (!this.enableAutoRegen) {
return;
}
// 最大シールドが 0 の場合、回復の意味がないので何もしない
if (this.maxShield <= 0) {
return;
}
// まだ一度も被弾していない場合、常に回復可能とみなす
const timeSinceLastHit = (this._lastHitTime < 0) ? Number.POSITIVE_INFINITY : (this._elapsedTime - this._lastHitTime);
// 被弾からの経過時間が待機時間に達していなければ回復しない
if (timeSinceLastHit < this.regenDelayAfterHit) {
return;
}
// すでに最大値なら回復不要
if (this._currentShield >= this.maxShield) {
return;
}
// deltaTime に応じてシールドを回復
const amountToRegen = this.regenRatePerSecond * deltaTime;
if (amountToRegen <= 0) {
return;
}
const before = this._currentShield;
this._currentShield += amountToRegen;
if (this.clampToMax) {
this._currentShield = math.clamp(this._currentShield, 0, this.maxShield);
}
if (this.debugLog) {
console.log('[ShieldGenerator] regen:', before, '→', this._currentShield);
}
}
/**
* ダメージをシールドに適用し、シールドで吸収しきれなかった残りダメージを返します。
*
* 例:
* - currentShield = 30 のときに damage = 10 → シールド 20、残りダメージ 0
* - currentShield = 30 のときに damage = 50 → シールド 0、残りダメージ 20
*
* @param damage 適用するダメージ量(負の値は 0 として扱います)
* @returns シールドで吸収しきれなかった残りダメージ
*/
public applyDamage(damage: number): number {
if (damage <= 0) {
return 0;
}
// 現在時刻(update 内で管理している経過時間)を「最後に被弾した時間」として記録
this._lastHitTime = this._elapsedTime;
const before = this._currentShield;
// シールドで吸収できる量は currentShield まで
const absorbed = Math.min(this._currentShield, damage);
this._currentShield -= absorbed;
// 吸収しきれなかった分が残りダメージ
const remainingDamage = damage - absorbed;
if (this.debugLog) {
console.log('[ShieldGenerator] applyDamage:', damage, 'absorbed =', absorbed, 'shield:', before, '→', this._currentShield, 'remainingDamage =', remainingDamage);
}
return remainingDamage;
}
/**
* シールドを即時に回復します(自動回復とは別の「強制回復」)。
*
* @param amount 回復量(負の値は 0 として扱います)
*/
public healShield(amount: number): void {
if (amount <= 0) {
return;
}
const before = this._currentShield;
this._currentShield += amount;
if (this.clampToMax) {
this._currentShield = math.clamp(this._currentShield, 0, this.maxShield);
}
if (this.debugLog) {
console.log('[ShieldGenerator] healShield:', amount, 'shield:', before, '→', this._currentShield);
}
}
/**
* シールドを即時に減少させます(ダメージ計算を伴わない「直接減算」)。
*
* @param amount 減少量(負の値は 0 として扱います)
*/
public reduceShield(amount: number): void {
if (amount <= 0) {
return;
}
const before = this._currentShield;
this._currentShield -= amount;
this._currentShield = Math.max(0, this._currentShield);
if (this.debugLog) {
console.log('[ShieldGenerator] reduceShield:', amount, 'shield:', before, '→', this._currentShield);
}
}
/**
* 現在のシールド値を取得します。
*/
public getCurrentShield(): number {
return this._currentShield;
}
/**
* シールドが最大値かどうかを返します。
*/
public isShieldFull(): boolean {
return this._currentShield >= this.maxShield;
}
/**
* シールドがゼロ(割れている状態)かどうかを返します。
*/
public isShieldBroken(): boolean {
return this._currentShield <= 0;
}
}
コードの主要部分の解説
onLoad()maxShieldが負になっていないかをチェックし、防御的に 0 に補正。startWithFullShieldがtrueなら現在シールドをmaxShieldにセット。falseの場合はinitialShieldを 0〜maxShieldにクランプしてセット。- デバッグログが有効なら初期値をコンソールに出力。
update(deltaTime)- 内部の
_elapsedTimeに経過時間を加算(秒単位)。 enableAutoRegenがfalseの場合は即リターン。maxShield <= 0の場合も回復する意味がないので何もしない。_lastHitTimeからの経過時間がregenDelayAfterHit未満なら回復しない。- シールドがすでに最大なら回復しない。
- それ以外の場合、
regenRatePerSecond * deltaTimeだけ回復し、必要ならmaxShieldまでクランプ。
- 内部の
applyDamage(damage)- 負のダメージは無視して 0 を返す。
- ダメージを受けた時刻として
_lastHitTimeに現在の_elapsedTimeを記録。 - シールドが吸収できる分だけ
absorbed = min(currentShield, damage)を計算し、シールドから差し引く。 remainingDamage = damage - absorbedを計算して返す。- デバッグログが有効なら詳細を
console.logに出力。
healShield(amount)- 即時にシールドを回復するメソッド。ポーションや回復スキルなどから呼び出す想定。
clampToMaxがtrueの場合はmaxShieldを超えないようにクランプ。
reduceShield(amount)- シールドを直接減少させるメソッド。特殊なギミックなどで使えます。
- 0 未満にならないように
Math.max(0, ...)で補正。
- 状態参照メソッド
getCurrentShield():現在シールド値の取得。isShieldFull():シールドが最大値かどうか。isShieldBroken():シールドがゼロかどうか。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリックします。
Create → TypeScriptを選択します。- ファイル名を
ShieldGenerator.tsに変更します。 - 作成された
ShieldGenerator.tsをダブルクリックしてエディタ(VS Code など)で開き、先ほどのコードを丸ごと貼り付けて保存します。
2. テスト用ノードの作成
ここでは簡単に動作確認するため、Sprite ノードにアタッチしてみます。
- Hierarchy パネルで右クリック →
Create → 2D Object → Spriteを選択して、新しい Sprite ノードを作成します。 - ノード名を
Playerなど分かりやすい名前に変更します。
Sprite コンポーネント自体はシールド処理に必須ではありませんが、ゲーム内のキャラクターやオブジェクトに見立てるために用意しています。
3. ShieldGenerator コンポーネントをアタッチ
- Hierarchy で先ほど作成した
Playerノードを選択します。 - Inspector の下部にある
Add Componentボタンをクリックします。 Custom Component→ShieldGeneratorを選択します。- もしリストに表示されない場合は、スクリプトのクラス名と
@ccclass('ShieldGenerator')の文字列が一致しているか、エディタのスクリプトコンパイルが完了しているか確認してください。
- もしリストに表示されない場合は、スクリプトのクラス名と
4. インスペクタでプロパティを設定
Player ノードの Inspector に ShieldGenerator が追加されたら、以下のように設定してみましょう。
- Max Shield:
100 - Start With Full Shield:
✔(オン) - Initial Shield:
0(Start With Full Shield がオンなので無視されます) - Enable Auto Regen:
✔(オン) - Regen Rate Per Second:
10(1秒で 10 シールド回復) - Regen Delay After Hit:
3(被弾後 3 秒間は回復しない) - Clamp To Max:
✔(オン) - Debug Log: テスト時は
✔(オン)にしてログを確認するのがおすすめです。
5. ダメージ適用のテスト方法(簡易)
エディタ上でボタンを押すだけでダメージを与える UI は用意していないため、ここでは 一時的なテストコード を追加して挙動を確認する方法を紹介します。
以下のような簡易テストを ShieldGenerator の末尾に追加してもよいですし、別スクリプトから呼び出しても構いません。
ここでは「ゲーム開始後 1 秒ごとに 30 ダメージを 3 回与える」テストコードの例を示します。
// --- 以下はテスト用コードの一例です。不要になったら削除してください。 ---
// onLoad や start の下に追加してもよいですが、
// わかりやすくするために start を少し書き換えた例を示します。
// 既存の start() を以下のように変更する例:
// start() {
// // ここでは特に何もしない
// }
start() {
// テスト用: 1秒ごとにダメージを3回与える
// 実際のゲームでは別スクリプトから applyDamage を呼び出してください。
this.scheduleOnce(() => {
this._testDamageSequence();
}, 1);
}
private _testDamageSequence() {
// 1回目のダメージ
const remain1 = this.applyDamage(30);
console.log('[ShieldGenerator TEST] Hit1: remainingDamage =', remain1, 'currentShield =', this.getCurrentShield());
// 2回目(1秒後)
this.scheduleOnce(() => {
const remain2 = this.applyDamage(30);
console.log('[ShieldGenerator TEST] Hit2: remainingDamage =', remain2, 'currentShield =', this.getCurrentShield());
}, 1);
// 3回目(2秒後)
this.scheduleOnce(() => {
const remain3 = this.applyDamage(30);
console.log('[ShieldGenerator TEST] Hit3: remainingDamage =', remain3, 'currentShield =', this.getCurrentShield());
}, 2);
}
このテストコードを入れた状態でゲームを再生すると、コンソールに以下のようなログが出力されるはずです(数値は設定によって変わります)。
[ShieldGenerator TEST] Hit1: remainingDamage = 0 currentShield = 70[ShieldGenerator TEST] Hit2: remainingDamage = 0 currentShield = 40[ShieldGenerator TEST] Hit3: remainingDamage = 0 currentShield = 10
この後、regenDelayAfterHit = 3 秒が経過すると自動回復が始まり、コンソールに [ShieldGenerator] regen: ... のログが流れてシールドが回復していく様子が確認できます。
実際のゲーム実装では、例えば「HP 管理コンポーネント」側から以下のように呼び出します。
// 例: HPコンポーネント側の疑似コード
// シールドコンポーネントを取得
const shield = this.getComponent(ShieldGenerator);
let damageToHp = incomingDamage;
if (shield) {
damageToHp = shield.applyDamage(incomingDamage);
}
// 残りダメージだけ HP から差し引く
this.currentHp -= damageToHp;
このようにすることで、HP 側のコードは「シールドがあるかもしれないし、ないかもしれない」という状態でも安全に動作し、ShieldGenerator は完全に独立した再利用可能なコンポーネントとして機能します。
まとめ
このガイドでは、
- HP より先に消費される「シールド値」を管理する
ShieldGeneratorコンポーネントを実装しました。 - 外部の GameManager やシングルトンに一切依存せず、アタッチするだけで使えるように設計しました。
- ダメージ適用は
applyDamage(damage)で行い、「シールドで吸収しきれなかった残りダメージ」を返すことで、どんな HP 実装とも疎結合に連携できます。 - 最大シールド値、自動回復の有無、回復速度、被弾後の回復待機時間などをすべてインスペクタから調整できるため、ゲームバランス調整が容易です。
このコンポーネントをベースに、
- シールドが割れた瞬間にエフェクトを再生する。
- シールドがフルになったときに UI を点滅させる。
- 武器やスキルによって
maxShieldやregenRatePerSecondを一時的に強化する。
といった拡張も簡単に行えます。Cocos Creator プロジェクト内で「シールド」を使いたくなったら、この ShieldGenerator を任意のノードにアタッチするだけで、共通のロジックとして再利用できるようになります。
必要に応じて、デバッグログやテストコードを調整しつつ、自分のゲームに合わせたパラメータを探ってみてください。




