Unityを触り始めた頃、「敵が死んだらアイテムを落とす」処理を全部 Update() や巨大な EnemyController クラスに書いてしまいがちですよね。

例えばこんな感じです:

// 典型的な「全部入り」アンチパターン例(あくまでダメな例)
public class EnemyController : MonoBehaviour
{
    public int hp;
    public GameObject coinPrefab;
    public GameObject potionPrefab;

    void Update()
    {
        // ダメージ処理
        // 移動処理
        // 攻撃処理
        // アニメーション処理
        // ...
        
        if (hp <= 0)
        {
            // ドロップ抽選もここにベタ書き
            float r = Random.value;
            if (r < 0.7f)
            {
                Instantiate(coinPrefab, transform.position, Quaternion.identity);
            }
            else
            {
                Instantiate(potionPrefab, transform.position, Quaternion.identity);
            }
            Destroy(gameObject);
        }
    }
}

これだと、

  • 敵の挙動とドロップ仕様が密結合していて、プレハブごとに調整しづらい
  • 「レアリティ」「重み付き抽選」などのロジックがコピペ地獄になりやすい
  • アイテム追加・確率変更のたびにスクリプトを開いて編集する必要がある

といった問題が出てきます。

そこでこの記事では、敵の死亡時に「レアリティごとに重み付き抽選」を行い、アイテムを生成する処理だけを切り出したコンポーネント 「DropTable(ドロップテーブル)」 を作っていきます。敵の死亡検知は別コンポーネントに任せて、「死んだら DropTable に通知する」だけのシンプルな構造を目指します。

【Unity】レアリティ別の重み付き抽選でスマートにドロップ!「DropTable」コンポーネント

コンポーネントの全体像

今回の DropTable は以下のような機能を持ちます:

  • レアリティごとに「重み付き」でアイテムを抽選
  • レアリティの中から、さらに「重み付き」でアイテムを抽選
  • ドロップしない(ハズレ)確率も設定可能
  • 敵の死亡イベントが来たら SpawnDrop() を呼ぶだけ

フルコード:DropTable.cs

using System;
using System.Collections.Generic;
using UnityEngine;

/// <summary>アイテムのレアリティ種別</summary>
public enum ItemRarity
{
    Common,
    Uncommon,
    Rare,
    Epic,
    Legendary
}

/// <summary>レアリティごとのドロップ候補1つ分の情報</summary>
[Serializable]
public class DropItemEntry
{
    [SerializeField] private string itemName;          // エディタ上での識別用(任意)
    [SerializeField] private GameObject prefab;        // 実際に生成するプレハブ
    [SerializeField] private float weight = 1f;        // 同レアリティ内での重み

    public string ItemName => itemName;
    public GameObject Prefab => prefab;
    public float Weight => Mathf.Max(0f, weight);
}

/// <summary>レアリティ単位の設定(レアリティ自体の重み + その中のアイテム一覧)</summary>
[Serializable]
public class RarityDropGroup
{
    [SerializeField] private ItemRarity rarity = ItemRarity.Common;
    [SerializeField, Tooltip("このレアリティが選ばれる確率の重み")] 
    private float rarityWeight = 1f;

    [SerializeField, Tooltip("このレアリティに属するドロップ候補一覧")]
    private List<DropItemEntry> items = new List<DropItemEntry>();

    public ItemRarity Rarity => rarity;
    public float RarityWeight => Mathf.Max(0f, rarityWeight);
    public IReadOnlyList<DropItemEntry> Items => items;

    /// <summary>このレアリティグループの中から 1 つアイテムを重み付きランダムで選ぶ</summary>
    public DropItemEntry GetRandomItem()
    {
        float totalWeight = 0f;
        foreach (var item in items)
        {
            totalWeight += item.Weight;
        }

        if (totalWeight <= 0f || items.Count == 0)
        {
            // 重みが全て 0 or アイテムがない場合は null を返す(このレアリティからはドロップしない)
            return null;
        }

        float r = UnityEngine.Random.value * totalWeight;
        float accum = 0f;

        foreach (var item in items)
        {
            accum += item.Weight;
            if (r <= accum)
            {
                return item;
            }
        }

        // 浮動小数点誤差対策として最後の要素を返す
        return items[items.Count - 1];
    }
}

