【Cocos Creator 3.8】DropTable の実装:アタッチするだけで「レアリティ付きドロップ抽選&アイテム生成」を実現する汎用スクリプト
この記事では、敵などの「倒されたタイミング」でレアリティ別に設定したテーブルからアイテムを抽選して生成する、汎用ドロップテーブルコンポーネント DropTable を実装します。
このコンポーネントは、
- 敵ノードにアタッチしておくだけで
- メソッドを1回呼ぶだけで(例:
drop()) - 設定した確率・レアリティに基づいて、プレハブ(Prefab)を自動生成
できるように設計します。敵死亡処理の中から drop() を呼ぶだけで機能するため、複雑なマネージャやシングルトンは不要です。
コンポーネントの設計方針
要件整理
- 敵が死亡したタイミングでドロップ抽選を行う。
- レアリティ(例:Common / Rare / Epic など)ごとにアイテム候補をまとめて管理したい。
- 各レアリティごとに「出現確率(重み)」と「その中でのアイテムの抽選」を行う。
- アイテムは Prefab として指定し、敵の位置(またはオフセット位置)に生成する。
- 外部の GameManager やシングルトンに依存せず、このコンポーネント単体で完結させる。
- インスペクタからすべての設定を行えるようにする。
設計アプローチ
レアリティ別のドロップテーブルを表現するために、以下のような構造を用意します。
- DropItemEntry:1つのアイテム候補(Prefab とその重み)
- DropRarityEntry:1つのレアリティ(レアリティ名、レアリティ重み、そのレアリティ内のアイテム候補リスト)
- DropTable コンポーネント:
- レアリティのリストを持つ
- ドロップ回数や位置オフセットなどの制御プロパティを持つ
- 外部から呼ばれる
drop()メソッドを提供
抽選の流れは次の通りです。
- 有効なレアリティ(weight > 0 かつアイテム候補あり)から、重みによるランダム選択。
- 選ばれたレアリティの中のアイテム候補から、同様に重みによるランダム選択。
- 選ばれたアイテム 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);
}
}
}
コードのポイント解説
onLoaddropPositionNodeが未設定なら自動的にthis.nodeを基準ノードに設定。- レアリティやアイテム候補の簡易チェックを行い、設定ミスがあれば
console.warnで通知。
drop()- 外部(敵の死亡処理など)から呼び出す公開メソッド。
enabledDropやmaxDropCountをチェックし、不正な場合は警告して早期リターン。- ループで最大
maxDropCount回までドロップ抽選を行う。 allowDuplicateItemsがfalseの場合、同じ Prefab の重複ドロップを防止。
_rollRarity()- 有効なレアリティ(
weight > 0、アイテム候補の総weight > 0、dropChance > 0)のみを対象に重み付きランダム選択。
- 有効なレアリティ(
_rollItem()- 選択されたレアリティ内のアイテム候補から、Prefab と weight の条件を満たすものだけを対象に重み付きランダム選択。
allowDuplicateItemsがfalseの場合は、すでに選ばれた Prefab を除外。
_spawnItem()- 基準ノードの位置を取得し、
positionOffsetとrandomOffsetRadiusを適用して最終的な生成位置を決定。 useWorldSpaceがtrueの場合はワールド座標で配置し、基準ノードと同じ親の子として生成。falseの場合は基準ノードの子としてローカル座標で生成。
- 基準ノードの位置を取得し、
使用手順と動作確認
1. スクリプトの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を
DropTable.tsにします。 - 自動生成された中身をすべて削除し、本記事の
DropTableコード全文を貼り付けて保存します。
2. テスト用の敵ノードの用意
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite などを選択し、テスト用の敵ノードを作成します。
- ノード名の例:
TestEnemy
- ノード名の例:
- 必要に応じて Sprite やアニメーションなどを設定しておきます(必須ではありません)。
3. DropTable コンポーネントのアタッチ
- Hierarchy で
TestEnemyノードを選択します。 - Inspector の下部で Add Component → Custom → DropTable を選択します。
- Inspector に DropTable のプロパティが表示されます。
4. ドロップ用アイテム Prefab の作成
- Hierarchy で右クリック → Create → 2D Object → Sprite などで、アイテム用ノードを作成します。
- 例:
Coin、Potionなど。
- 例:
- 見た目やサイズを調整します。
- 作成したノードを Assets パネルへドラッグ&ドロップして Prefab 化します。
- 例:
Coin.prefab,Potion.prefab
- 例:
- Prefab 化後、シーン内の元ノードは削除しても構いません。
5. DropTable のレアリティとアイテム設定
TestEnemyノードを選択し、Inspector の DropTable コンポーネントを確認します。- 基本設定:
- Enabled Drop:チェックを入れたまま(true)。
- Max Drop Count:
3などお好みで設定。 - Allow Duplicate Items:とりあえず
trueにして、同じアイテムが複数出る挙動を確認しても良いです。 - Drop Position Node:空欄のままで OK(自動的に
TestEnemyが使われます)。 - Position Offset:
(0, -20, 0)などにすると、敵の少し下にドロップします。 - Random Offset Radius:
30などにすると、敵の周囲にランダムに散らばります。 - Use World Space:2Dゲームであれば基本的に
trueで問題ありません。
- レアリティ設定:
Raritiesの右側にある+ボタンをクリックして、要素を1つ追加します。- 1つ目のレアリティ例:
- Name:
Common - Weight:
80 - Drop Chance:
1.0(必ず何かが落ちる) - Items:
+ボタンでアイテムを追加。- Prefab に
Coin.prefabをドラッグ&ドロップ。 - Weight:
1
- Name:
- 2つ目のレアリティ例:
Raritiesのサイズを 2 にし、2つ目を設定。- Name:
Rare - Weight:
20 - Drop Chance:
0.3(30%の確率でレア枠から何かが落ちる) - Items:
+ボタンでアイテムを追加。- Prefab に
Potion.prefabをドラッグ&ドロップ。 - Weight:
1
この設定例では、1回の drop() 呼び出しにつき最大3個までアイテムが生成され、
- Common レアリティは必ず何かが落ちる(Coin)。
- Rare レアリティは 30% の確率で Potion が追加で落ちる。
6. 実際に drop() を呼び出してみる
テストのために、簡単なスクリプトから drop() を呼び出してみます。
- Assets パネルで右クリック → Create → TypeScript を選択し、
TestEnemyDeath.tsなどの名前で作成します。 - 以下のような簡易スクリプトを記述します(このスクリプトはテスト用なので、プロジェクト本番では自前の死亡処理スクリプトから
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();
}
}
}
}
TestEnemyノードに TestEnemyDeath コンポーネントを追加します。- Inspector → Add Component → Custom → TestEnemyDeath
DropTableプロパティは空欄でも OK(自動取得を試みます)。
- エディタ右上の Play ボタンでゲームを実行します。
- ゲームウィンドウが開いたら、キーボードの Space キーを押します。
- 設定に応じて、
TestEnemyの周囲に Coin や Potion の Prefab が生成されることを確認します。
これで、敵ノードに DropTable をアタッチして drop() を呼ぶだけで、レアリティ付きドロップが簡単に実現できることが確認できます。
まとめ
本記事では、Cocos Creator 3.8 / TypeScript で、
- 外部の GameManager やシングルトンに一切依存せず
- 敵ノードにアタッチするだけで使える
- レアリティ別のドロップテーブルからアイテムを抽選・生成する
汎用コンポーネント DropTable を実装しました。
このコンポーネントを使うことで、
- 敵ごとに異なるドロップ構成(ボスはレア率高め、雑魚はコモンのみ等)を Inspector から簡単に調整できる。
- 重みと確率をいじるだけで、ゲームバランス調整がしやすい。
- Prefab を差し替えるだけで、ドロップ内容を差し替え可能。
- スクリプト側の死亡処理は「
this.getComponent(DropTable)?.drop();」と書くだけで済む。
応用例としては、
- 宝箱やガチャ演出に流用する(クリック時に
drop()を呼ぶ)。 - ステージクリア報酬として、特定ノードからアイテムをドロップさせる。
- 敵の種類ごとに DropTable プリセットを Prefab 化し、再利用する。
といった使い方も可能です。
プロジェクト内で共通の「ドロップ処理」を1つのコンポーネントにまとめておくことで、ゲーム全体の設計がシンプルになり、調整もしやすくなります。
あとは、実際のゲーム仕様に合わせてレアリティ構成やアイテム Prefab を増やしたり、ドロップ時のエフェクトやサウンド再生を追加して、よりリッチな演出へ発展させてみてください。




