【Cocos Creator 3.8】DamageReflection の実装:アタッチするだけで「受けたダメージの一部を攻撃者に反射」できる汎用スクリプト
このガイドでは、任意のキャラクターや敵ノードにアタッチするだけで、「受けたダメージの一部を攻撃者に反射する」 挙動を実現できる汎用コンポーネント DamageReflection を実装します。
あらかじめ用意された HP 管理や GameManager に依存せず、どんなプロジェクトにも単体で持ち込んで使えるように設計します。攻撃側・被弾側ともに「決まったインターフェース(メソッド名)」だけを共有していればよく、システム全体の構造を変えずに導入できるのがポイントです。
コンポーネントの設計方針
1. 要件整理
- このコンポーネントをアタッチしたノードが「ダメージを受けた」とき、そのダメージの一部を攻撃元(Attacker)に返す。
- 攻撃元(Attacker)は
Nodeとして渡される前提とし、そこに「ダメージを受けるメソッド」があれば、そこへ反射ダメージを送る。 - 他のカスタムスクリプト(GameManager や HP コンポーネントなど)には依存しない。
- 「ダメージを受けた」というイベントは、外部からこのコンポーネントの公開メソッドを呼ぶことで通知する。
- 反射ダメージの割合や固定値、クールダウンなどをインスペクタから調整できるようにする。
2. 外部依存をなくすためのアプローチ
「ダメージを反射する」には、次の 2 つの情報が必要です。
- 被弾ダメージ量(number)
- 攻撃してきたノード(Attacker: Node)
この情報を受け取るために、DamageReflection 自身に公開メソッドを用意します:
applyIncomingDamage(amount: number, attacker?: Node)- 外部から「ダメージを受けた」ときに呼び出してもらう入り口。
attackerが渡されなかった場合は、反射処理は行わない(安全にスキップ)。
反射する側(このコンポーネント)は、攻撃者ノードに対して次のように「ダメージを与えるメソッド」を探しに行きます:
receiveDamage(damage: number, attacker?: Node)というメソッドを持つコンポーネントがあれば、それを呼ぶ。- 見つからない場合は、
console.warnでログを出して処理をスキップ。
このように「メソッド名だけを約束」することで、HP 管理の実装が何であっても、receiveDamage さえ実装しておけば連携できます。
3. インスペクタで設定可能なプロパティ設計
今回の DamageReflection では、次のような設定項目を用意します。
- enabledReflection (boolean)
- ダメージ反射の ON/OFF を切り替え。
- 一時的に反射を止めたい場合に使用。
- reflectionRate (number)
- 受けたダメージに対する「割合」で反射する量。
- 例:
0.5なら「受けたダメージの 50% を反射」。
- useFixedReflection (boolean)
- true の場合、割合ではなく「固定値」で反射ダメージを与える。
- 割合と固定値は排他使用(どちらか一方を使う)。
- fixedReflectionAmount (number)
useFixedReflectionが true のときに使用する固定ダメージ値。- 例:
10なら「常に 10 ダメージを反射」。
- minReflectedDamage (number)
- 反射ダメージの下限値。
- 0 にすると「0 以下のダメージは反射しない」。
- maxReflectedDamage (number)
- 反射ダメージの上限値。
- 0 以下なら「上限なし」として扱う。
- enableCooldown (boolean)
- 反射にクールダウンを設けるかどうか。
- 連続攻撃に対して毎回反射したくない場合に使用。
- cooldownDuration (number)
- クールダウン時間(秒)。
enableCooldownが true のときのみ有効。
- logDebug (boolean)
- 反射処理の詳細ログをコンソールに出すかどうか。
- デバッグ時のみ ON にする想定。
また、攻撃者側にどのメソッド名を呼ぶかを変えたい場合のために、カスタムメソッド名も設定可能にします。
- attackerReceiveDamageMethod (string)
- 攻撃者側コンポーネントが持つ「ダメージを受けるメソッド名」。
- デフォルトは
"receiveDamage"。
TypeScriptコードの実装
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
/**
* DamageReflection
* 受けたダメージの一部を攻撃者に反射する汎用コンポーネント。
*
* 想定する外部からの呼び出し:
* const comp = targetNode.getComponent(DamageReflection);
* comp?.applyIncomingDamage(damageAmount, attackerNode);
*
* 攻撃者側ノードには、次のようなメソッドを持つコンポーネントをアタッチしておくと連携しやすい:
* public receiveDamage(damage: number, attacker?: Node) { ... }
*/
@ccclass('DamageReflection')
export class DamageReflection extends Component {
@property({
tooltip: 'ダメージ反射の有効 / 無効を切り替えます。false の場合、反射処理は行われません。'
})
public enabledReflection: boolean = true;
@property({
tooltip: '受けたダメージに対する反射割合(0.5 で 50% 反射)。useFixedReflection が true の場合は無視されます。'
})
public reflectionRate: number = 0.5;
@property({
tooltip: 'true の場合、割合ではなく固定値で反射ダメージを与えます。'
})
public useFixedReflection: boolean = false;
@property({
tooltip: '固定反射ダメージ量。useFixedReflection が true のときのみ使用されます。'
})
public fixedReflectionAmount: number = 10;
@property({
tooltip: '反射ダメージの最小値。この値未満の反射ダメージは 0 とみなされます。'
})
public minReflectedDamage: number = 0;
@property({
tooltip: '反射ダメージの最大値。0 以下の場合は上限なしとして扱います。'
})
public maxReflectedDamage: number = 0;
@property({
tooltip: '反射にクールダウンを設定するかどうか。true の場合、cooldownDuration 秒ごとに 1 回だけ反射します。'
})
public enableCooldown: boolean = false;
@property({
tooltip: 'クールダウン時間(秒)。enableCooldown が true のときのみ使用されます。'
})
public cooldownDuration: number = 0.5;
@property({
tooltip: '攻撃者側コンポーネントが持つ「ダメージを受けるメソッド名」。デフォルトは "receiveDamage"。'
})
public attackerReceiveDamageMethod: string = 'receiveDamage';
@property({
tooltip: 'true の場合、反射処理の詳細ログをコンソールに出力します。'
})
public logDebug: boolean = false;
// 内部状態: 最後に反射を行った時間(秒)
private _lastReflectionTime: number = -Infinity;
onLoad() {
// 防御的な初期化・バリデーション
if (this.reflectionRate < 0) {
console.warn('[DamageReflection] reflectionRate が負の値だったため 0 に補正しました。', this.node.name);
this.reflectionRate = 0;
}
if (this.fixedReflectionAmount < 0) {
console.warn('[DamageReflection] fixedReflectionAmount が負の値だったため 0 に補正しました。', this.node.name);
this.fixedReflectionAmount = 0;
}
if (this.cooldownDuration < 0) {
console.warn('[DamageReflection] cooldownDuration が負の値だったため 0 に補正しました。', this.node.name);
this.cooldownDuration = 0;
}
if (this.logDebug) {
console.log('[DamageReflection] onLoad: 初期化完了。', this.node.name);
}
}
/**
* 外部から呼び出して使用するメソッド。
* このノードがダメージを受けたときに呼び出してください。
*
* @param amount 受けたダメージ量(0 以上を想定)
* @param attacker ダメージを与えた攻撃者ノード(省略可能)
*/
public applyIncomingDamage(amount: number, attacker?: Node | null): void {
if (!this.enabledReflection) {
if (this.logDebug) {
console.log('[DamageReflection] 反射は無効化されています。処理をスキップします。', this.node.name);
}
return;
}
if (amount <= 0) {
if (this.logDebug) {
console.log('[DamageReflection] 受けたダメージ量が 0 以下のため、反射しません。amount =', amount, 'node =', this.node.name);
}
return;
}
if (!attacker) {
if (this.logDebug) {
console.log('[DamageReflection] attacker が指定されていないため、反射できません。node =', this.node.name);
}
return;
}
if (this.enableCooldown && !this._canReflectNow()) {
if (this.logDebug) {
console.log('[DamageReflection] クールダウン中のため、反射をスキップします。node =', this.node.name);
}
return;
}
const reflected = this._calculateReflectedDamage(amount);
if (reflected <= 0) {
if (this.logDebug) {
console.log('[DamageReflection] 計算結果が 0 以下のため、反射を行いません。reflected =', reflected, 'node =', this.node.name);
}
return;
}
const success = this._applyDamageToAttacker(attacker, reflected);
if (success) {
this._lastReflectionTime = this.node.scene?.frameStart || 0; // 現在時刻の代替(frameStart は秒)
if (this.logDebug) {
console.log(
`[DamageReflection] 反射ダメージを適用しました。amount=${amount}, reflected=${reflected}, target=${attacker.name}, from=${this.node.name}`
);
}
} else {
if (this.logDebug) {
console.log('[DamageReflection] 攻撃者へのダメージ適用に失敗しました。攻撃者に対応メソッドが存在しない可能性があります。attacker =', attacker.name);
}
}
}
/**
* 現在、反射を行ってよいか(クールダウン判定を含む)を返します。
*/
private _canReflectNow(): boolean {
if (!this.enableCooldown) {
return true;
}
const now = this.node.scene?.frameStart || 0;
return (now - this._lastReflectionTime) >= this.cooldownDuration;
}
/**
* 反射ダメージ量を計算します。
*/
private _calculateReflectedDamage(incomingDamage: number): number {
let reflected = 0;
if (this.useFixedReflection) {
reflected = this.fixedReflectionAmount;
} else {
reflected = incomingDamage * this.reflectionRate;
}
// 最小値・最大値でクランプ
if (reflected < this.minReflectedDamage) {
reflected = this.minReflectedDamage;
}
if (this.maxReflectedDamage > 0 && reflected > this.maxReflectedDamage) {
reflected = this.maxReflectedDamage;
}
// 小数がイヤならここで Math.round なども可能だが、汎用性のためそのまま残す。
return reflected;
}
/**
* 攻撃者ノードに対して、attackerReceiveDamageMethod で指定されたメソッドを探して呼び出します。
*
* @returns 成功した場合 true、対応メソッドが見つからなかった場合 false。
*/
private _applyDamageToAttacker(attacker: Node, damage: number): boolean {
if (!attacker.isValid) {
console.warn('[DamageReflection] 攻撃者ノードが無効です。', attacker);
return false;
}
const methodName = this.attackerReceiveDamageMethod;
// ノードにアタッチされている全コンポーネントを走査し、メソッドを探す
const comps = attacker.getComponents(Component);
for (const comp of comps) {
const anyComp = comp as any;
const method = anyComp[methodName];
if (typeof method === 'function') {
try {
// 攻撃者側のメソッドシグネチャは: (damage: number, attacker?: Node)
method.call(anyComp, damage, this.node);
return true;
} catch (e) {
console.error('[DamageReflection] 攻撃者へのダメージ適用中にエラーが発生しました。', e);
return false;
}
}
}
console.warn(
`[DamageReflection] 攻撃者ノード "${attacker.name}" に、メソッド "${methodName}" を持つコンポーネントが見つかりませんでした。`
);
return false;
}
}
コード解説(ライフサイクルと主要メソッド)
- onLoad()
- インスペクタから設定された値のバリデーション(負値の補正など)を行います。
logDebugが true の場合、初期化ログを出力します。
- applyIncomingDamage(amount, attacker)
- 外部から「ダメージを受けた」ときに呼び出す公開メソッドです。
- 反射が無効 / ダメージ量が 0 以下 / attacker 未指定 / クールダウン中 の場合は安全にスキップします。
- 反射ダメージを計算し、0 以下なら何もしません。
- 攻撃者ノードに対して
_applyDamageToAttackerを通じてダメージを送ります。
- _canReflectNow()
- クールダウンが有効な場合、
_lastReflectionTimeとの時間差から、反射可能かどうかを判定します。
- クールダウンが有効な場合、
- _calculateReflectedDamage(incomingDamage)
useFixedReflectionに応じて、割合または固定値で反射ダメージを計算します。minReflectedDamage/maxReflectedDamageでクランプします。
- _applyDamageToAttacker(attacker, damage)
- 攻撃者ノードにアタッチされている全コンポーネントを走査し、
attackerReceiveDamageMethod(デフォルトreceiveDamage)というメソッドを持つコンポーネントを探します。 - 見つかった場合は
method.call(comp, damage, this.node)を実行し、攻撃者にダメージを適用します。 - 見つからない場合は
console.warnを出して false を返します。
- 攻撃者ノードにアタッチされている全コンポーネントを走査し、
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を
DamageReflection.tsにします。 - 自動生成されたコードを削除し、本記事の
DamageReflectionコードを丸ごと貼り付けて保存します。
2. テスト用ノード(被弾側)の準備
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite など、適当なノードを作成します。
- 名前例:
PlayerやEnemyWithReflectionなど。
- 名前例:
- 作成したノードを選択し、Inspector の Add Component → Custom → DamageReflection を選択してアタッチします。
- Inspector で次のように設定してみます(例):
Enabled Reflection: ONReflection Rate: 0.5(受けたダメージの 50% を反射)Use Fixed Reflection: OFFFixed Reflection Amount: 10(今回は未使用)Min Reflected Damage: 0Max Reflected Damage: 0(上限なし)Enable Cooldown: お好みで ON/OFF- ON の場合、
Cooldown Durationを 0.5 秒などに設定。
- ON の場合、
Attacker Receive Damage Method: receiveDamage(デフォルトのまま)Log Debug: テスト中は ON にしておくと挙動が分かりやすいです。
3. テスト用ノード(攻撃者側)の準備
攻撃者側は、receiveDamage というメソッドを持つ任意のコンポーネントがあれば動作します。ここでは、簡易的なテスト用スクリプトを用意します。
- Assets パネルで右クリック → Create → TypeScript を選択し、
DummyAttacker.tsという名前で作成します。 - 以下のような簡易コンポーネントを記述します(あくまでテスト用・任意):
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('DummyAttacker')
export class DummyAttacker extends Component {
@property
public hp: number = 100;
public receiveDamage(damage: number, attacker?: Node) {
this.hp -= damage;
console.log(
`[DummyAttacker] ダメージを受けました: damage=${damage}, 残りHP=${this.hp}, 攻撃者=${attacker ? attacker.name : '不明'}`
);
}
}
※この DummyAttacker は本ガイドの「完全独立コンポーネント」条件には含めません。
DamageReflection 自体は、このスクリプトが無くてもコンパイル・実行可能であり、「こういうメソッドを持つコンポーネントがあれば連携できる」というテスト用の例です。
- Hierarchy で攻撃者用ノード(例:
Attacker)を作成し、Inspector から Add Component → Custom → DummyAttacker をアタッチします。
4. 攻撃イベントから DamageReflection を呼び出す
最後に、「攻撃がヒットしたときに applyIncomingDamage を呼ぶ」仕組みを作ります。これは各プロジェクトの攻撃判定ロジックに依存するため、ここでは簡単なサンプルを示します。
- Assets パネルで
DamageReflectionTest.tsというスクリプトを作成し、次のように書きます:
import { _decorator, Component, Node } from 'cc';
import { DamageReflection } from './DamageReflection';
const { ccclass, property } = _decorator;
@ccclass('DamageReflectionTest')
export class DamageReflectionTest extends Component {
@property({ tooltip: 'ダメージを反射する対象(被弾側)ノード' })
public targetWithReflection: Node | null = null;
@property({ tooltip: '攻撃者ノード(反射ダメージを受ける側)' })
public attacker: Node | null = null;
@property({ tooltip: '1 回の攻撃で与えるダメージ量' })
public attackDamage: number = 20;
start() {
// 起動直後にテスト攻撃を 1 回行う例
this.scheduleOnce(() => {
this._testAttack();
}, 1.0);
}
private _testAttack() {
if (!this.targetWithReflection || !this.attacker) {
console.warn('[DamageReflectionTest] targetWithReflection または attacker が未設定です。');
return;
}
const reflectionComp = this.targetWithReflection.getComponent(DamageReflection);
if (!reflectionComp) {
console.warn('[DamageReflectionTest] targetWithReflection に DamageReflection コンポーネントが見つかりません。');
return;
}
console.log('[DamageReflectionTest] テスト攻撃を実行します。');
// 実際にはここで target 側の HP を減らす処理などを行うはずですが、
// 反射テストのため、DamageReflection のみを呼び出します。
reflectionComp.applyIncomingDamage(this.attackDamage, this.attacker);
}
}
- Hierarchy で新しいノード(例:
DamageReflectionTester)を作成し、Add Component → Custom → DamageReflectionTest をアタッチします。 - Inspector で次のように設定します:
Target With Reflection: 先ほどDamageReflectionを付けたノード(例:EnemyWithReflection)Attacker:DummyAttackerを付けたノード(例:Attacker)Attack Damage: 20 など任意
- ゲームを再生すると、1 秒後にテスト攻撃が 1 回行われ、コンソールに次のようなログが出力されます:
[DamageReflectionTest] テスト攻撃を実行します。[DamageReflection] 反射ダメージを適用しました。amount=20, reflected=10, ...(reflectionRate = 0.5 の場合)[DummyAttacker] ダメージを受けました: damage=10, 残りHP=90, 攻撃者=EnemyWithReflection
これで、「被弾側が受けたダメージの一部を攻撃者に返す」挙動が確認できます。
まとめ
DamageReflectionは、どのノードにもアタッチできる汎用コンポーネントとして設計しました。- 外部の GameManager や HP コンポーネントに依存せず、公開メソッド
applyIncomingDamageと、攻撃者側のreceiveDamage(または任意名)だけを約束にしています。 - 反射割合・固定値・最小 / 最大ダメージ・クールダウン・ログ出力など、すべてインスペクタから調整可能なため、ゲームバランスの調整がしやすくなります。
- このスクリプト単体を別プロジェクトにコピーしても、そのまま動作し、攻撃者側に
receiveDamageさえ実装すればすぐに連携できます。
同じ設計パターン(「イベントを受け取る公開メソッド」+「相手ノードのメソッド探索」)は、状態異常付与・ライフスティール・シールドなどの汎用効果コンポーネントにも応用できます。
プロジェクトごとの GameManager に依存しない「再利用可能なギミック」として、ぜひ自分用のコンポーネントライブラリに加えてみてください。