/// <summary>
/// 敵死亡時にレアリティごとのテーブルからアイテムを抽選して生成するコンポーネント。
/// 
/// 使い方:敵オブジェクトにアタッチして、外部から SpawnDrop() を呼ぶ。
/// </summary>
public class DropTable : MonoBehaviour
{
    [Header("=== 全体設定 ===")]
    [SerializeField, Tooltip("ドロップの基礎確率(0〜1)。0なら絶対に落ちない、1なら必ず何かが落ちる。")]
    private float dropChance = 1f;

    [SerializeField, Tooltip("生成位置のオフセット(敵の中心からどれだけずらすか)")]
    private Vector3 spawnOffset = Vector3.zero;

    [SerializeField, Tooltip("生成時に少しランダムな揺らぎを加える範囲(X,Z)。0なら揺らぎなし。")]
    private Vector2 randomHorizontalOffsetRange = new Vector2(0.2f, 0.2f);

    [Header("=== レアリティテーブル ===")]
    [SerializeField, Tooltip("レアリティごとのドロップ設定一覧")]
    private List<RarityDropGroup> rarityGroups = new List<RarityDropGroup>();

    [Header("=== デバッグ ===")]
    [SerializeField, Tooltip("テスト用:Inspector から強制的に SpawnDrop() を呼ぶボタン代わりのフラグ")]
    private bool debugSpawnOnStart = false;

    private void Start()
    {
        // テスト用:ゲーム開始時に一度だけドロップを試す
        if (debugSpawnOnStart)
        {
            SpawnDrop();
        }
    }

    /// <summary>
    /// 外部(例:EnemyHealth コンポーネントなど)から呼び出して使う公開API。
    /// 敵が死亡したタイミングでこの関数を呼べば、ドロップ抽選と生成を行う。
    /// </summary>
    public void SpawnDrop()
    {
        // まず「何かを落とすかどうか」を判定
        if (!RollDropChance())
        {
            // 何も落とさない
            return;
        }

        // レアリティを重み付きランダムで選ぶ
        RarityDropGroup selectedRarityGroup = GetRandomRarityGroup();
        if (selectedRarityGroup == null)
        {
            // レアリティ設定が無い or 全ての重みが 0 の場合は何も落とさない
            return;
        }

        // 選ばれたレアリティの中から、さらにアイテムを選ぶ
        DropItemEntry selectedItem = selectedRarityGroup.GetRandomItem();
        if (selectedItem == null || selectedItem.Prefab == null)
        {
            // アイテムが設定されていない場合も何も落とさない
            return;
        }

        // 実際にアイテムプレハブを生成
        Vector3 spawnPos = GetSpawnPosition();
        Instantiate(selectedItem.Prefab, spawnPos, Quaternion.identity);
    }

    /// <summary>ドロップするかどうかの判定</summary>
    private bool RollDropChance()
    {
        float clamped = Mathf.Clamp01(dropChance);
        if (clamped <= 0f)
        {
            return false;
        }
        if (clamped >= 1f)
        {
            return true;
        }

        float r = UnityEngine.Random.value;
        return r <= clamped;
    }

    /// <summary>レアリティグループを重み付きランダムで 1 つ選ぶ</summary>
    private RarityDropGroup GetRandomRarityGroup()
    {
        if (rarityGroups == null || rarityGroups.Count == 0)
        {
            return null;
        }

        float totalWeight = 0f;
        foreach (var group in rarityGroups)
        {
            totalWeight += group.RarityWeight;
        }

        if (totalWeight <= 0f)
        {
            return null;
        }

        float r = UnityEngine.Random.value * totalWeight;
        float accum = 0f;

        foreach (var group in rarityGroups)
        {
            accum += group.RarityWeight;
            if (r <= accum)
            {
                return group;
            }
        }

        // 浮動小数点誤差対策として最後の要素を返す
        return rarityGroups[rarityGroups.Count - 1];
    }

