【Cocos Creator 3.8】LootDropper の実装:ノードが破棄される直前に確率でアイテムプレハブを生成する汎用スクリプト
この記事では、敵キャラや壊れるオブジェクトなどのノードにアタッチしておくだけで、「そのノードが破棄される直前に、一定確率でアイテムプレハブをその場に生成」できる汎用コンポーネント LootDropper を実装します。
ゲーム中のさまざまな「消えるオブジェクト」にそのまま付け替えて使えるよう、完全に独立した設計にし、外部の GameManager などには一切依存しません。
コンポーネントの設計方針
基本コンセプト
- このコンポーネントがアタッチされたノードが「破棄される直前」に一度だけ処理を行う。
- 指定した確率で、指定した「アイテム用プレハブ」をインスタンス化し、元ノードと同じ位置(およびオプションで同じ親)に配置する。
- ドロップ候補を複数登録し、「確率付きテーブル」から1つをランダム選択できる。
- 外部スクリプトに依存せず、すべてインスペクタで設定できる。
Cocos Creator には Godot の queue_free() に相当するメソッド名はありませんが、ノードを破棄する一般的な方法は node.destroy() です。Cocos Creator では、ノードが破棄される直前に onDestroy() ライフサイクルメソッドが呼ばれるため、ここで「ドロップ判定と生成」を行います。
LootDropper の動作要件
- ノードが
destroy()されたときにだけ発動する(シーン切り替えなどによる全体破棄も含む)。 - 1ノードにつき1回のみドロップ判定を行う(多重実行防止)。
- ドロップ確率を 0〜100% で指定可能。
- 複数のアイテム候補を設定し、それぞれに「重み(weight)」を持たせて抽選できる。
- インスタンス化したアイテムの親ノードを、以下から選択可能:
- 破棄されるノードの親(デフォルト)
- 任意に指定したノード
- ドロップ位置を元ノードの位置を基準に、少しランダムオフセットを加えられる。
インスペクタで設定可能なプロパティ
LootDropper には、以下のプロパティを用意します。
enabledDrop: boolean
ドロップ機能の有効/無効。テスト時などに一時的にオフにできます。dropChance: number
ドロップが発生する確率(0〜100)。
例:50 なら 50% の確率でドロップ。useWeightedTable: booleanfalse:singlePrefabのみをドロップ対象として扱う。true:lootTableから重み付きランダムで 1 つ選ぶ。
singlePrefab: Prefab | null
単一ドロップ用のプレハブ。
useWeightedTable = falseの時に使われます。lootTable: LootEntry[]
複数候補からランダム抽選するためのテーブル。
LootEntryは以下の2フィールドを持つシリアライズ可能クラス:prefab: Prefab | null… ドロップ候補のプレハブweight: number… 抽選の重み(相対値)。0以下は無効扱い。
overrideParent: boolean
ドロップしたアイテムの親ノードを上書きするかどうか。false:破棄されたノードの親をそのまま親にする。true:parentOverrideNodeを親として使用。
parentOverrideNode: Node | null
overrideParent = trueのとき、ドロップしたアイテムをぶら下げるノード。positionOffsetRange: Vec3
位置ランダムオフセットの最大値。
例:(0.3, 0.3, 0)とした場合、x, y それぞれ[-0.3, 0.3]の範囲でランダムにずらして生成します。logDebug: boolean
デバッグログをコンソールに出すかどうか。挙動確認時に有効にします。
この設計により、単純な「1種類だけのアイテムドロップ」から、「複数のアイテム候補を重み付きで抽選する」まで、同じコンポーネントで対応できるようになります。
TypeScriptコードの実装
以下が完成した LootDropper.ts の全コードです。
import { _decorator, Component, Node, Prefab, instantiate, Vec3, math } from 'cc';
const { ccclass, property } = _decorator;
/**
* 複数候補から重み付きで選ぶための1エントリ
*/
@ccclass('LootEntry')
export class LootEntry {
@property({
type: Prefab,
tooltip: 'ドロップ候補となるアイテムのPrefab',
})
prefab: Prefab | null = null;
@property({
tooltip: 'このアイテムが選ばれる相対的な重み(0以下は無効扱い)',
})
weight: number = 1;
}
@ccclass('LootDropper')
export class LootDropper extends Component {
@property({
tooltip: 'true のときのみドロップ処理を行います(一時的に無効化する用途)',
})
enabledDrop: boolean = true;
@property({
tooltip: 'ドロップが発生する確率(0〜100)。例:50 で 50% の確率でドロップ',
min: 0,
max: 100,
slide: true,
})
dropChance: number = 50;
@property({
tooltip: 'true の場合は lootTable から重み付きランダムで選択し、false の場合は singlePrefab のみを使用します',
})
useWeightedTable: boolean = false;
@property({
type: Prefab,
tooltip: '単一のドロップアイテムPrefab。useWeightedTable = false のときに使用されます',
})
singlePrefab: Prefab | null = null;
@property({
type: [LootEntry],
tooltip: '複数候補から重み付きで選ぶためのテーブル。useWeightedTable = true のときに使用されます',
})
lootTable: LootEntry[] = [];
@property({
tooltip: 'true の場合、ドロップしたアイテムの親ノードを parentOverrideNode に固定します',
})
overrideParent: boolean = false;
@property({
type: Node,
tooltip: 'overrideParent = true のとき、ドロップしたアイテムをぶら下げる親ノード',
})
parentOverrideNode: Node | null = null;
@property({
tooltip: 'ドロップ位置のランダムオフセット最大値。x,y,z それぞれ [-値, 値] の範囲でランダムにずらします',
})
positionOffsetRange: Vec3 = new Vec3(0, 0, 0);
@property({
tooltip: 'true にするとドロップ処理の詳細ログをコンソールに出力します',
})
logDebug: boolean = false;
// 多重実行防止フラグ
private _hasDropped: boolean = false;
onLoad() {
// 特に必須コンポーネントはないが、設定ミスを防ぐために軽いチェックを行う
if (!this.useWeightedTable && !this.singlePrefab) {
console.warn('[LootDropper] singlePrefab が設定されていません。useWeightedTable = false の場合、何もドロップされません。', this.node);
}
if (this.useWeightedTable && this.lootTable.length === 0) {
console.warn('[LootDropper] lootTable が空です。useWeightedTable = true の場合、何もドロップされません。', this.node);
}
if (this.overrideParent && !this.parentOverrideNode) {
console.warn('[LootDropper] overrideParent = true ですが parentOverrideNode が設定されていません。破棄されるノードの親が使われます。', this.node);
}
}
/**
* ノードが destroy() される直前に呼ばれるライフサイクル
* ここで一度だけドロップ処理を行う
*/
onDestroy() {
this.tryDropLoot();
}
/**
* 実際のドロップ処理本体
*/
private tryDropLoot() {
if (this._hasDropped) {
// 念のため多重実行を防ぐ
return;
}
this._hasDropped = true;
if (!this.enabledDrop) {
if (this.logDebug) {
console.log('[LootDropper] enabledDrop = false のためドロップ処理をスキップしました。', this.node.name);
}
return;
}
// 0〜100 の範囲で確率判定
const randomValue = Math.random() * 100;
if (randomValue > this.dropChance) {
if (this.logDebug) {
console.log(`[LootDropper] ドロップ失敗: random=${randomValue.toFixed(2)} / chance=${this.dropChance}`, this.node.name);
}
return;
}
// 実際にドロップするPrefabを決定
const prefabToSpawn = this.useWeightedTable
? this.pickFromLootTable()
: this.singlePrefab;
if (!prefabToSpawn) {
if (this.logDebug) {
console.log('[LootDropper] ドロップ対象のPrefabが設定されていないため、何も生成しません。', this.node.name);
}
return;
}
// 親ノードを決定
let parent: Node | null = null;
if (this.overrideParent && this.parentOverrideNode) {
parent = this.parentOverrideNode;
} else {
parent = this.node.parent;
}
if (!parent) {
console.warn('[LootDropper] ドロップ先の親ノードが存在しません。生成は行われますが、どこにもぶら下がりません。', this.node.name);
}
// Prefab をインスタンス化
const itemNode = instantiate(prefabToSpawn);
if (!itemNode) {
console.error('[LootDropper] Prefab のインスタンス化に失敗しました。', prefabToSpawn.name);
return;
}
// 親を設定
if (parent) {
itemNode.parent = parent;
}
// 位置を設定(元ノードのワールド座標を基準に、ランダムオフセットを加算)
const baseWorldPos = this.node.worldPosition.clone();
const offset = this.getRandomOffset();
const finalWorldPos = baseWorldPos.add(offset);
itemNode.setWorldPosition(finalWorldPos);
if (this.logDebug) {
console.log('[LootDropper] アイテムをドロップしました:', {
sourceNode: this.node.name,
spawnedNode: itemNode.name,
worldPosition: finalWorldPos,
});
}
}
/**
* lootTable から重み付きランダムで 1 つ選ぶ
*/
private pickFromLootTable(): Prefab | null {
if (!this.lootTable || this.lootTable.length === 0) {
return null;
}
// 有効なエントリのみ対象にする(prefab が null でない && weight > 0)
const validEntries = this.lootTable.filter(entry => entry.prefab && entry.weight > 0);
if (validEntries.length === 0) {
if (this.logDebug) {
console.log('[LootDropper] lootTable に有効なエントリがありません。', this.node.name);
}
return null;
}
// 総重みを計算
let totalWeight = 0;
for (const entry of validEntries) {
totalWeight += entry.weight;
}
// 0〜totalWeight の範囲でランダム値を取得し、その位置にあるエントリを選ぶ
let random = Math.random() * totalWeight;
for (const entry of validEntries) {
random -= entry.weight;
if (random <= 0) {
return entry.prefab!;
}
}
// 浮動小数誤差などでここまで来た場合は最後の要素を返す
return validEntries[validEntries.length - 1].prefab!;
}
/**
* positionOffsetRange をもとに [-range, range] のランダム Vec3 を返す
*/
private getRandomOffset(): Vec3 {
const rx = (Math.random() * 2 - 1) * this.positionOffsetRange.x;
const ry = (Math.random() * 2 - 1) * this.positionOffsetRange.y;
const rz = (Math.random() * 2 - 1) * this.positionOffsetRange.z;
return new Vec3(rx, ry, rz);
}
}
コードのポイント解説
onDestroy()
ノードがdestroy()される直前に呼ばれるので、ここからtryDropLoot()を一度だけ呼び出しています。_hasDropped
予期せぬ多重呼び出しに備えたセーフティ。tryDropLoot()冒頭でチェックしています。dropChanceと確率判定
Math.random() * 100で 0〜100 の乱数を生成し、dropChanceより大きければドロップ失敗としています。useWeightedTableとpickFromLootTable()falseの場合 …singlePrefabだけを使用。trueの場合 …lootTableのweightを合計し、その範囲でランダム値を取って重み付き抽選を行っています。
- 位置決定
this.node.worldPositionを基準に、getRandomOffset()で求めたランダムオフセットを加算し、itemNode.setWorldPosition()で配置しています。 - 親ノードの扱い
overrideParent = falseの場合 … 破棄されたノードのparentをそのまま使用。overrideParent = trueかつparentOverrideNodeが設定されている場合 … そこを親として使用。
- 防御的実装
- Prefab が設定されていない場合・テーブルが空の場合などは
console.warnやconsole.logで状況を明示。 - 親ノードが存在しないケースでは警告を出しつつ、ノード自体は生成する挙動にしています。
- Prefab が設定されていない場合・テーブルが空の場合などは
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を
LootDropper.tsにします。 - 自動生成されたコードをすべて削除し、本記事の
LootDropper.tsコードを丸ごと貼り付けて保存します。
2. テスト用のアイテムPrefabを用意する
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite(または 3D オブジェクト)などで、簡単なアイテム用ノードを作成します。
- そのノードに Sprite 画像などを設定して、見た目を分かりやすくしておきます。
- Assets パネルにそのノードをドラッグ&ドロップし、Prefab 化します(例:
Item_Coin.prefab)。 - 元のシーン上のノードは、テスト用に残しておくか削除しても構いません。
3. 敵(または壊れるオブジェクト)ノードの準備
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite などで、敵や箱などを想定したノードを作成します(例:
EnemyTest)。 - 任意で Collider やスクリプトなどを追加しても構いませんが、LootDropper 自体は何にも依存しません。
4. LootDropper コンポーネントをアタッチ
- Hierarchy でテスト用ノード(例:
EnemyTest)を選択します。 - Inspector で Add Component → Custom → LootDropper を選択してアタッチします。
5. 単一Prefabドロップの設定例
まずはシンプルな「1種類のアイテムを一定確率で落とす」パターンで確認します。
- Inspector 上の
LootDropperのプロパティを次のように設定します:- Enabled Drop: チェック ON(true)
- Drop Chance:
50(50% ドロップ) - Use Weighted Table: チェック OFF(false)
- Single Prefab: 先ほど作成した
Item_Coin.prefabをドラッグ&ドロップで指定 - Override Parent: チェック OFF(false)
→ ドロップしたアイテムはEnemyTestと同じ親にぶら下がります。 - Position Offset Range:
(0.3, 0.3, 0)など、少しだけランダムに揺らしたい値を設定 - Log Debug: テスト時はチェック ON にすると挙動が分かりやすいです。
6. 破棄テストのための簡易スクリプト(任意)
LootDropper は node.destroy() が呼ばれたときに発動します。手軽にテストするには、例えば次のような簡単なスクリプトを EnemyTest に追加して、キー入力で破棄する方法があります。
※このスクリプトはあくまでテスト用であり、LootDropper 自体はこのスクリプトに依存していません。
import { _decorator, Component, systemEvent, SystemEvent, EventKeyboard, KeyCode } from 'cc';
const { ccclass } = _decorator;
@ccclass('DestroyOnKey')
export class DestroyOnKey extends Component {
onEnable() {
systemEvent.on(SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
}
onDisable() {
systemEvent.off(SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
}
private onKeyDown(event: EventKeyboard) {
if (event.keyCode === KeyCode.SPACE) {
// スペースキーで自分を破棄
this.node.destroy();
}
}
}
- Assets で
DestroyOnKey.tsを作成し、上記コードを貼り付けます。 EnemyTestノードに Add Component → Custom → DestroyOnKey でアタッチします。
7. 実行して動作確認
- エディタ右上の Play ボタンでゲームを実行します。
- ゲームウィンドウが開いたら、スペースキー を押して
EnemyTestノードを破棄します。 Drop Chance = 50の場合、だいたい 2 回に 1 回の確率でItem_Coinが生成されるはずです。Log Debugを ON にしている場合、Console に次のようなログが出ます:- ドロップ成功時:
[LootDropper] アイテムをドロップしました: { sourceNode: "EnemyTest", ... } - ドロップ失敗時:
[LootDropper] ドロップ失敗: random=72.34 / chance=50
- ドロップ成功時:
8. 複数アイテムからの重み付きドロップ設定例
次に、複数のアイテム候補から「レア度に応じて確率を変える」ような設定を行ってみます。
- まず、複数のアイテムPrefabを用意します:
Item_Coin.prefabItem_Gem.prefabItem_Heart.prefab
EnemyTestのLootDropper設定を次のように変更します:- Use Weighted Table: チェック ON(true)
- Single Prefab: 空のままで OK(使われません)
- Loot Table:
- サイズを
3に設定。 - Element 0:
- Prefab:
Item_Coin.prefab - Weight:
70(よく出る)
- Prefab:
- Element 1:
- Prefab:
Item_Gem.prefab - Weight:
25
- Prefab:
- Element 2:
- Prefab:
Item_Heart.prefab - Weight:
5(レア)
- Prefab:
- サイズを
- Drop Chance:
100にすると、破棄のたびに何かしら 1 つは必ずドロップされます。
- 再度ゲームを実行し、何度か
EnemyTestを破棄してみて、コイン・ジェム・ハートの出現比率が概ね 70:25:5 になっていることを確認します。
まとめ
本記事では、Cocos Creator 3.8 / TypeScript で、ノードが破棄される直前に確率でアイテムPrefabを生成する汎用コンポーネント LootDropper を実装しました。
LootDropperを任意のノードにアタッチし、Prefab と確率をインスペクタから設定するだけで、簡単にドロップ演出を追加できます。- 単一Prefabだけのシンプルなドロップから、複数アイテムを重み付きで抽選するリッチな仕様まで、同じコンポーネントで対応できます。
- 外部の GameManager などには一切依存せず、このスクリプト単体で完結しているため、どのプロジェクトにもコピー&ペーストで持ち込めます。
- 防御的な実装とデバッグログにより、設定ミスや挙動確認も行いやすくなっています。
敵キャラ、壊れるオブジェクト、宝箱、草むらなど、「消えるときに何かを落としたい」あらゆるノードにそのまま使い回せるので、ゲーム全体の報酬設計やバランス調整が格段にやりやすくなるはずです。
あとは各Prefab側でアニメーションやエフェクト、サウンドを付けていけば、よりリッチなドロップ演出を簡単に構築できます。




