コンテンツへスキップ

【C#】Random Extensions – .NET / Unity用の乱数ライブラリ

  • C#

.NET / Unity向けに新たな擬似乱数ライブラリ「Random Extensions」を作成しました!いつも通りOSSとしてGithubに公開しています。

.NETで擬似乱数を扱いたい場合、多くの場合はSystem.Randomクラスを使用することになります。が、このクラスは歴史的経緯もあり、内部実装や設計が混沌としたことになっています。(シードを指定するか否かで生成アルゴリズムが違う、実装ミスにより周期が保証されない、絶妙に抽象化層として役に立たないRandomクラスと謎のRandom.ImplBase、etc…)

また、Unityで乱数を扱う場合はUnityEngine.Randomが用いられますが、こちらも古くからあるクラスなので、色々と設計上の問題が目立ちます。特に厳しいのが乱数の再現を行いたい場合。UnityEngine.Randomはstaticクラスであるためインスタンス化ができず、さらにUnityEngine.Random.Stateのフィールドはprivateであるため、リフレクションやUnsafe.Asなどを用いなければアクセスすることができません。直前のstateを保持して元に戻すことで再現性のある乱数を実装することも可能ですが、そもそも最初からインスタンス化可能できればこんな面倒な実装を行う必要はないでしょう。(一応今ならUnity.Mathematics.Randomという選択肢もありますが)

というわけで、Random Extensionsでは新たなIRandomインターフェースと、複数のアルゴリズムによるハイパフォーマンスな擬似乱数生成の実装を提供します。これらは.NET / Unityの両方で利用可能なほか、System.NumericsやUnityの型に対応した拡張パッケージも同時に用意されています。

使い方

基本的なAPIはSystem.Randomとほとんど同じになるように揃えているので、ほとんどそのまま移行できます。

using RandomExtensions;

// [0, 9]の範囲でランダムな値を取得
var n = RandomEx.Shared.NextInt(0, 10);

// [0, 1)の範囲でランダムな値を取得
var r = RandomEx.Shared.NextFloat();

// trueまたはfalseをランダムに取得
var flag = RandomEx.Shared.NextBool();

RandomEx.Sharedはスレッドセーフな共用のIRandomインスタンスで、再現性が不要な乱数を手軽に取得することが可能です。

シード指定や再現性が必要な場合はIRandomを実装したクラスをインスタンス化して利用できます。Random Extensionsでは標準でsplitmix、PCG、xorshift、xoshiro**を用いた実装をそれぞれ用意していますが、特にこだわりがなければ.NET 6以降でも採用されているxoshiro256**の実装を利用すると良いでしょう。

var rand = new Xoshiro256StarStarRandom();
var r = rand.NextDouble();

注意として、System.Randomはnew()でインスタンス化するとシード値がランダムに割り当てられますが、Random Extensionsでは初期シード値が固定されています。同一の結果を避けたい場合はInitSeed(uint seed)を呼び出して手動でシードを指定してください。(RandomEx.SharedとRandomEx.Create()で作成したインスタンスはシード値がランダムに設定されます。)

// シード値を設定
rand.InitSeed(123456);

コレクションのランダムな操作

Random Extensionsは単に擬似乱数生成を行うためのライブラリではなく、乱数を扱う上で便利な機能を多く搭載しています。

var rand = RandomEx.Create();

// 値を保持する配列を作成
var array = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

// ランダムな要素を取得
var item = rand.GetItem(array);

// ランダムな要素を5つ取得 (重複あり)
var items = rand.GetItems(array, 5);

// 配列の要素をシャッフル
rand.Shuffle(array);

また、RandomExtensions.Linq以下にはIEnumerable<T>に対する拡張メソッドが用意されています。もちろん、これらはLINQのオペレータと合わせて使用できます。

using System;
using System.Linq;
using RandomExtensions.Linq;

var sequence = Enumerable.Range(0, 100);

// ランダムな要素を取得
var r = sequence.RandomElement();

// 順序をシャッフル
foreach (var item in sequence.Shuffle())
{
    Console.WriteLine(item);
}

// [0, 9]の範囲のランダムな値を10回流すIEnumerable<T>を作成
foreach (var item in RandomEnumerable.Repeat(0, 10, 10))
{
    Console.WriteLine(item);
}

重み付き抽選

複数の要素から抽選を行う場合、全て同じ確率ではなく要素毎に重みをつけることも考えられます。Random Extensionsでは重み付きの抽選を簡単に行うためのコレクションとしてIWeightedCollection<T>インターフェースと、その実装であるWeightedList<T>を提供します。

// 重み付きリストを作成
var weightedList = new WeightedList<string>();

