【C#/Unity】重み付き抽選機能を実装する

よくある重み付きの抽選機能の実装例の紹介です。

重み付き抽選とは要素ごとに選ばれる確率が違う抽選方法です。

例えば以下のように各々確率が違うものをランダムで選びます。

  • Aは50%
  • Bは25%
  • Cは20%
  • Dは5%

確認環境

  • Unity 2022.3.5f1
  • VisualStudio 2022

Editor 上のみで動作を確認

使い方

まずは使い方です

// Sample.cs

// フルーツを重み付き抽選で選ぶ
public void Select()
{
    // 抽選確立のリストを作成する
    List<LotteryItem<Fruit>> items = new();
    items.Add(new LotteryItem<Fruit>(Fruit.Apple, 50.5f));    // 50.5%
    items.Add(new LotteryItem<Fruit>(Fruit.Banana, 25.5f));   // 25.5%
    items.Add(new LotteryItem<Fruit>(Fruit.Orange, 15.0f));   // 15.0%
    items.Add(new LotteryItem<Fruit>(Fruit.Grape, 5.0f));     // 5.0%
    items.Add(new LotteryItem<Fruit>(Fruit.Pineapple, 4.0f)); // 4.0%

    // 抽選する
    Fruit result = RandomUtil.SelectOne(items);
}

public enum Fruit 
{
    Apple, Banana, Orange, Grape, Pineapple
}

実装コード

合計で 100になるようにサンプルを作りましたが重みは100でなくても大丈夫です。100より多くても少なくてもその割合で要素が抽選されます。

// RandomUtil.cs

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

public static class RandomUtil
{
    // 重み付き抽選を行う(配列用)
    public static T SelectOne<T>(LotteryItem<T>[] list)
    {
        float total = 0;
        for (int i = 0; i < list.Length; i++)
        {
            total += list[i].Weight;
        }

        float value = Random.Range(0, total);
        for (int i = 0; i < list.Length; i++)
        {
            value -= list[i].Weight;
            if (value <= 0) return list[i].Value;
        }

        return default;
    }

    // 重み付き抽選を行う(リスト用:ちょっと動作が遅い)
    public static T SelectOne<T>(List<LotteryItem<T>> list)
    {
        float total = 0;
        for (int i = 0; i < list.Count; i++)
        {
            total += list[i].Weight;
        }

        float value = Random.Range(0, total);
        for (int i = 0; i < list.Count; i++)
        {
            value -= list[i].Weight;
            if (value <= 0) return list[i].Value;
        }

        return default;
    }
}

[System.Serializable]
public readonly struct LotteryItem<T>
{
    public readonly T Value;
    public readonly float Weight;

    public LotteryItem(T value, float weight)
    {
        Value = value;
        Weight = weight;
    }
}

余談ですが、この実装は seed の指定がないため起動ごとに適当な seed が使用されるので再現性が無いです。もしかするとデバッグ時に困るケースがあります。

もし、必要なら UnityEngine.Random.state をどこかにもってそれを指定する仕組みが必要です。その場合 static メソッドではなくクラスにしてインスタンスの中に Random.State を持つなど対応しましょう。