    /// <summary>アイテムの生成位置を計算する</summary>
    private Vector3 GetSpawnPosition()
    {
        Vector3 basePosition = transform.position + spawnOffset;

        // X,Z に少しランダムな揺らぎを加える(重なり防止)
        float offsetX = 0f;
        float offsetZ = 0f;

        if (randomHorizontalOffsetRange.x > 0f)
        {
            offsetX = UnityEngine.Random.Range(-randomHorizontalOffsetRange.x, randomHorizontalOffsetRange.x);
        }
        if (randomHorizontalOffsetRange.y > 0f)
        {
            offsetZ = UnityEngine.Random.Range(-randomHorizontalOffsetRange.y, randomHorizontalOffsetRange.y);
        }

        return basePosition + new Vector3(offsetX, 0f, offsetZ);
    }
}

おまけ:敵の死亡を検知して DropTable を呼ぶシンプルな例

敵の HP 管理と死亡検知だけを行う、最小限のコンポーネント例も載せておきます。
必須ではないですが、使い方イメージとしてどうぞ。)

using UnityEngine;

/// <summary>とてもシンプルな敵HP管理コンポーネントの例</summary>
[RequireComponent(typeof(DropTable))]
public class EnemyHealth : MonoBehaviour
{
    [SerializeField] private int maxHp = 10;
    [SerializeField] private int currentHp;

    private DropTable dropTable;
    private bool isDead = false;

    private void Awake()
    {
        dropTable = GetComponent<DropTable>();
        currentHp = maxHp;
    }

    /// <summary>外部からダメージを与える想定の関数</summary>
    public void TakeDamage(int amount)
    {
        if (isDead)
        {
            return;
        }

        currentHp -= amount;
        if (currentHp <= 0)
        {
            Die();
        }
    }

    private void Die()
    {
        if (isDead)
        {
            return;
        }

        isDead = true;

        // 死亡時にドロップテーブルを発動
        if (dropTable != null)
        {
            dropTable.SpawnDrop();
        }

        // 本来はアニメーション再生や遅延破壊などを行う
        Destroy(gameObject);
    }
}

使い方の手順

  1. プレハブを用意する
    例として、以下のようなアイテムプレハブを作っておきます。
    • Coin(コモン)
    • PotionSmall(アンコモン)
    • PotionLarge(レア)
    • EpicSword(エピック)
    • LegendaryOrb(レジェンダリー)

    それぞれ 3D/2D オブジェクトにコライダー、見た目のメッシュやスプライトを付けてプレハブ化しておきましょう。

  2. 敵プレハブに DropTable を追加する
    • 敵プレハブ(例:EnemyGoblin)を開く
    • Add Component から DropTable を追加
    • 必要なら EnemyHealth などの HP 管理コンポーネントも追加

    Inspector で DropTable の項目が見えるようになります。

  3. レアリティテーブルを設定する

    DropTableRarity GroupsrarityGroups)リストに要素を追加していきます。

    • サイズを 5 にして、各要素に以下のように設定してみましょう:
    // 例:ゴブリン用のドロップテーブル設定イメージ
    // Common グループ
    rarity = Common
    rarityWeight = 70
    items:
      - itemName = "Coin x1", prefab = Coin, weight = 3
      - itemName = "Coin x3", prefab = CoinStack, weight = 1
    
    // Uncommon グループ
    rarity = Uncommon
    rarityWeight = 20
    items:
      - itemName = "Small Potion", prefab = PotionSmall, weight = 1
    
    // Rare グループ
    rarity = Rare
    rarityWeight = 8
    items:
      - itemName = "Large Potion", prefab = PotionLarge, weight = 1
    
    // Epic グループ
    rarity = Epic
    rarityWeight = 1.5
    items:
      - itemName = "Epic Sword", prefab = EpicSword, weight = 1
    
    // Legendary グループ
    rarity = Legendary
    rarityWeight = 0.5
    items:
      - itemName = "Legendary Orb", prefab = LegendaryOrb, weight = 1
    

    さらに、dropChance0.8 にすると、「80% の確率でいずれかのレアリティが選ばれ、その中からアイテムが 1 つ落ちる」という挙動になります。

  4. 死亡時に SpawnDrop() を呼び出す
    例として、EnemyHealth コンポーネントを使う場合:
    • 敵に EnemyHealth をアタッチ(RequireComponentDropTable も自動で付く)
    • 攻撃側のスクリプトから TakeDamage() を呼ぶ

    例えばプレイヤーの攻撃スクリプトから:

    private void OnTriggerEnter(Collider other)
    {
        // シンプルな例:攻撃の当たった相手に 5 ダメージを与える
        var enemyHealth = other.GetComponent<EnemyHealth>();
        if (enemyHealth != null)
        {
            enemyHealth.TakeDamage(5);
        }
    }
    

    これで HP が 0 以下になった瞬間、EnemyHealthDropTable.SpawnDrop() を呼び出し、設定どおりの確率でアイテムが生成されます。