// 要素を重みを指定して追加
weightedList.Add("Legendary", 0.5);
weightedList.Add("Epic", 2.5);
weightedList.Add("Rare", 12);
weightedList.Add("Uncommon", 25);
weightedList.Add("Common", 60);

// 重み付きでランダムな要素を取得
var rarity = weightedList.GetItem();

重複なしで抽選を行いたい場合はRemoveRandomメソッドが便利です。

// 重み付きでランダムな要素を削除し、削除したアイテムを取得
weightedList.RemoveRandom(out var item);

また、前述のRandomExtensions.LinqにはIEnumerable<T>からWeightedList<T>を作成可能な拡張メソッドが用意されています。

// IEnumerable<T>からWeightedList<T>を作成
// 引数では要素の重みを返すFunc<T, double>を指定
Enumerable.Range(0, 100)
    .ToWeightedList(x => x);

擬似乱数生成器の実装

RandomExtensions.Algorithmsには様々な擬似乱数生成の実装が用意されています。これらは最小限の実装とステートのみを備えた構造体であり、自前の乱数生成機能を作成したり、乱数の状態をシリアライズする際などに役立ちます。

using RandomExtensions.Algorithms;

// xorshiftを用いた乱数の生成
var state = 123456;
var xorshift = new Xorshift32(state);

var r = xorshift.Next();

これらはunmanaged構造体であるため、UnityのBurst Compilerで使用されるHPC#とも互換性があるという利点もあります。

擬似乱数生成のアルゴリズムには、以前はメルセンヌ・ツイスタ法がよく用いられていましたが、近年はより軽量で十分に高品質なxoshiroやPCGなども利用されるようになってきています。非常に大きな周期が必要であればメルセンヌ・ツイスタは有効ですが、多くの場合はxorshiroやPCGで十分でしょう。

Unity

Random Extensionsの本体はNuGetで配布されていますが、それとは別にUnity用の拡張パッケージも提供されています。UnityのパッケージにはRandom Extensions本体は含まれないため、本体は別途NugetForUnityなどを用いてdllをプロジェクトに追加してください。

RandomExtensions.Unityを追加すると、IRandomに以下の拡張メソッドが追加されます。

using UnityEngine;
using RandomExtensions;
using RandomExtensions.Unity;

var rand = RandomEx.Create();

// Vector2
rand.NextVector2();
rand.NextVector2(new Vector2(10f, 10f));
rand.NextVector2(new Vector2(10f, 10f), new Vector2(20f, 20f));
rand.NextVector2Direction();    // ランダムな長さ1の方向ベクトルを取得
rand.NextVector2InsideCircle(); // 半径1の円の内側のランダムな座標を取得

// Vector3
rand.NextVector3();
rand.NextVector3(new Vector3(10f, 10f, 10f));
rand.NextVector3(new Vector3(10f, 10f, 10f), new Vector2(20f, 20f, 20f));
rand.NextVector3Direction();    // ランダムな長さ1の方向ベクトルを取得
rand.NextVector3InsideSphere(); // 半径1の球の内側のランダムな座標を取得

// Vector4
rand.NextVector4();
rand.NextVector4(new Vector4(10f, 10f, 10f, 10f));
rand.NextVector4(new Vector4(10f, 10f, 10f, 10f), new Vector2(20f, 20f, 20f, 20f));

// Quaternion
rand.NextQuaternionRotation();  // ランダムな回転を表すQuaternionを取得

// Color
rand.NextColor();
rand.NextColor(new Color(1f, 1f, 1f));
rand.NextColor(new Color(0f, 0f, 0f), new Color(1f, 1f, 1f));
rand.NextColorHSV(0f, 1f, 0f, 1f, 0f, 1f);          // HSVの範囲を指定
rand.NextColorHSV(0f, 1f, 0f, 1f, 0f, 1f, 0f, 1f);  // HSVとalphaの範囲を指定

ランダムなVector2 / Vector3が必要な場面はかなり多いため、こういった拡張メソッドがあると記述をより簡潔にできます。また、単純な範囲指定のみではなくNextVector3InsideSphere()なども用意されており、UnityEngine.Randomを完全に置き換えることができます。

さらにSystem.Numericsの型にも同様の拡張を追加するパッケージがNuGetで配布されています。詳細はREADMEを参照してください。

まとめ

擬似乱数は特にゲーム開発などで頻繁に利用される機能ですが、その割に.NET / Unityの標準ライブラリの機能は貧弱で、代替のライブラリもあまり見つかりませんでした。Random Extensionsは必要十分な機能を提供しつつ、実装や設計もシンプルにまとめてあり、標準のものと比べても扱いやすいライブラリになっているのではないでしょうか。

というわけで便利なライブラリになってると思うので、是非是非使ってみてください!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です