【Cocos Creator 3.8】ElementAffinity の実装:アタッチするだけで「炎・氷などの属性ダメージ倍率(弱点・半減・無効)」を一元管理できる汎用スクリプト
このガイドでは、RPG やアクションゲームでよく使う「属性耐性(炎に弱い/氷に強い/雷無効…)」を、1つのコンポーネントをノードにアタッチするだけで管理できる ElementAffinity を実装します。
このコンポーネントは、
- 敵・プレイヤー・ギミックなど「ダメージを受けるノード」にアタッチ
- インスペクタから「属性ごとの倍率」を設定
- スクリプトから
applyElementDamage/getElementMultiplierを呼ぶだけ
というシンプルな設計で、外部の GameManager やシングルトンに一切依存せず動作します。
コンポーネントの設計方針
要件整理
- 炎・氷などの「属性タイプ」を識別できる。
- 属性ごとに「被ダメージ倍率」を設定できる(例:炎 2.0 倍で弱点、氷 0.5 倍で半減、雷 0 倍で無効)。
- スクリプトから簡単に呼べる API を提供する。
getElementMultiplier(element: ElementType): number… 属性に対する倍率を返す。applyElementDamage(element: ElementType, baseDamage: number): number… 基礎ダメージに倍率をかけた最終ダメージを返す。
- コンポーネント単体で完結し、他のカスタムスクリプトに依存しない。
- インスペクタから属性リストと倍率を直感的に編集できる。
設計アプローチ
Cocos Creator 3.8 では、@property で Enum や Array を扱うことで、インスペクタから簡単に設定できる UI を作れます。これを利用して:
- 属性の種類を
enum ElementTypeとして定義。 - 属性ごとの倍率テーブルを、インスペクタから編集可能な配列として持つ。
- 配列は「属性 & 倍率」のペア構造にする。
インスペクタで設定可能なプロパティ
ElementAffinity に用意するプロパティと役割は次の通りです。
affinityTable: ElementAffinityEntry[]- 型:カスタムクラス
ElementAffinityEntryの配列。 - 役割:属性ごとの被ダメージ倍率を登録するテーブル。
- インスペクタでの使い方:
- +ボタンで要素を追加。
elementで「炎 / 氷 / 雷 / 風 / 土 / 光 / 闇 / 無属性」などを選択。multiplierに数値を入力(例:2.0, 1.0, 0.5, 0)。
- 型:カスタムクラス
defaultMultiplier: number- 役割:テーブルに存在しない属性に対して使う「デフォルト倍率」。
- 例:
1.0(等倍)を基本とし、未登録の属性は 1.0 倍で処理。
clampMultiplierMin: number/clampMultiplierMax: number- 役割:倍率を安全な範囲にクランプするための下限・上限。
- 例:
0~10にして、誤入力で 9999 などを入れても暴走しないようにする。
logCalculation: boolean- 役割:
applyElementDamageを呼んだとき、計算内容をconsole.logに出すかどうか。 - デバッグ用。実際のゲームではオフにしておくとよい。
- 役割:
また、インスペクタに直接は出ませんが、倍率テーブルの 1 要素として以下のクラスを定義します。
ElementAffinityEntryelement: ElementType… 対象の属性。multiplier: number… その属性に対する被ダメージ倍率。
TypeScriptコードの実装
import { _decorator, Component, Enum, math } from 'cc';
const { ccclass, property } = _decorator;
/**
* 属性の種類を表す列挙型
* 必要に応じて要素を追加・削除してください。
*/
export enum ElementType {
None = 0,
Fire = 1,
Ice = 2,
Thunder = 3,
Wind = 4,
Earth = 5,
Light = 6,
Dark = 7,
}
/**
* インスペクタで編集する「属性 & 倍率」の1エントリ
*/
@ccclass('ElementAffinityEntry')
export class ElementAffinityEntry {
@property({
type: Enum(ElementType),
tooltip: 'このエントリが対象とする属性タイプ',
})
public element: ElementType = ElementType.Fire;
@property({
tooltip: 'この属性に対する被ダメージ倍率。\n例: 2.0 = 弱点(2倍), 0.5 = 半減, 0 = 無効',
})
public multiplier: number = 1.0;
}
@ccclass('ElementAffinity')
export class ElementAffinity extends Component {
@property({
type: [ElementAffinityEntry],
tooltip: '属性ごとの被ダメージ倍率テーブル。\n' +
'同じ属性を複数登録した場合、最初に見つかったエントリが使用されます。',
})
public affinityTable: ElementAffinityEntry[] = [];
@property({
tooltip: 'テーブルに存在しない属性に対して使用するデフォルト倍率。\n' +
'通常は 1.0 (等倍) を推奨します。',
})
public defaultMultiplier: number = 1.0;
@property({
tooltip: '倍率の下限値。\n' +
'0 にすると「これ以上は減らない(完全無効)」のような扱いができます。',
})
public clampMultiplierMin: number = 0.0;
@property({
tooltip: '倍率の上限値。\n' +
'極端な値(例: 9999)を防ぐための安全装置として利用します。',
})
public clampMultiplierMax: number = 10.0;
@property({
tooltip: 'true の場合、ダメージ計算時にログを出力します(デバッグ用)。',
})
public logCalculation: boolean = false;
/**
* ノードにアタッチされた直後に呼ばれます。
* 主に設定値のバリデーションを行います。
*/
onLoad() {
// クランプ範囲の妥当性チェック
if (this.clampMultiplierMax < this.clampMultiplierMin) {
console.warn(
`[ElementAffinity] clampMultiplierMax (${this.clampMultiplierMax}) ` +
`is less than clampMultiplierMin (${this.clampMultiplierMin}). ` +
'値を入れ替えます。'
);
const tmp = this.clampMultiplierMin;
this.clampMultiplierMin = this.clampMultiplierMax;
this.clampMultiplierMax = tmp;
}
// デフォルト倍率をクランプ
this.defaultMultiplier = this._clampMultiplier(this.defaultMultiplier);
// 各エントリの倍率もクランプしておく
for (const entry of this.affinityTable) {
entry.multiplier = this._clampMultiplier(entry.multiplier);
}
}
/**
* 属性に対する被ダメージ倍率を取得します。
* テーブルに存在しない場合は defaultMultiplier を返します。
*
* @param element - ダメージの属性タイプ
* @returns 被ダメージ倍率
*/
public getElementMultiplier(element: ElementType): number {
for (const entry of this.affinityTable) {
if (entry.element === element) {
return this._clampMultiplier(entry.multiplier);
}
}
return this.defaultMultiplier;
}
/**
* 属性付きダメージを適用し、倍率をかけた「最終ダメージ値」を返します。
* このコンポーネント自身は HP を持たず、計算のみを担当します。
*
* @param element - ダメージの属性タイプ
* @param baseDamage - 基礎ダメージ値(例: スキルの威力など)
* @returns 倍率を適用した最終ダメージ値
*/
public applyElementDamage(element: ElementType, baseDamage: number): number {
const multiplier = this.getElementMultiplier(element);
const finalDamage = baseDamage * multiplier;
if (this.logCalculation) {
console.log(
`[ElementAffinity] element=${ElementType[element]}, ` +
`baseDamage=${baseDamage}, multiplier=${multiplier}, ` +
`finalDamage=${finalDamage}`
);
}
return finalDamage;
}
/**
* 倍率を設定された下限〜上限にクランプします。
*/
private _clampMultiplier(value: number): number {
return math.clamp(value, this.clampMultiplierMin, this.clampMultiplierMax);
}
}
コードのポイント解説
enum ElementType- 炎・氷・雷・風・土・光・闇・無属性を定義。
- 必要に応じて要素を増減して構いません(例:Poison, Holy など)。
ElementAffinityEntry- インスペクタで「属性」と「倍率」を 1 行として編集するためのクラス。
@ccclass('ElementAffinityEntry')を付けることで、配列の要素としてインスペクタに表示できます。
affinityTable- 複数の
ElementAffinityEntryを持つ配列。 - インスペクタで+ボタンを押して、属性と倍率をどんどん追加していきます。
- 複数の
onLoad()- コンポーネントが有効になったタイミングで、設定値の整合性チェックを行います。
clampMultiplierMinとclampMultiplierMaxの大小関係がおかしい場合に警告を出し、値を入れ替えます。- デフォルト倍率と各エントリの倍率を、クランプ範囲内に収めます。
getElementMultiplier()- 外部スクリプトから「この属性の倍率はいくつ?」と問い合わせるためのメソッド。
- テーブルを先頭から走査し、最初に見つかった一致エントリの倍率を返します。
- 見つからない場合は
defaultMultiplierを返します。
applyElementDamage()- 「属性 × 基礎ダメージ」から最終ダメージを計算し、数値として返すだけの純粋関数的メソッド。
- このコンポーネントは HP を持たず、計算ロジックだけに徹しているため、どんなゲームにも組み込みやすいです。
logCalculationが true のとき、計算過程をログ出力します(デバッグに便利)。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を
ElementAffinity.tsにします。 - 自動生成されたテンプレートコードをすべて削除し、このガイドの「TypeScriptコードの実装」セクションにあるコードを丸ごと貼り付けて保存します。
2. テスト用ノードの作成
ここでは、単純な Sprite ノードを例にしますが、プレイヤーや敵キャラのルートノードなど、どのノードでも構いません。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択します。
- 作成されたノード名を
TestTargetなど分かりやすい名前に変更します。
このノードに属性耐性コンポーネントをアタッチします。
3. ElementAffinity コンポーネントのアタッチ
- Hierarchy で先ほど作成した
TestTargetノードを選択します。 - Inspector パネルの下部にある 「Add Component」 ボタンをクリックします。
- 「Custom」→「ElementAffinity」 を選択して追加します。
これで ElementAffinity がノードにアタッチされ、インスペクタに以下のプロパティが表示されます。
Affinity TableDefault MultiplierClamp Multiplier MinClamp Multiplier MaxLog Calculation
4. 属性倍率の設定例
RPG 風の設定例として、次のように入力してみましょう。
- Default Multiplier:
1.0- 特に設定していない属性は等倍ダメージ。
- Clamp Multiplier Min:
0.0 - Clamp Multiplier Max:
10.0 - Log Calculation:
true(動作確認のため一旦オン) - Affinity Table:
- サイズを「3」に設定(要素数 3)。
- 各要素を次のように設定:
- 要素 0:
element:Firemultiplier:2.0(炎が弱点で 2 倍ダメージ)
- 要素 1:
element:Icemultiplier:0.5(氷に強く、半減)
- 要素 2:
element:Thundermultiplier:0.0(雷完全無効)
- 要素 0:
この設定により、
- 炎属性:2 倍
- 氷属性:0.5 倍
- 雷属性:0 倍(無効)
- その他の属性(風・土・光・闇・None など):1 倍(等倍)
という耐性を持ったノードになります。
5. スクリプトからの呼び出しテスト
次に、別の簡単なスクリプトから ElementAffinity を呼び出して、動作を確認します。
- Assets パネルで右クリック → Create → TypeScript を選択し、
ElementAffinityTester.tsという名前でスクリプトを作成します。 - 以下のコードを貼り付けます。
import { _decorator, Component } from 'cc';
import { ElementAffinity, ElementType } from './ElementAffinity';
const { ccclass, property } = _decorator;
@ccclass('ElementAffinityTester')
export class ElementAffinityTester extends Component {
@property({
type: ElementAffinity,
tooltip: 'テスト対象の ElementAffinity コンポーネントを持つノードを指定します。',
})
public targetAffinity: ElementAffinity | null = null;
start() {
if (!this.targetAffinity) {
console.error('[ElementAffinityTester] targetAffinity が設定されていません。');
return;
}
// 基礎ダメージ 100 を想定して、各属性での最終ダメージを計算
const baseDamage = 100;
const fireDamage = this.targetAffinity.applyElementDamage(ElementType.Fire, baseDamage);
const iceDamage = this.targetAffinity.applyElementDamage(ElementType.Ice, baseDamage);
const thunderDamage = this.targetAffinity.applyElementDamage(ElementType.Thunder, baseDamage);
const windDamage = this.targetAffinity.applyElementDamage(ElementType.Wind, baseDamage);
console.log('=== ElementAffinity Test Result ===');
console.log(`Fire : base=${baseDamage}, final=${fireDamage}`);
console.log(`Ice : base=${baseDamage}, final=${iceDamage}`);
console.log(`Thunder: base=${baseDamage}, final=${thunderDamage}`);
console.log(`Wind : base=${baseDamage}, final=${windDamage}`);
}
}
テスターのセットアップ
- Hierarchy で右クリック → Create → Empty Node を選択し、
Testerという名前にします。 Testerノードを選択し、Inspector の Add Component → Custom → ElementAffinityTester を追加します。ElementAffinityTesterの Target Affinity プロパティに、先ほど作成したTestTargetノード上のElementAffinityをドラッグ&ドロップします。
再生してログを確認
- エディタ上部の「▶」ボタンでゲームを再生します。
- Console パネルを開き、出力を確認します。
先ほどの設定(炎 2.0, 氷 0.5, 雷 0.0, その他 1.0, 基礎ダメージ 100)であれば、ログは次のようになります。
=== ElementAffinity Test Result ===
Fire : base=100, final=200
Ice : base=100, final=50
Thunder: base=100, final=0
Wind : base=100, final=100
さらに ElementAffinity の Log Calculation を true にしている場合、applyElementDamage を呼んだタイミングで、個別の計算ログも出力されます。
まとめ
このガイドでは、属性耐性(ElementAffinity) を管理する汎用コンポーネントを、Cocos Creator 3.8 + TypeScript で実装しました。
- ノードに
ElementAffinityをアタッチし、インスペクタから属性ごとの倍率を設定するだけで、「弱点・半減・無効」などの耐性を簡単に表現できます。 - 外部の GameManager や HP 管理クラスに依存せず、「ダメージ倍率の計算」だけに特化した独立コンポーネントなので、どんなプロジェクトにも組み込みやすい構造になっています。
getElementMultiplier/applyElementDamageの 2 つのメソッドを呼ぶだけで、他のスクリプトからシンプルに利用できます。
応用例として:
- スキルや攻撃スクリプト側で
ElementTypeを持たせ、命中時にapplyElementDamageで最終ダメージを算出する。 - UI 側で「炎耐性 50%」などを表示する際に、
getElementMultiplierの値から自動的にパーセント表記を生成する。 - ボス戦などで、フェーズごとに
affinityTableを動的に書き換え、弱点が変化するギミックを実装する。
このように、「計算ロジックを 1 コンポーネントに閉じ込める」ことで、ゲーム全体のコードが見通しやすくなり、再利用性も高まります。必要に応じて属性の種類を増やしたり、状態異常用の別 Enum を追加するなど、プロジェクトに合わせて拡張してみてください。