具体的な使用例

  • プレイヤーを強化するアイテムドロップ
    雑魚敵はコインや小さな回復アイテムを中心に、ボス敵はレア度の高い武器やスキル解放アイテムを中心に設定しておくと、ゲームの成長感が演出しやすくなります。
  • 敵の種類ごとにテーブルを変える
    EnemyGoblin はお金多め、EnemySlime はポーション多め、EnemyDragon はレア武器多め、といった形でプレハブごとに DropTable の中身を変えるだけで個性を出せます。
  • 動く宝箱や壊れるオブジェクト
    敵だけでなく、「壊せるツボ」「宝箱」「採掘ポイント」などにも DropTable をアタッチしておけば、壊れたときに SpawnDrop() を呼ぶだけで共通の仕組みでアイテムを出せます。

メリットと応用

DropTable をコンポーネントとして切り出すことで、以下のようなメリットがあります。

  • 敵のロジックとドロップロジックを分離
    敵の行動 AI やアニメーションと、ドロップの確率調整を完全に切り離せます。EnemyHealthEnemyAI は「死んだら通知するだけ」、DropTable は「通知されたら抽選して生成するだけ」という単一責任になります。
  • プレハブ単位での調整が簡単
    エディタ上で DropTable のパラメータをいじるだけで、敵ごとのドロップ傾向を変えられます。レベルデザイナーやゲームデザイナーがスクリプトに触らずに調整できるのも大きな利点ですね。
  • テーブルの再利用性が高い
    「ゴブリン系共通の DropTable プレハブ」を作っておき、複数の敵プレハブにドラッグ&ドロップして使い回す、といった運用もしやすくなります。
  • Godクラスを避けられる
    「敵クラスが 1000 行超えてきたから、そろそろヤバい…」という事態を避けやすくなります。ドロップだけでなく、移動、攻撃、アニメーション、サウンドなども同じ思想でコンポーネント化すると、全体がかなりスッキリします。

改造案:レアリティに応じてエフェクトを変える

レアアイテムほど派手なエフェクトを出したい、というケースも多いと思います。その場合、SpawnDrop() の中で「選ばれたレアリティ」を使ってエフェクトを出す処理を追加できます。

例えば、DropTable 内にこんな関数を追加してみましょう:

/// <summary>レアリティに応じてエフェクトを再生する例(簡易版)</summary>
private void PlayRarityEffect(ItemRarity rarity, Vector3 position)
{
    // 実際にはレアリティごとに別のプレハブを持たせたり、
    // パーティクルシステムを再生したりする想定。
    // ここではログ出力のみ。
    switch (rarity)
    {
        case ItemRarity.Common:
            Debug.Log($"Common item dropped at {position}");
            break;
        case ItemRarity.Uncommon:
            Debug.Log($"Uncommon item dropped at {position}");
            break;
        case ItemRarity.Rare:
            Debug.Log($"Rare item dropped at {position}");
            break;
        case ItemRarity.Epic:
            Debug.Log($"Epic item dropped at {position}");
            break;
        case ItemRarity.Legendary:
            Debug.Log($"Legendary item dropped at {position}");
            break;
    }
}

そして SpawnDrop() の中で、アイテム生成直後に PlayRarityEffect(selectedRarityGroup.Rarity, spawnPos); を呼ぶようにすれば、レア度に応じた演出を簡単に追加できます。

こうやって少しずつ責務ごとにコンポーネントを分けていくと、プロジェクト全体がかなり扱いやすくなります。
ドロップ周りで悩んでいたら、ぜひこの DropTable コンポーネントをベースに自分のゲーム用にカスタマイズしてみてください。