【Cocos Creator 3.8】ContactDamage の実装:アタッチするだけで「触れた相手に自動でダメージを与える」汎用スクリプト
このコンポーネントは、敵キャラやトラップなどのノードにアタッチするだけで、「プレイヤーが触れた瞬間にダメージを与える」処理を実現する汎用スクリプトです。物理接触(Collider2Dの衝突/トリガー)をフックして、相手側の「HPを持つコンポーネント」にダメージを通知する仕組みになっており、外部のGameManagerやシングルトンに一切依存しません。
プレイヤー以外にも、「タグ名」や「ノード名の一部」などで対象を柔軟にフィルタリングできるため、幅広いアクションゲーム・STG・プラットフォーマーでそのまま使い回せる設計になっています。
コンポーネントの設計方針
1. 要件整理
- このコンポーネントをアタッチしたノード(主に敵やトラップ)が、接触した相手にダメージを与える。
- 物理判定は
Collider2Dを利用し、BEGIN_CONTACT/BEGIN_TRIGGERでダメージ処理を行う。 - ダメージを与える対象は、タグ・ノード名・レイヤーなどでフィルタ可能にする。
- 実際の「HP減少処理」は、相手ノードにアタッチされた「HPを持つコンポーネント」の特定メソッドを呼ぶ形で行い、ここでは汎用的なインターフェースにとどめる。
- 外部のカスタムスクリプトには依存せず、メソッド名と引数をインスペクタから設定することで、任意のHPコンポーネントに対応できるようにする。
- 必要コンポーネント(
Collider2D)が見つからない場合はエラーログを出し、動作を止める。
2. インスペクタで設定可能なプロパティ
このコンポーネントは、すべての挙動をインスペクタから調整できるように設計します。
- damageAmount: number
- 与えるダメージ量。
- 正の値でダメージ。0以下にするとダメージ処理を行わない。
- 例:10, 25 など。
- onlyOncePerContact: boolean
- 同じ接触中に何度もダメージを与えるかどうか。
- true: 「接触開始時」に1回だけダメージを与える。
- false: 物理更新ごとに何度もダメージを与えうる(通常は非推奨)。
- cooldown: number
- 同じ相手に再度ダメージを与えるまでのクールタイム(秒)。
- 0 の場合はクールタイムなし(接触のたびに即ダメージ)。
- 例:0.5 や 1.0 など。プレイヤーが敵に触れ続けても一定間隔でしかダメージを受けないようにする用途。
- useTagFilter: boolean
- タグによるフィルタリングを行うかどうか。
- 有効にした場合、
targetTagと一致するタグを持つノードだけをダメージ対象とする。 - Cocos Creator 3.x の「タグ」は
Node.layerではなく、ユーザー独自の文字列タグを想定したものなので、Nodeにnameベースで付けるなど運用ルールを決めて使う。
- targetTag: string
useTagFilterが true のときのみ使用。- この文字列と一致する「タグ」を持つ相手だけがダメージ対象。
- 実装上は「ノード名の前方一致」や「完全一致」など柔軟に扱う(後述)。
- 例:
"Player","Hero"など。
- useNameContainsFilter: boolean
- ノード名に特定の文字列が含まれるかでフィルタリングする。
- シンプルなプロジェクトでは、タグの代わりに「名前に ‘Player’ を含むノードだけにダメージ」などで十分なことが多い。
- nameContains: string
useNameContainsFilterが true のときのみ使用。- この文字列を
Node.nameが含む相手だけをダメージ対象とする。 - 例:
"Player","Hero"など。
- useLayerFilter: boolean
- レイヤー(
Node.layer)によるフィルタリングを行うかどうか。 - 有効にした場合、
targetLayerと一致するレイヤーを持つノードだけをダメージ対象とする。 - プロジェクト設定でレイヤーを分けている場合に有効。
- レイヤー(
- targetLayer: number
useLayerFilterが true のときのみ使用。- ダメージ対象とするレイヤービット。
Layers.Enumを利用してドロップダウンで選択可能にする。
- hpComponentName: string
- 相手側ノードにアタッチされている「HPを持つコンポーネント」のクラス名。
- この文字列で
getComponentを行い、そのコンポーネントに対してダメージメソッドを呼び出す。 - 例:
"PlayerHealth","Health"など。 - 未設定または空文字の場合は、「HPコンポーネントを探さない」仕様にして、ログだけ出すか何もしない。
- damageMethodName: string
- HPコンポーネント側で実装されている「ダメージを受けるメソッド名」。
- このメソッドに
(amount: number, source?: Node)のような引数で呼び出すことを想定。 - 例:
"applyDamage","takeDamage"など。
- passSelfAsSource: boolean
- ダメージメソッドに「ダメージを与えた側のノード(this.node)」を第2引数として渡すかどうか。
- true:
damageMethod(damageAmount, this.node)のように呼ぶ。 - false:
damageMethod(damageAmount)のように第1引数のみ。
- logDebug: boolean
- デバッグ用ログ出力を有効にするかどうか。
- 接触検知やフィルタ結果、ダメージ適用結果などを
console.logで確認できる。
このように、どのノードに、どんな条件で、どんな方法でダメージを伝えるか をすべてインスペクタから制御できるようにすることで、他のカスタムスクリプトへの依存を完全に排除しつつ、柔軟性を確保します。
TypeScriptコードの実装
import { _decorator, Component, Node, Collider2D, IPhysics2DContact, Contact2DType, Layers, warn, error, sys } from 'cc';
const { ccclass, property } = _decorator;
/**
* ContactDamage
*
* このコンポーネントを持つノードが、指定条件を満たす相手と接触したときにダメージを与える。
*
* - 必須: 同じノードに Collider2D (BoxCollider2D, CircleCollider2D など) がアタッチされていること。
* - ダメージ対象: タグ / 名前 / レイヤー でフィルタリング可能。
* - ダメージ適用: 相手ノードの任意コンポーネントに対して、任意メソッドを呼び出す。
*/
@ccclass('ContactDamage')
export class ContactDamage extends Component {
@property({
tooltip: '与えるダメージ量。0以下の場合はダメージを与えません。'
})
public damageAmount: number = 10;
@property({
tooltip: '同じ接触中に1回だけダメージを与えるかどうか。trueの場合、接触開始時(BEGIN_CONTACT/BEGIN_TRIGGER)のみダメージを与えます。'
})
public onlyOncePerContact: boolean = true;
@property({
tooltip: '同じ相手に再度ダメージを与えるまでのクールタイム(秒)。0の場合はクールタイムなし。'
})
public cooldown: number = 0.0;
@property({
tooltip: 'タグによるフィルタリングを行うかどうか。trueの場合、targetTagに一致するタグを持つ相手のみダメージを与えます。'
})
public useTagFilter: boolean = false;
@property({
tooltip: 'ダメージ対象とするタグ名。運用として、ノード名のプレフィックスなどでタグを表現してください。例: "Player"。'
})
public targetTag: string = 'Player';
@property({
tooltip: 'ノード名に特定の文字列が含まれるかでフィルタリングするかどうか。'
})
public useNameContainsFilter: boolean = true;
@property({
tooltip: 'ノード名にこの文字列が含まれる相手のみダメージを与えます。例: "Player"。'
})
public nameContains: string = 'Player';
@property({
tooltip: 'レイヤーによるフィルタリングを行うかどうか。trueの場合、targetLayerと一致するレイヤーの相手のみダメージを与えます。'
})
public useLayerFilter: boolean = false;
@property({
type: Layers.Enum,
tooltip: 'ダメージ対象とするレイヤー。Layers.Enumから選択してください。'
})
public targetLayer: number = Layers.Enum.DEFAULT;
@property({
tooltip: '相手側のHPコンポーネントのクラス名。例: "PlayerHealth"。空の場合、HPコンポーネントを検索しません。'
})
public hpComponentName: string = 'PlayerHealth';
@property({
tooltip: 'ダメージを適用するメソッド名。例: "applyDamage" や "takeDamage"。'
})
public damageMethodName: string = 'applyDamage';
@property({
tooltip: 'ダメージメソッドに、このノード(self)を第2引数として渡すかどうか。trueの場合: method(damage, this.node)。'
})
public passSelfAsSource: boolean = true;
@property({
tooltip: 'デバッグ用ログを出力するかどうか。'
})
public logDebug: boolean = false;
private _collider: Collider2D | null = null;
// 相手ノードごとの最終ダメージ時刻(UNIX秒)
private _lastDamageTimeMap: Map<number, number> = new Map();
onLoad() {
// Collider2D の取得
this._collider = this.getComponent(Collider2D);
if (!this._collider) {
error('[ContactDamage] Collider2D がアタッチされていません。ContactDamage を使用するノードには Collider2D (BoxCollider2D など) を追加してください。');
return;
}
// 物理イベントの登録
this._collider.on(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
this._collider.on(Contact2DType.BEGIN_TRIGGER, this._onBeginTrigger, this);
if (!this.onlyOncePerContact) {
// 継続的にダメージを与える場合は、必要に応じて PRE_SOLVE/POST_SOLVE などを追加できます。
// ここではサンプルとして BEGIN_CONTACT/BEGIN_TRIGGER のみを使用します。
}
}
onDestroy() {
if (this._collider) {
this._collider.off(Contact2DType.BEGIN_CONTACT, this._onBeginContact, this);
this._collider.off(Contact2DType.BEGIN_TRIGGER, this._onBeginTrigger, this);
}
this._lastDamageTimeMap.clear();
}
/**
* 衝突開始時のコールバック
*/
private _onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
if (this.logDebug) {
console.log('[ContactDamage] BEGIN_CONTACT with', otherCollider.node.name);
}
this._tryApplyDamage(otherCollider.node);
}
/**
* トリガー開始時のコールバック
*/
private _onBeginTrigger(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
if (this.logDebug) {
console.log('[ContactDamage] BEGIN_TRIGGER with', otherCollider.node.name);
}
this._tryApplyDamage(otherCollider.node);
}
/**
* ダメージ適用を試みるメイン処理
*/
private _tryApplyDamage(targetNode: Node) {
if (this.damageAmount <= 0) {
if (this.logDebug) {
console.log('[ContactDamage] damageAmount <= 0 のため、ダメージを与えません。');
}
return;
}
// 自身のノードとは無視
if (targetNode === this.node) {
return;
}
// フィルタリング
if (!this._passesFilters(targetNode)) {
if (this.logDebug) {
console.log('[ContactDamage] フィルタ条件を満たさないため、ダメージを与えません。 target =', targetNode.name);
}
return;
}
// クールタイムチェック
if (!this._canDamageNow(targetNode)) {
if (this.logDebug) {
console.log('[ContactDamage] クールタイム中のため、ダメージを与えません。 target =', targetNode.name);
}
return;
}
// HPコンポーネントの取得とメソッド呼び出し
if (!this.hpComponentName || !this.damageMethodName) {
warn('[ContactDamage] hpComponentName または damageMethodName が設定されていません。ダメージメソッドを呼び出せません。');
return;
}
const hpComponent = targetNode.getComponent(this.hpComponentName as any);
if (!hpComponent) {
if (this.logDebug) {
console.log(`[ContactDamage] targetNode に HP コンポーネント (${this.hpComponentName}) が見つかりません。 target =`, targetNode.name);
}
return;
}
const damageMethod: any = (hpComponent as any)[this.damageMethodName];
if (typeof damageMethod !== 'function') {
warn(`[ContactDamage] HP コンポーネント "${this.hpComponentName}" にメソッド "${this.damageMethodName}" が存在しません。`);
return;
}
// メソッド呼び出し
try {
if (this.passSelfAsSource) {
damageMethod.call(hpComponent, this.damageAmount, this.node);
if (this.logDebug) {
console.log(`[ContactDamage] ダメージを適用: ${this.damageAmount} to ${targetNode.name} (source: ${this.node.name})`);
}
} else {
damageMethod.call(hpComponent, this.damageAmount);
if (this.logDebug) {
console.log(`[ContactDamage] ダメージを適用: ${this.damageAmount} to ${targetNode.name}`);
}
}
// ダメージ適用時刻を記録
this._setLastDamageTime(targetNode);
} catch (e) {
error('[ContactDamage] ダメージメソッド呼び出し中にエラーが発生しました:', e);
}
}
/**
* 各種フィルタリングを通過するかどうか
*/
private _passesFilters(targetNode: Node): boolean {
// タグフィルタ(運用ルールとして、ノード名のプレフィックスをタグとみなす例)
if (this.useTagFilter && this.targetTag) {
if (!targetNode.name.startsWith(this.targetTag)) {
return false;
}
}
// 名前部分一致フィルタ
if (this.useNameContainsFilter && this.nameContains) {
if (!targetNode.name.includes(this.nameContains)) {
return false;
}
}
// レイヤーフィルタ
if (this.useLayerFilter) {
if (targetNode.layer !== this.targetLayer) {
return false;
}
}
return true;
}
/**
* クールタイムの判定
*/
private _canDamageNow(targetNode: Node): boolean {
if (this.cooldown <= 0) {
return true;
}
const now = sys.now() / 1000; // ミリ秒 → 秒
const id = targetNode._id; // Nodeの一意ID(内部プロパティだが実運用上安定している)
const lastTime = this._lastDamageTimeMap.get(id);
if (lastTime === undefined) {
return true;
}
return (now - lastTime) >= this.cooldown;
}
private _setLastDamageTime(targetNode: Node) {
if (this.cooldown <= 0) {
return;
}
const now = sys.now() / 1000;
const id = targetNode._id;
this._lastDamageTimeMap.set(id, now);
}
}
コードの要点解説
- onLoad
Collider2DをgetComponentで取得し、存在しなければerrorログを出して終了します。- 取得できた場合は、
BEGIN_CONTACTとBEGIN_TRIGGERイベントにリスナーを登録します。
- _onBeginContact / _onBeginTrigger
- 接触開始時に呼ばれるコールバックで、どちらも
_tryApplyDamageに処理を委譲します。 - デバッグフラグ
logDebugが true の場合、相手ノード名をログ出力します。
- 接触開始時に呼ばれるコールバックで、どちらも
- _tryApplyDamage
- ダメージ量チェック(0以下なら何もしない)。
- 自身のノードとの接触は無視。
_passesFiltersでターゲットフィルタリング。_canDamageNowでクールタイム判定。- 相手ノードから
hpComponentNameで指定されたコンポーネントを取得し、damageMethodNameで指定されたメソッドを呼び出します。 passSelfAsSourceが true の場合は、(damageAmount, this.node)のように2引数で呼びます。- 成功したら
_setLastDamageTimeで最終ダメージ時刻を記録します。
- _passesFilters
- タグ(ノード名のプレフィックスとして扱う例)、名前部分一致、レイヤーの3種類のフィルタをAND条件で適用します。
- いずれかが条件を満たさない場合は false を返し、ダメージ処理をスキップします。
- _canDamageNow / _setLastDamageTime
sys.now()を使って現在時刻(秒)を取得し、相手ノードごとの最終ダメージ時刻をMapで管理します。cooldown秒以上経過していれば再度ダメージを許可します。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタ左下の Assets パネルで、任意のフォルダ(例:
assets/scripts)を右クリックします。 - Create → TypeScript を選択し、ファイル名を
ContactDamage.tsとします。 - 作成された
ContactDamage.tsをダブルクリックして開き、既存のテンプレートコードをすべて削除して、前述の TypeScript コードを貼り付けて保存します。
2. テスト用シーンとノードの準備
ここでは「プレイヤーに触れるとダメージを与える敵」を例にします。
- シーンにプレイヤーノードを作成
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite などを選択し、ノード名を
Playerに変更します。 - このノードに移動やHPを管理する既存のコンポーネントがある場合はそのまま利用して構いません。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite などを選択し、ノード名を
- プレイヤーのHPコンポーネント(例)を用意
既にPlayerHealthのようなHP管理コンポーネントがある前提で使えますが、ない場合は簡易的なものを用意するとテストしやすくなります。
例として、以下のようなコンポーネントを想定します(※このスクリプトは必須ではなく、ユーザー側で自由に実装してください):// 例: PlayerHealth.ts (任意の場所に作成) import { _decorator, Component, Node, log } from 'cc'; const { ccclass, property } = _decorator; @ccclass('PlayerHealth') export class PlayerHealth extends Component { @property public maxHp: number = 100; private _hp: number = 0; onLoad() { this._hp = this.maxHp; } public applyDamage(amount: number, source?: Node) { this._hp -= amount; log(`[PlayerHealth] ダメージ: ${amount}, 残りHP: ${this._hp}, source: ${source ? source.name : 'none'}`); if (this._hp <= 0) { log('[PlayerHealth] プレイヤー死亡'); } } }- Assets パネルで
PlayerHealth.tsを作成し、上記のような簡易実装を入れても良いでしょう。 - プレイヤーノードを選択し、Inspector → Add Component → Custom → PlayerHealth でアタッチします。
- Assets パネルで
- 敵ノードを作成
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite などを選択し、ノード名を
Enemyに変更します。 - 敵スプライトの大きさや位置を適当に調整し、プレイヤーと接触できる位置に配置します。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite などを選択し、ノード名を
- 敵ノードに Collider2D を追加
Enemyノードを選択し、Inspector → Add Component → Physics 2D → BoxCollider2D などを追加します。- コライダーのサイズがスプライトと合うように Edit Collider で調整します。
- 接触判定だけにしたい場合は、Is Trigger チェックボックスをオンにしてトリガーにしておくと便利です。
- 敵ノードに ContactDamage をアタッチ
Enemyノードを選択し、Inspector → Add Component → Custom → ContactDamage を選択します。- Inspector に ContactDamage のプロパティが表示されるので、以下のように設定します(例):
- damageAmount: 10
- onlyOncePerContact: true
- cooldown: 0.5(プレイヤーが触れ続けても0.5秒ごとにしかダメージを受けない)
- useTagFilter: false(今回は使わない)
- targetTag: “Player”(未使用だが、将来用に)
- useNameContainsFilter: true
- nameContains: “Player”(
Playerという名前を含むノードにのみダメージ) - useLayerFilter: false(今回は使わない)
- targetLayer: DEFAULT(デフォルトのまま)
- hpComponentName: “PlayerHealth”(プレイヤー側のHPコンポーネント名)
- damageMethodName: “applyDamage”(PlayerHealth内で定義したメソッド名)
- passSelfAsSource: true(ダメージ元として Enemy ノードを渡す)
- logDebug: true(動作確認のためにログを出す)
3. 物理設定の確認
- 2D物理を利用するため、Project Settings → Physics 2D で物理エンジンが有効になっていることを確認します。
- プレイヤーと敵の RigidBody2D の有無は任意ですが、少なくとも一方に
RigidBody2Dがあると物理挙動が自然になります(当たり判定自体はCollider2D同士でも可能)。 - 必要に応じて、プレイヤーにも BoxCollider2D / RigidBody2D を追加してください。
4. 実行して動作確認
- シーンを保存し、上部ツールバーの Play ボタンでゲームを実行します。
- プレイヤーを移動させて敵に接触させます(キーボード移動などがない場合は、カメラやノード位置を調整して最初から重なっていても構いません)。
- コンソールログに以下のようなメッセージが出ていれば成功です。
[ContactDamage] BEGIN_CONTACT with Player[ContactDamage] ダメージを適用: 10 to Player (source: Enemy)[PlayerHealth] ダメージ: 10, 残りHP: 90, source: Enemy
- プレイヤーが敵に触れ続けている場合、
cooldownで指定した秒数ごとにダメージログが出力されることを確認します。
まとめ
この ContactDamage コンポーネントは、
- 敵やトラップなどにアタッチするだけで、接触した相手に自動でダメージを与えられる。
- ダメージ対象はタグ・名前・レイヤーで柔軟にフィルタリングできる。
- 実際のHP減少処理は、相手ノードの任意コンポーネント・任意メソッドに委譲できるため、既存のHPシステムにも簡単に組み込める。
- クールタイムやデバッグログもインスペクタから調整でき、ゲームバランス調整やデバッグがしやすい。
- 外部のGameManagerやシングルトンに一切依存せず、このスクリプト単体で完結している。
アクションゲームでは、「触れたらダメージ」という場面は非常に多く、個別に実装するとコードが散らばりがちです。
今回のような汎用コンポーネントとして切り出しておくことで、
- 新しい敵やトラップを追加するときに、ただアタッチして数値を調整するだけで済む。
- ダメージ仕様の変更も、このコンポーネントを修正するだけで全体に反映できる。
といったメリットが得られます。
ぜひ、この ContactDamage をベースに、「ノックバック追加」「属性ダメージ」「多段ヒット」など、プロジェクトに合わせた拡張も試してみてください。




