【Cocos Creator】アタッチするだけ!DropTable (ドロップテーブル)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】DropTable の実装:アタッチするだけで「レアリティ付きドロップ抽選&アイテム生成」を実現する汎用スクリプト

この記事では、敵などの「倒されたタイミング」でレアリティ別に設定したテーブルからアイテムを抽選して生成する、汎用ドロップテーブルコンポーネント DropTable を実装します。

このコンポーネントは、

  • 敵ノードにアタッチしておくだけで
  • メソッドを1回呼ぶだけで(例:drop()
  • 設定した確率・レアリティに基づいて、プレハブ(Prefab)を自動生成

できるように設計します。敵死亡処理の中から drop() を呼ぶだけで機能するため、複雑なマネージャやシングルトンは不要です。


コンポーネントの設計方針

要件整理

  • 敵が死亡したタイミングでドロップ抽選を行う。
  • レアリティ(例:Common / Rare / Epic など)ごとにアイテム候補をまとめて管理したい。
  • 各レアリティごとに「出現確率(重み)」と「その中でのアイテムの抽選」を行う。
  • アイテムは Prefab として指定し、敵の位置(またはオフセット位置)に生成する。
  • 外部の GameManager やシングルトンに依存せず、このコンポーネント単体で完結させる。
  • インスペクタからすべての設定を行えるようにする。

設計アプローチ

レアリティ別のドロップテーブルを表現するために、以下のような構造を用意します。

  • DropItemEntry:1つのアイテム候補(Prefab とその重み)
  • DropRarityEntry:1つのレアリティ(レアリティ名、レアリティ重み、そのレアリティ内のアイテム候補リスト)
  • DropTable コンポーネント:
    • レアリティのリストを持つ
    • ドロップ回数や位置オフセットなどの制御プロパティを持つ
    • 外部から呼ばれる drop() メソッドを提供

抽選の流れは次の通りです。

  1. 有効なレアリティ(weight > 0 かつアイテム候補あり)から、重みによるランダム選択。
  2. 選ばれたレアリティの中のアイテム候補から、同様に重みによるランダム選択。
  3. 選ばれたアイテム Prefab を、指定ノード(デフォルトはこのコンポーネントが付いているノード)の位置+オフセットに instantiate して配置。

インスペクタで設定可能なプロパティ

以下のプロパティを設計します。

  • enabledDrop (boolean)
    • ドロップ機能の有効/無効。
    • 一時的にドロップを止めたい場合に使用。
  • maxDropCount (number)
    • drop() 1回の呼び出しで、最大いくつのアイテムを生成するか。
    • 例:1 なら1個だけ。3 なら最大3個まで抽選・生成。
  • allowDuplicateItems (boolean)
    • 同じ Prefab が1回のドロップ内で複数回出ても良いか。
    • false の場合、同じ Prefab は1回の drop() 呼び出し内で1つまで。
  • dropPositionNode (Node | null)
    • ドロップ位置の基準ノード。
    • 未設定(null)の場合は、このコンポーネントがアタッチされているノードの位置を使用。
  • positionOffset (Vec3)
    • 基準ノードの位置からのオフセット。
    • 例:(0, -20, 0) で足元に落とすイメージ。
  • randomOffsetRadius (number)
    • ランダムな散らばり半径(2DならX,Y平面を想定)。
    • 0 の場合は完全に同じ位置に重なって生成。
    • 例:30 なら、半径30の円内でランダムに配置。
  • useWorldSpace (boolean)
    • true:世界座標で位置を決定し、生成アイテムは同じ親ノード配下に生成。
    • false:基準ノードのローカル座標系で位置を決定し、基準ノードの子として生成。
  • rarities (DropRarityEntry[] / @property({ type: [DropRarityEntry] }))
    • レアリティごとの設定リスト。
    • 各レアリティは以下のフィールドを持つ:
      • name (string):レアリティ名(例:Common, Rare, Epic)。
      • weight (number):レアリティの抽選重み(0 の場合は抽選対象外)。
      • dropChance (number):このレアリティから「何かが落ちる」確率(0〜1の範囲)。
      • items (DropItemEntry[]):このレアリティ内のアイテム候補リスト。
  • DropItemEntry(内部クラス)
    • prefab (Prefab):生成するアイテム。
    • weight (number):このアイテムの抽選重み。

また、防御的な実装として:

  • レアリティやアイテム候補が1つも設定されていない場合、drop() 呼び出し時に警告ログを出す。
  • Prefab が設定されていないアイテム候補はスキップし、最後に有効な候補がなければ警告ログ。
  • エディタ上での設定ミスを検出しやすいよう、onLoad 時にも簡単なバリデーションログを出す。

TypeScriptコードの実装


import { _decorator, Component, Node, Prefab, instantiate, Vec3, math, CCFloat, CCBoolean, CCInteger } from 'cc';
const { ccclass, property } = _decorator;

/**
 * 1つのアイテム候補を表すエントリ
 */
@ccclass('DropItemEntry')
export class DropItemEntry {

    @property({
        type: Prefab,
        tooltip: 'ドロップさせるアイテムのPrefab。未設定の場合、この候補は無視されます。',
    })
    public prefab: Prefab | null = null;

    @property({
        type: CCFloat,
        tooltip: 'このアイテムの抽選重み。大きいほど選ばれやすくなります。0以下は無効。',
    })
    public weight: number = 1;
}

/**
 * レアリティ単位のドロップ設定
 */
@ccclass('DropRarityEntry')
export class DropRarityEntry {

    @property({
        tooltip: 'レアリティ名(例: Common, Rare, Epic)。ログやデバッグ用の識別名です。',
    })
    public name: string = 'Common';

    @property({
        type: CCFloat,
        tooltip: 'このレアリティ自体の抽選重み。大きいほど選ばれやすくなります。0以下は無効。',
    })
    public weight: number = 1;

    @property({
        type: CCFloat,
        tooltip: 'このレアリティから「何かが落ちる」確率 (0〜1)。1なら必ず何かが落ちます。',
        range: [0, 1, 0.01],
        slide: true,
    })
    public dropChance: number = 1.0;

    @property({
        type: [DropItemEntry],
        tooltip: 'このレアリティでドロップするアイテム候補リスト。',
    })
    public items: DropItemEntry[] = [];
}

/**
 * DropTable
 * 敵死亡時などに drop() を呼び出すことで、
 * レアリティとアイテム候補からランダムにPrefabを生成する汎用コンポーネント。
 */
@ccclass('DropTable')
export class DropTable extends Component {

    @property({
        type: CCBoolean,
        tooltip: 'ドロップ機能を有効にするかどうか。falseの場合、drop()を呼んでも何も起こりません。',
    })
    public enabledDrop: boolean = true;

    @property({
        type: CCInteger,
        tooltip: '1回のdrop()呼び出しで生成するアイテムの最大数。0以下ならドロップしません。',
        min: 0,
    })
    public maxDropCount: number = 1;

    @property({
        type: CCBoolean,
        tooltip: '1回のdrop()内で同じPrefabを複数回ドロップすることを許可するかどうか。',
    })
    public allowDuplicateItems: boolean = true;

    @property({
        type: Node,
        tooltip: 'ドロップ位置の基準となるノード。未設定の場合、このコンポーネントが付いているノードが基準になります。',
    })
    public dropPositionNode: Node | null = null;

    @property({
        tooltip: '基準ノードの位置からのオフセット。例: (0, -20, 0) で足元に落とすイメージ。',
    })
    public positionOffset: Vec3 = new Vec3(0, 0, 0);

    @property({
        type: CCFloat,
        tooltip: 'ランダムな散らばり半径。0なら完全に同じ位置に生成。2DゲームではXY平面内で散らばります。',
        min: 0,
    })
    public randomOffsetRadius: number = 0;

    @property({
        type: CCBoolean,
        tooltip: 'true: ワールド座標で位置決定し、基準ノードと同じ親に生成。false: 基準ノードのローカル座標として子ノードに生成。',
    })
    public useWorldSpace: boolean = true;

    @property({
        type: [DropRarityEntry],
        tooltip: 'レアリティごとのドロップ設定リスト。上から順に評価されるわけではなく、weightによるランダム抽選です。',
    })
    public rarities: DropRarityEntry[] = [];

    /**
     * onLoad: 簡易バリデーションとログ
     */
    onLoad() {
        if (!this.dropPositionNode) {
            this.dropPositionNode = this.node;
        }

        if (this.rarities.length === 0) {
            console.warn('[DropTable] レアリティが1つも設定されていません。Inspectorでraritiesを設定してください。', this.node.name);
        } else {
            // レアリティとアイテムの簡易チェック
            let validRarityCount = 0;
            for (const rarity of this.rarities) {
                const totalItemWeight = this._getTotalItemWeight(rarity);
                if (rarity.weight > 0 && totalItemWeight > 0) {
                    validRarityCount++;
                }
            }
            if (validRarityCount === 0) {
                console.warn('[DropTable] 有効なレアリティ/アイテム候補がありません。weightやPrefab設定を確認してください。', this.node.name);
            }
        }
    }

    /**
     * 敵死亡時などに外部から呼び出すメソッド。
     * 設定に基づき、最大 maxDropCount 個のアイテムを生成します。
     */
    public drop(): void {
        if (!this.enabledDrop) {
            return;
        }

        if (this.maxDropCount <= 0) {
            console.warn('[DropTable] maxDropCountが0以下のため、ドロップは行われません。', this.node.name);
            return;
        }

        if (!this.dropPositionNode) {
            this.dropPositionNode = this.node;
        }

        if (!this.dropPositionNode) {
            console.error('[DropTable] ドロップ位置ノードが取得できませんでした。', this.node.name);
            return;
        }

        const createdPrefabs: Prefab[] = [];

        for (let i = 0; i < this.maxDropCount; i++) {
            const rarity = this._rollRarity();
            if (!rarity) {
                // 有効なレアリティがなければ、その回のドロップはスキップ
                continue;
            }

            // このレアリティで何かが落ちるかを判定
            if (!this._rollDropChance(rarity.dropChance)) {
                continue;
            }

            const itemEntry = this._rollItem(rarity, createdPrefabs);
            if (!itemEntry || !itemEntry.prefab) {
                continue;
            }

            // allowDuplicateItemsがfalseなら、同じPrefabを次回以降選ばないように記録
            if (!this.allowDuplicateItems) {
                createdPrefabs.push(itemEntry.prefab);
            }

            this._spawnItem(itemEntry.prefab);
        }
    }

    /**
     * レアリティを重みに基づいてランダムに選択
     */
    private _rollRarity(): DropRarityEntry | null {
        const validRarities: DropRarityEntry[] = [];
        let totalWeight = 0;

        for (const rarity of this.rarities) {
            const itemWeight = this._getTotalItemWeight(rarity);
            if (rarity.weight > 0 && itemWeight > 0 && rarity.dropChance > 0) {
                validRarities.push(rarity);
                totalWeight += rarity.weight;
            }
        }

        if (validRarities.length === 0 || totalWeight <= 0) {
            // 有効なレアリティがない
            return null;
        }

        const r = Math.random() * totalWeight;
        let acc = 0;
        for (const rarity of validRarities) {
            acc += rarity.weight;
            if (r <= acc) {
                return rarity;
            }
        }

        return validRarities[validRarities.length - 1];
    }

    /**
     * レアリティ内のアイテム候補から重みに基づいてランダム選択
     */
    private _rollItem(rarity: DropRarityEntry, alreadySelected: Prefab[]): DropItemEntry | null {
        const validItems: DropItemEntry[] = [];
        let totalWeight = 0;

        for (const item of rarity.items) {
            if (!item.prefab) {
                continue;
            }
            if (item.weight <= 0) {
                continue;
            }
            if (!this.allowDuplicateItems && alreadySelected.includes(item.prefab)) {
                continue;
            }
            validItems.push(item);
            totalWeight += item.weight;
        }

        if (validItems.length === 0 || totalWeight <= 0) {
            return null;
        }

        const r = Math.random() * totalWeight;
        let acc = 0;
        for (const item of validItems) {
            acc += item.weight;
            if (r <= acc) {
                return item;
            }
        }

        return validItems[validItems.length - 1];
    }

    /**
     * レアリティ内の有効なアイテムの総weightを計算
     */
    private _getTotalItemWeight(rarity: DropRarityEntry): number {
        let total = 0;
        for (const item of rarity.items) {
            if (item.prefab && item.weight > 0) {
                total += item.weight;
            }
        }
        return total;
    }

    /**
     * dropChance(0〜1)に基づき、何かが落ちるかどうかを判定
     */
    private _rollDropChance(chance: number): boolean {
        if (chance <= 0) {
            return false;
        }
        if (chance >= 1) {
            return true;
        }
        return Math.random() <= chance;
    }

    /**
     * Prefabを生成して配置
     */
    private _spawnItem(prefab: Prefab): void {
        const baseNode = this.dropPositionNode ?? this.node;
        if (!baseNode) {
            console.error('[DropTable] ドロップ位置ノードが存在しません。', this.node.name);
            return;
        }

        const instance = instantiate(prefab);

        // 基準位置の取得
        let basePos = new Vec3();
        if (this.useWorldSpace) {
            baseNode.getWorldPosition(basePos);
        } else {
            basePos = baseNode.position.clone();
        }

        // オフセットの適用
        basePos.add(this.positionOffset);

        // ランダムな散らばり
        if (this.randomOffsetRadius > 0) {
            const angle = Math.random() * Math.PI * 2;
            const radius = Math.random() * this.randomOffsetRadius;
            const offsetX = Math.cos(angle) * radius;
            const offsetY = Math.sin(angle) * radius;
            // 2D想定: XY平面
            basePos.x += offsetX;
            basePos.y += offsetY;
        }

        if (this.useWorldSpace) {
            // 同じ親ノードの子として生成し、ワールド座標を維持
            const parent = baseNode.parent;
            if (!parent) {
                console.warn('[DropTable] 基準ノードに親がいないため、生成されたアイテムはシーンルートに配置されます。', baseNode.name);
            }
            instance.parent = parent;
            instance.setWorldPosition(basePos);
        } else {
            // 基準ノードの子としてローカル座標で生成
            instance.parent = baseNode;
            instance.setPosition(basePos);
        }
    }
}

コードのポイント解説

  • onLoad
    • dropPositionNode が未設定なら自動的に this.node を基準ノードに設定。
    • レアリティやアイテム候補の簡易チェックを行い、設定ミスがあれば console.warn で通知。
  • drop()
    • 外部(敵の死亡処理など)から呼び出す公開メソッド。
    • enabledDropmaxDropCount をチェックし、不正な場合は警告して早期リターン。
    • ループで最大 maxDropCount 回までドロップ抽選を行う。
    • allowDuplicateItemsfalse の場合、同じ Prefab の重複ドロップを防止。
  • _rollRarity()
    • 有効なレアリティ(weight > 0、アイテム候補の総weight > 0、dropChance > 0)のみを対象に重み付きランダム選択。
  • _rollItem()
    • 選択されたレアリティ内のアイテム候補から、Prefab と weight の条件を満たすものだけを対象に重み付きランダム選択。
    • allowDuplicateItemsfalse の場合は、すでに選ばれた Prefab を除外。
  • _spawnItem()
    • 基準ノードの位置を取得し、positionOffsetrandomOffsetRadius を適用して最終的な生成位置を決定。
    • useWorldSpacetrue の場合はワールド座標で配置し、基準ノードと同じ親の子として生成。
    • false の場合は基準ノードの子としてローカル座標で生成。

使用手順と動作確認

1. スクリプトの作成

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. ファイル名を DropTable.ts にします。
  3. 自動生成された中身をすべて削除し、本記事の DropTable コード全文を貼り付けて保存します。

2. テスト用の敵ノードの用意

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite などを選択し、テスト用の敵ノードを作成します。
    • ノード名の例:TestEnemy
  2. 必要に応じて Sprite やアニメーションなどを設定しておきます(必須ではありません)。

3. DropTable コンポーネントのアタッチ

  1. Hierarchy で TestEnemy ノードを選択します。
  2. Inspector の下部で Add ComponentCustomDropTable を選択します。
  3. Inspector に DropTable のプロパティが表示されます。

4. ドロップ用アイテム Prefab の作成

  1. Hierarchy で右クリック → Create → 2D Object → Sprite などで、アイテム用ノードを作成します。
    • 例:CoinPotion など。
  2. 見た目やサイズを調整します。
  3. 作成したノードを Assets パネルへドラッグ&ドロップして Prefab 化します。
    • 例:Coin.prefab, Potion.prefab
  4. Prefab 化後、シーン内の元ノードは削除しても構いません。

5. DropTable のレアリティとアイテム設定

  1. TestEnemy ノードを選択し、Inspector の DropTable コンポーネントを確認します。
  2. 基本設定
    • Enabled Drop:チェックを入れたまま(true)。
    • Max Drop Count3 などお好みで設定。
    • Allow Duplicate Items:とりあえず true にして、同じアイテムが複数出る挙動を確認しても良いです。
    • Drop Position Node:空欄のままで OK(自動的に TestEnemy が使われます)。
    • Position Offset(0, -20, 0) などにすると、敵の少し下にドロップします。
    • Random Offset Radius30 などにすると、敵の周囲にランダムに散らばります。
    • Use World Space:2Dゲームであれば基本的に true で問題ありません。
  3. レアリティ設定
    1. Rarities の右側にある + ボタンをクリックして、要素を1つ追加します。
    2. 1つ目のレアリティ例:
      • NameCommon
      • Weight80
      • Drop Chance1.0(必ず何かが落ちる)
      • Items
        1. + ボタンでアイテムを追加。
        2. PrefabCoin.prefab をドラッグ&ドロップ。
        3. Weight1
    3. 2つ目のレアリティ例:
      • Rarities のサイズを 2 にし、2つ目を設定。
      • NameRare
      • Weight20
      • Drop Chance0.3(30%の確率でレア枠から何かが落ちる)
      • Items
        1. + ボタンでアイテムを追加。
        2. PrefabPotion.prefab をドラッグ&ドロップ。
        3. Weight1

この設定例では、1回の drop() 呼び出しにつき最大3個までアイテムが生成され、

  • Common レアリティは必ず何かが落ちる(Coin)。
  • Rare レアリティは 30% の確率で Potion が追加で落ちる。

6. 実際に drop() を呼び出してみる

テストのために、簡単なスクリプトから drop() を呼び出してみます。

  1. Assets パネルで右クリック → Create → TypeScript を選択し、TestEnemyDeath.ts などの名前で作成します。
  2. 以下のような簡易スクリプトを記述します(このスクリプトはテスト用なので、プロジェクト本番では自前の死亡処理スクリプトから drop() を呼び出してください)。

import { _decorator, Component, input, Input, EventKeyboard, KeyCode } from 'cc';
import { DropTable } from './DropTable';
const { ccclass, property } = _decorator;

@ccclass('TestEnemyDeath')
export class TestEnemyDeath extends Component {

    @property({ type: DropTable, tooltip: '同じノード上のDropTableコンポーネントへの参照。未設定でもonLoadで自動取得を試みます。' })
    public dropTable: DropTable | null = null;

    onLoad() {
        if (!this.dropTable) {
            this.dropTable = this.getComponent(DropTable);
        }
        if (!this.dropTable) {
            console.error('[TestEnemyDeath] DropTableコンポーネントが見つかりません。TestEnemyノードにDropTableを追加してください。', this.node.name);
        }
    }

    start() {
        // キーボード入力を監視して、スペースキーで擬似的に死亡処理を発火
        input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
    }

    onDestroy() {
        input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
    }

    private onKeyDown(event: EventKeyboard) {
        if (event.keyCode === KeyCode.SPACE) {
            // 擬似的な死亡時処理:DropTableにドロップさせる
            if (this.dropTable) {
                this.dropTable.drop();
            }
        }
    }
}
  1. TestEnemy ノードに TestEnemyDeath コンポーネントを追加します。
    • Inspector → Add ComponentCustomTestEnemyDeath
    • DropTable プロパティは空欄でも OK(自動取得を試みます)。
  2. エディタ右上の Play ボタンでゲームを実行します。
  3. ゲームウィンドウが開いたら、キーボードの Space キーを押します。
  4. 設定に応じて、TestEnemy の周囲に Coin や Potion の Prefab が生成されることを確認します。

これで、敵ノードに DropTable をアタッチして drop() を呼ぶだけで、レアリティ付きドロップが簡単に実現できることが確認できます。


まとめ

本記事では、Cocos Creator 3.8 / TypeScript で、

  • 外部の GameManager やシングルトンに一切依存せず
  • 敵ノードにアタッチするだけで使える
  • レアリティ別のドロップテーブルからアイテムを抽選・生成する

汎用コンポーネント DropTable を実装しました。

このコンポーネントを使うことで、

  • 敵ごとに異なるドロップ構成(ボスはレア率高め、雑魚はコモンのみ等)を Inspector から簡単に調整できる。
  • 重みと確率をいじるだけで、ゲームバランス調整がしやすい。
  • Prefab を差し替えるだけで、ドロップ内容を差し替え可能。
  • スクリプト側の死亡処理は「this.getComponent(DropTable)?.drop();」と書くだけで済む。

応用例としては、

  • 宝箱やガチャ演出に流用する(クリック時に drop() を呼ぶ)。
  • ステージクリア報酬として、特定ノードからアイテムをドロップさせる。
  • 敵の種類ごとに DropTable プリセットを Prefab 化し、再利用する。

といった使い方も可能です。
プロジェクト内で共通の「ドロップ処理」を1つのコンポーネントにまとめておくことで、ゲーム全体の設計がシンプルになり、調整もしやすくなります。

あとは、実際のゲーム仕様に合わせてレアリティ構成やアイテム Prefab を増やしたり、ドロップ時のエフェクトやサウンド再生を追加して、よりリッチな演出へ発展させてみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!