【Cocos Creator 3.8】EvasionCalculator(回避判定)の実装:アタッチするだけで「Agilityに応じた回避率計算&ダメージ判定」を行える汎用スクリプト
このガイドでは、RPGやアクションゲームでよくある「素早さ(Agility)に応じて攻撃を回避する」処理を、1コンポーネントだけで完結させる EvasionCalculator を実装します。
ノードにアタッチしておくだけで、任意のスクリプトから「この攻撃が当たるか?」「回避されたか?」を問い合わせできるようになり、ゲームロジックをシンプルに保てます。
コンポーネントの設計方針
1. 要件整理
- Agility(敏捷 / 素早さ)ステータスを持ち、その値に応じて回避率を計算する。
- 攻撃を受けたときに「命中 or 回避(Miss)」を判定し、回避した場合はダメージ無効とみなせるようにする。
- このコンポーネント単体で完結させるため、外部のGameManagerやステータス管理クラスには依存しない。
- 他のスクリプトから使いやすいように、公開メソッドで判定処理を提供する。
- インスペクタから、回避率の計算式に関わるパラメータを調整できるようにする。
2. 回避率の設計
汎用的に使えるよう、回避率は次のような「シンプルで直感的な式」にします。
回避率(0〜1) = clamp01( baseEvasion + agility * agilityFactor )
- baseEvasion: 基本回避率(すべてのキャラが共通で持つ最低限の回避力)
- agility: このコンポーネントが持つAgility値
- agilityFactor: Agility1ポイントあたり、どれくらい回避率が上がるかの係数
- clamp01: 0〜1の範囲に収める(0未満は0、1より大きい場合は1)
例えば:
baseEvasion = 0.05 (5%)agilityFactor = 0.01 (1%)agility = 20
なら、回避率 = clamp01(0.05 + 20 * 0.01) = 0.25 (25%) となります。
3. インスペクタで設定可能なプロパティ
このコンポーネントは、他のノードやスクリプトに依存せず、すべての設定をインスペクタから行えるようにします。
agility: number- このキャラクターのAgility値。
- 0以上の整数を想定(小数も可)。
- 例:10, 20, 50 など。
baseEvasion: number- 基本回避率(0〜1)。
- 0.1 なら 10% の意味。
- 例:0.05(5%)、0.1(10%)。
agilityFactor: number- Agility1ポイントあたりの回避率増加量(0〜1)。
- 0.01 なら Agility 1 につき +1% 回避率。
- 例:0.005(0.5%)、0.01(1%)。
maxEvasion: number- 回避率の上限(0〜1)。
- 1.0 なら最大100%まで。
- 例:0.8(80%)、0.95(95%)。
useSeed: boolean- テスト・デバッグ用に、擬似乱数のシード固定を行うかどうか。
- ONにすると、同じ条件なら毎回同じ結果になり、デバッグしやすくなる。
seed: numberuseSeed = trueのときに使用するシード値。- テスト用に 1, 12345 などを設定。
logDebug: boolean- 回避判定の詳細(回避率・乱数・結果)を
console.logに出力するかどうか。 - ONにすると、回避判定の挙動をエディタの Console で確認できる。
- 回避判定の詳細(回避率・乱数・結果)を
4. 公開メソッドの設計
他のスクリプトから使いやすくするため、次のようなメソッドを公開します。
getCurrentEvasionRate(): number- 現在の設定値から計算した「回避率(0〜1)」を返す。
tryEvade(): boolean- 1回分の回避判定を行い、true なら回避成功(Miss)、false なら命中 を返す。
- 攻撃処理側から呼び出して「ダメージを与えるかどうか」を決めるのに使用。
rollDamageAfterEvasion(damage: number): number- ダメージ値を受け取り、回避判定を行ったあと、
- 回避成功なら
0、失敗なら元のdamageを返す。 - 「ダメージ計算 → 最後にこの関数を通すだけ」で、回避を簡単に統合できるようにする。
TypeScriptコードの実装
import { _decorator, Component, random, randomRange, clamp01 } from 'cc';
const { ccclass, property } = _decorator;
/**
* EvasionCalculator
*
* Agility(敏捷)に基づいて回避率を計算し、
* 攻撃命中時に「命中 or 回避(Miss)」を判定する汎用コンポーネント。
*
* 他のスクリプトからは、tryEvade / rollDamageAfterEvasion を呼び出して使用します。
*/
@ccclass('EvasionCalculator')
export class EvasionCalculator extends Component {
@property({
tooltip: 'このキャラクターのAgility(敏捷)値。\n値が高いほど回避率が上昇します。',
})
public agility: number = 10;
@property({
tooltip: '基本回避率(0〜1)。\n0.1 なら 10% の回避率を意味します。',
min: 0,
max: 1,
step: 0.01,
})
public baseEvasion: number = 0.05;
@property({
tooltip: 'Agility 1ポイントあたりの回避率増加量(0〜1)。\n0.01 なら Agility 1 につき +1% 回避率。',
min: 0,
max: 1,
step: 0.001,
})
public agilityFactor: number = 0.01;
@property({
tooltip: '回避率の上限(0〜1)。\n1.0 なら 100% まで回避可能。',
min: 0,
max: 1,
step: 0.01,
})
public maxEvasion: number = 0.95;
@property({
tooltip: 'テスト用に乱数のシードを固定するかどうか。\nONにすると、同じ条件で毎回同じ結果になります。',
})
public useSeed: boolean = false;
@property({
tooltip: 'useSeed が true のときに使用するシード値。\n任意の整数を設定してください。',
})
public seed: number = 1;
@property({
tooltip: '回避判定の詳細ログをコンソールに出力するかどうか。',
})
public logDebug: boolean = false;
// 内部用のシード値(擬似乱数生成に使用)
private _internalSeed: number = 1;
onLoad() {
// 初期シードの設定
this._internalSeed = this.seed;
// 防御的チェック:パラメータの範囲を補正
if (this.agility < 0) {
console.warn('[EvasionCalculator] agility が 0 未満だったため、0 に補正しました。');
this.agility = 0;
}
if (this.baseEvasion < 0 || this.baseEvasion > 1) {
console.warn('[EvasionCalculator] baseEvasion は 0〜1 の範囲にある必要があります。値をクランプします。');
this.baseEvasion = clamp01(this.baseEvasion);
}
if (this.agilityFactor < 0) {
console.warn('[EvasionCalculator] agilityFactor が 0 未満だったため、0 に補正しました。');
this.agilityFactor = 0;
}
if (this.maxEvasion < 0 || this.maxEvasion > 1) {
console.warn('[EvasionCalculator] maxEvasion は 0〜1 の範囲にある必要があります。値をクランプします。');
this.maxEvasion = clamp01(this.maxEvasion);
}
}
start() {
// 特に必須コンポーネントはないため、ここでは何もしません。
// 必要に応じて、このノードの名前などをログ出力できます。
if (this.logDebug) {
console.log(`[EvasionCalculator] 初期化完了。Node="${this.node.name}", agility=${this.agility}`);
}
}
/**
* 現在のステータスに基づいて回避率(0〜1)を計算して返します。
*/
public getCurrentEvasionRate(): number {
const rate = this.baseEvasion + this.agility * this.agilityFactor;
const clamped = Math.min(Math.max(rate, 0), this.maxEvasion); // 0〜maxEvasion にクランプ
return clamped;
}
/**
* 1回分の回避判定を行います。
*
* @returns true の場合は回避成功(Miss)、false の場合は命中。
*/
public tryEvade(): boolean {
const evasionRate = this.getCurrentEvasionRate();
const r = this._nextRandom();
const isEvaded = r < evasionRate;
if (this.logDebug) {
console.log(
`[EvasionCalculator] Node="${this.node.name}" ` +
`rate=${(evasionRate * 100).toFixed(1)}%, rand=${r.toFixed(4)}, result=${isEvaded ? 'EVADE' : 'HIT'}`
);
}
return isEvaded;
}
/**
* ダメージ値を受け取り、回避判定後のダメージを返します。
*
* @param damage 元のダメージ値
* @returns 回避成功なら 0、失敗なら元の damage
*/
public rollDamageAfterEvasion(damage: number): number {
if (damage <= 0) {
// 0 以下のダメージはそのまま返す(回避判定は行わない)
return damage;
}
const evaded = this.tryEvade();
return evaded ? 0 : damage;
}
/**
* 擬似乱数を生成します。
* useSeed が true の場合は、シードを用いた線形合同法を使用。
* false の場合は、Cocos の random() を使用します。
*/
private _nextRandom(): number {
if (!this.useSeed) {
// Cocos の random() は 0〜1 の乱数を返す
return random();
}
// --- 線形合同法 (Linear Congruential Generator) ---
// X_{n+1} = (a * X_n + c) mod m
// ここではよく使われるパラメータを採用
const a = 1664525;
const c = 1013904223;
const m = 0x100000000; // 2^32
this._internalSeed = (a * this._internalSeed + c) % m;
// 0〜1 に正規化して返す
return this._internalSeed / m;
}
}
コードのポイント解説
- onLoad
- インスペクタで設定された値に対して、防御的な補正を行っています。
- 範囲外の値が入っていた場合は、警告を出しつつ補正(クランプ)します。
- 乱数用の内部シード
_internalSeedを初期化します。
- start
- 必須コンポーネントがないため、特別な処理はしていません。
logDebugが ON の場合のみ、初期化ログを出力します。
- getCurrentEvasionRate
baseEvasion + agility * agilityFactorで回避率を計算します。- 結果は
0〜maxEvasionの範囲にクランプされます。 - 返り値は
0〜1の数値(0.25 なら 25%)。
- tryEvade
- 現在の回避率を取得し、
_nextRandom()で 0〜1 の乱数を生成します。 rand < rateなら回避成功(true)、そうでなければ命中(false)。logDebugが ON の場合、回避率・乱数・結果をログに出します。
- 現在の回避率を取得し、
- rollDamageAfterEvasion
- ダメージ値を受け取り、内部で
tryEvade()を呼び出します。 - 回避成功なら 0 を返し、失敗なら元のダメージをそのまま返します。
- 攻撃側から見ると、「最後にこの関数を通すだけ」で回避処理を組み込めます。
- ダメージ値を受け取り、内部で
- _nextRandom
useSeed = falseのときは Cocos のrandom()を使用。useSeed = trueのときは、線形合同法による擬似乱数生成を使い、毎回同じシードから同じ乱数列を生成します。- テスト時に「同じシチュエーションで毎回同じ結果にしたい」ときに便利です。
使用手順と動作確認
1. スクリプトファイルの作成
- エディタの Assets パネルで、回避用スクリプトを置きたいフォルダ(例:
assets/scripts)を選択します。 - そのフォルダ上で右クリックし、Create > TypeScript を選択します。
- 新しく作成されたファイル名を
EvasionCalculator.tsに変更します。 - ダブルクリックしてエディタ(VS Code など)で開き、中身をすべて削除してから、前述のコードをそのまま貼り付けて保存します。
2. テスト用ノードの作成
- Hierarchy パネルで右クリックし、Create > 3D Object > Node もしくは Create > 2D Object > Sprite など、適当なテスト用ノードを作成します。
- 名前はわかりやすく
TestCharacterなどにすると良いでしょう。
- 名前はわかりやすく
3. コンポーネントのアタッチ
- 作成した
TestCharacterノードを選択します。 - Inspector パネル下部の Add Component ボタンをクリックします。
- 表示されるメニューから Custom > EvasionCalculator を選択します。
- リストに表示されない場合は、スクリプトの保存やビルドが完了していない可能性があるので、少し待ってから再度確認してください。
4. プロパティの設定例
TestCharacter ノードにアタッチされた EvasionCalculator のプロパティを、Inspector で次のように設定してみます。
agility: 20baseEvasion: 0.05(5%)agilityFactor: 0.01(Agility1につき+1%)maxEvasion: 0.9(90%)useSeed: ONseed: 1logDebug: ON
この設定だと、回避率は:
baseEvasion + agility * agilityFactor
= 0.05 + 20 * 0.01
= 0.25 (25%)
となり、maxEvasion = 0.9 なのでクランプはかからず、25%の確率で回避するキャラクターになります。
5. 実際に回避判定を呼び出してみる(簡易テスト)
このコンポーネントは「アタッチするだけ」で使えますが、挙動を確認するために簡単なテストスクリプトを追加してもよいでしょう。
(※以下はあくまでテスト用の例であり、本ガイドのコンポーネント自体は他のスクリプトに依存していません)
- Assets パネルで右クリック → Create > TypeScript を選択し、
EvasionTest.tsという名前で作成します。 - 中身を以下のようにします:
import { _decorator, Component } from 'cc';
import { EvasionCalculator } from './EvasionCalculator';
const { ccclass, property } = _decorator;
@ccclass('EvasionTest')
export class EvasionTest extends Component {
@property(EvasionCalculator)
public evasion!: EvasionCalculator;
start() {
if (!this.evasion) {
this.evasion = this.getComponent(EvasionCalculator)!;
}
if (!this.evasion) {
console.error('[EvasionTest] EvasionCalculator が見つかりません。ノードにアタッチされているか確認してください。');
return;
}
const trials = 20;
let evadeCount = 0;
for (let i = 0; i < trials; i++) {
if (this.evasion.tryEvade()) {
evadeCount++;
}
}
console.log(
`[EvasionTest] 試行回数=${trials}, 回避回数=${evadeCount}, ` +
`実測回避率=${(evadeCount / trials * 100).toFixed(1)}%`
);
}
}
TestCharacterノードに EvasionTest コンポーネントも追加し、evasionプロパティに同じノード上のEvasionCalculatorをドラッグ&ドロップで割り当てます。- エディタ右上の ▶(再生) ボタンでプレビューを開始し、Console タブを確認します。
[EvasionCalculator]と[EvasionTest]のログが表示され、回避率や試行結果が見えるはずです。
これにより、設定した Agility と係数に応じて、期待通りの確率で回避しているかを簡単に確認できます。
6. 実際のゲームロジックでの使い方イメージ
ゲーム側のダメージ処理では、次のような流れを想定しています:
- 攻撃力や防御力などから 最終ダメージ を計算する。
- ダメージ適用先のノードにアタッチされた
EvasionCalculatorを取得する。 rollDamageAfterEvasion(finalDamage)を呼び出し、戻り値を実際に適用するダメージとする。- 戻り値が
0なら「Miss」表示を出すなどの演出を行う。
例(攻撃側スクリプト内のイメージ):
// targetNode: 攻撃対象のノード
const evasion = targetNode.getComponent(EvasionCalculator);
let finalDamage = 50; // 何らかの計算結果
if (evasion) {
finalDamage = evasion.rollDamageAfterEvasion(finalDamage);
}
if (finalDamage <= 0) {
// 回避された(Miss)
// → 「Miss」エフェクトやログを表示
} else {
// ダメージをHPから減算
}
このように、「最後に EvasionCalculator を通すだけ」という設計にしておくと、他のロジックと疎結合に保ちながら回避システムを導入できます。
まとめ
EvasionCalculatorは、Agility に基づいて回避率を計算し、攻撃を確率的に無効化する汎用コンポーネントです。- 外部の GameManager やステータス管理クラスに依存せず、このスクリプト単体で完結するように設計しました。
- インスペクタから Agility / 基本回避率 / 上限 / 係数 / デバッグ用シード を調整でき、ゲームバランスの調整が容易です。
- 公開メソッド
tryEvade()とrollDamageAfterEvasion()により、既存のダメージ計算ロジックに簡単に組み込めます。 logDebugとシード固定機能により、回避判定の挙動を視覚的・再現性高く検証できるため、テストやデバッグも効率的です。
このコンポーネントをベースに、
- 装備やバフによる Agility 増減
- 状態異常による回避率ダウン(例:スタン中は回避率0)
- 攻撃側の「命中率」との対抗関係(命中 – 回避)
などを追加していけば、よりリッチなバトルシステムを構築できます。
まずはこの EvasionCalculator をプロジェクトに組み込んで、回避システムをシンプルに導入してみてください。




