【Unity】RPGアイテムドロップで使える汎用的な重み付け抽選

抽選テーブルとして`IEnumerable<KeyValuePair<T, float>>`を引数に渡し、T型で返すことで汎用的に使えるようにしました。

アイテムドロップ以外に、AIのランダム行動、称号付与など重み付けをしたい確率の抽選に使えます。


```C#:RandomWithWeight.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public static class RandomWithWeight  {
    /// <summary>
    /// 重み付き抽選を行う
    /// </summary>
    /// <param name="itemWeightPairs">Key: 抽選する対象, Value: 確率の重み</param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    public static T Lotto<T>(IEnumerable<KeyValuePair<T, float>> itemWeightPairs) 
    {
        // Weight降順でソート
        var sortedPairs = itemWeightPairs.OrderByDescending(x => x.Value).ToArray();
        
        // ドロップアイテムの抽選
        float total = sortedPairs.Select(x => x.Value).Sum();

        float randomPoint = Random.Range(0, total);
        
        // randomPointの位置に該当するキーを返す
        foreach (KeyValuePair<T, float> elem in sortedPairs)
        {
            if (randomPoint < elem.Value){
                return elem.Key;
            }

            randomPoint -= elem.Value;
        }
        
        return sortedPairs[sortedPairs.Length - 1].Key;
    }
}
```

## 使用例

```C#:DropItem.cs
public class Item
{
    public string Name;

    public Item(string name)
    {
        Name = name;
    }
}

public class DropItem : MonoBehaviour
{
    private static readonly List<KeyValuePair<Item, float>> itemDict = new List<KeyValuePair<Item, float>>()
    {
        new KeyValuePair<Item, float>(new Item("無し"), 60f),
        new KeyValuePair<Item, float>(new Item("鉄の剣"), 30f),
        new KeyValuePair<Item, float>(new Item("鋼の剣"), 15f),
        new KeyValuePair<Item, float>(new Item("銀の剣"), 5f)
    };
    
    private void Start()
    {
        var item = RandomWithWeight.Lotto<Item>(itemDict);
        if(item.Name != "無し"){
            Debug.Log($"{item.Name}がドロップしました");
        } else {
            Debug.Log($"ドロップはありません");
        }
    }
}
```


## おまけ

データマスタ上でアイテムにWeightをもたせて管理する場合もあると思います。その場合はKeyValuePairを宣言するのは無駄な処理になります。

代わりにinterfaceを使ってWeightフィールドをもたせると次のように書けます。

```C#:IHaveWeight.cs
public interface IHaveWeight
{
    float Weight { get; }
}
```


```C#:RandomWithWeight.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Random = UnityEngine.Random;

public static class RandomWithWeight  {
    /// <summary>
    /// 重み付き抽選を行う
    /// </summary>
    /// <param name="itemWeightPairs"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns> 
    // interface利用
    public static T Lotto<T>(IEnumerable<T> itemWeightPairs) where T : IHaveWeight
    {
        // Weight順でソート
        var sortedPairs = itemWeightPairs.OrderByDescending(x => x.Weight).ToArray();
                
        // ドロップアイテムの抽選
        float total = sortedPairs.Select(x => x.Weight).Sum();

        float randomPoint = Random.Range(0, total);
        
        // randomPointの位置に該当するキーを返す
        foreach (T elem in sortedPairs)
        {
            if (randomPoint < elem.Weight){
                return elem;
            }

            randomPoint -= elem.Weight;
        }
        
        return sortedPairs[sortedPairs.Length - 1];
    }
}

```

```C#:DropItem.cs
// IHaveWeightを継承する！！
public class Item : IHaveWeight
{
    public string Name;
    private float _weight;

    public float Weight => _weight;

    public Item(string name, float weight)
    {
        Name = name;
        _weight = weight;
    }
}

public class DropItem : MonoBehaviour
{
    private static readonly List<Item> itemDict = new List<Item>()
    {
        new Item("無し", 60f),
        new Item("鉄の剣", 30f),
        new Item("鋼の剣", 15f),
        new Item("銀の剣", 5f)
    };
    
    private void Start()
    {
        var item = RandomWithWeight.Lotto<Item>(itemDict);
        if(item.Name != "無し"){
            Debug.Log($"{item.Name}がドロップしました");
        } else {
            Debug.Log($"ドロップはありません");
        }
    }
}
```


## 参考
[【Unity】UnityのRandomを使って重み付き抽選を実装するサンプル](https://ekulabo.com/random-with-weight)