【C#】NRandom - .NET / Unity向けの擬似乱数生成ライブラリ
以前RandomExtensionsとして公開していたライブラリですが、今回v2へのアップデートと共にNRandomという名称への変更を行いました。名称の変更と合わせて、新たな機能の追加や破壊的変更を含むAPIの調整を行っています。
RandomExtensionsという名称でありつつもSystem.Random
を投げ捨てて再実装する構成になっていて全然Extensionsじゃないという問題があったので、もうちょっと汎用的な名前になってます。特に捻りのない命名と言えばそうですが、まあ悪くはないんじゃないでしょうか。
RandomExtensionsからの移行についてはこのmdに書いてあるので、こちらも参考にしてもらえると。
使い方
というわけで改めて使い方を見ていきましょう。System.Random
のAPIを踏襲しつつ、より多くの機能が使えるようになっています。
using NRandom;
var rand = RandomEx.Create();
// bool
rand.NextBool();
// int
rand.NextInt(); // [int.MinValue, int.MaxValue]
rand.NextInt(10); // [0, 10)
rand.NextInt(10, 20); // [10, 20)
// uint
rand.NextUInt(); // [0, uint.MaxValue]
rand.NextUInt(10); // [0, 10)
rand.NextUInt(10, 20); // [10, 20)
// long
rand.NextLong(); // [long.MinValue, long.MaxValue]
rand.NextLong(10); // [0, 10)
rand.NextLong(10, 20); // [10, 20)
// ulong
rand.NextULong(); // [0, ulong.MaxValue]
rand.NextULong(10); // [0, 10)
rand.NextULong(10, 20); // [10, 20)
// float
rand.NextFloat(); // [0f, 1f)
rand.NextFloat(10f); // [0f, 10f)
rand.NextFloat(10f, 20f); // [10f, 20f)
// double
rand.NextDouble(); // [0.0, 1.0)
rand.NextDouble(10.0); // [0.0, 10.0)
rand.NextDouble(10.0, 20.0); // [10.0, 20.0)
rand.NextDoubleGaussian(); // 平均0.0、標準偏差1.0の正規分布の乱数を取得
// byte[], Span<byte>
rand.NextBytes(buffer); // バッファをランダムなbyte列で埋める
インスタンスの作成が不要な場合はスレッドセーフなRandomEx.Shared
が利用できます。
var n = RandomEx.Shared.NextInt(0, 10);
var flag = RandomEx.Shared.NextBool();
また、コレクションに対する拡張メソッドもいくつか用意されています。
using System;
using System.Linq;
using NRandom.Linq;
var sequence = Enumerable.Range(0, 100);
// ランダムな要素を取得
var r = sequence.RandomElement();
// NRandomを利用したShuffle()
foreach (var item in sequence.Shuffle(RandomEx.Shared))
{
Console.WriteLine(item);
}
// [0, 9]の範囲のランダムな値を10回流すIEnumerable<T>を作成
foreach (var item in RandomEnumerable.Repeat(0, 10, 10))
{
Console.WriteLine(item);
}
さらにSystem.NumericsやUnity向けの実装も用意されています。
// 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を取得
重み付き抽選
一様分布からの抽選だけでなく、各要素に重み付けをしたい場面も多いでしょう。そこでNRandomでは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.GetRandom();
重複なしで抽選する場合はRemoveRandom()
が利用できます。
var list = new WeightedList<string>();
list.Add("Foo", 1.0);
list.Add("Bar", 1.5);
list.Add("Baz", 3.0);
// 重み付きでランダムな要素を削除
list.RemoveRandom(out var item0);
list.RemoveRandom(out var item1);
list.RemoveRandom(out var item2);
Unityの場合はSerializableWeightedList<T>
を用いることで、Inspector上から値を編集することも可能になっています。
using NRandom;
using NRandom.Collections;
using NRandom.Unity;
using UnityEngine;
public class Sandbox : MonoBehaviour
{
[SerializeField] WeightedValue<string> value;
[SerializeField] SerializableWeightedList<string> list;
}
IRandom
C#には標準でSystem.Random
という擬似乱数生成のための型が用意されていますが、NRandomではこれを使わず独自のIRandom
インターフェースを抽象化層として使っています。
public interface IRandom
{
void InitState(uint seed);
uint NextUInt();
ulong NextULong();
}
擬似乱数生成のアルゴリズムに応じてこのIRandom
の実装がそれぞれ用意されています。一覧にするとこんな感じ。
クラス名 | アルゴリズム |
---|---|
ChaChaRandom |
ChaCha (デフォルトはChaCha8) |
MersenneTwisterRandom |
Mersenne Twister (MT19937) |
Pcg32Random |
PCG32 (PCG-XSH-RR) |
Philox4x32Random |
Philox4x32 (デフォルトはPhilox4x32-10) |
Sfc32Random |
SFC32 |
Sfc64Random |
SFC64 |
SplitMix32Random |
splitmix32 |
SplitMix64Random |
splitmix64 |
TinyMt32Random |
Tiny Mersenne Twister (32bit) |
TinyMt64Random |
Tiny Mersenne Twister (64bit) |
Xorshift32Random |
xorshift32 |
Xorshift64Random |
xorshift64 |
Xorshift128Random |
xorshift128 |
Xoshiro128StarStarRandom |
xoshiro128** |
Xoshiro256StarStarRandom |
xoshiro256** |
無駄に大量の実装がありますが、基本的にはデフォルトのxoshiro256**を使ってもらえれば良いと思います。詳しくは後述しますが、.NET 6以降ではSystem.Random
にも採用されている優秀なアルゴリズムです。より高品質または高速なものが使いたい場合にのみ別のものに切り替えると良いでしょう。
一昔前はMersenne Twisterが主流でしたが、メモリ消費量の大きさやオーバースペックな周期の長さなどが要因で、近年ではあまり使われなくなっている印象です。最近はxoshiftの派生やPCGなどがよく使われているアルゴリズムでしょうか。また、rustのrandクレートやgoのmath/rand/v2ではChaCha8の擬似乱数生成部分が利用されていたりもします。
また、IRandom
を通さずに乱数生成器の実装をそのまま利用することも可能です。各実装はNRandom.Algorithms
名前空間に置かれています。
using NRandom.Algorithms;
var seed = 123456;
var xorshift = new Xorshift32(seed);
var r = xorshift.Next();
System.Randomの問題
C#で乱数といえばSystem.Random
ではあるんですが、最初期から存在するAPIであることもあって、実はこの内部はかなり悲惨なことになっています。
詳細は上の記事が非常によくまとまっているのでそちらを読んでもらえると良いのですが、初期のSystem.Random
は欠陥だらけの実装であり、出力されるのは非常に低品質な乱数になっています。これは.NET 6で改善されxoshiro128/256**ベースの実装に変更されたのですが、seed
を指定する場合は下位互換性の問題から実装を変更できず、今に至るまでそのままです。
またSystem.Random
は抽象化層として利用するにも設計的にかなりの難があります。公式ドキュメントではこれを継承して独自の乱数生成器を実装する例が挙げられていますが、System.Random
にはそれ自体に(上の記事によると)280byteほどの内部状態を抱えており、継承するともれなくこの無駄なメモリ領域がついてくることになります。
さらに内部状態がprivateで隠蔽されているため、シリアライズや再現が難しいという問題もあります。リフレクションを使えば操作できないこともないですが、そのくらいなら自前の実装を突っ込んだ方が早いでしょう。
これらの反省から、NRandomでは独自のインターフェースIRandom
を用いて抽象化を行っています。シリアライズや内部状態の再現などを行いたい場合はNRandom.Algorithmsの構造体を使って独自の型を作成することも容易です。
UnityEngine.Randomの問題
またUnityにも標準で乱数生成のAPIがありますが、こちらもあまり使いやすいとは言い難い代物です。こちらも先程と同じ方の記事で解説されています。(この方の乱数の記事、毎度詳しく書かれていて面白いのおすすめです)
そもそもAPIが貧弱で、範囲指定のfloat
/int
か円/球体内のランダムな点を取るくらいしかできません。また他のUnityのAPI同様、メインスレッド以外からアクセスしようとするとエラーを吐きます。
最大の問題はUnityEngine.Random
がstatic classである故にインスタンス化ができないことで、このせいで乱数の再現がかなり難しくなります。アルゴリズム自体は素直なxorshiftですが、こちらも内部状態が隠蔽されているため細かい状態の再現はできません。
なお、DOTS用の数学ライブラリであるUnity.Mathematicsには構造体ベースのRandomが用意されていたりします。内部的にはuint
のラッパーとして動作するxorshiftの実装ですが、多くの型に対応した拡張メソッドが用意されていてかなり優秀です。
簡単な用途であればこれで十分、と言えるだけの機能は揃っています。より豊富な機能や高品質な乱数が欲しければNRandomを使ってもらえると良いでしょう。
まとめ
NRandomの機能周りの紹介と、System.RandomやUnityEngine.Randomへの不満を色々と書いてみました。正直もう少しまともな乱数生成の機能が標準ライブラリでも欲しいな、という気はしないでもないですが...
というわけでNRandomではこの辺りの不満を解消してくれるライブラリになったのではないでしょうか。v2で扱いやすさもさらに向上しているはずなので、ぜひ試してみてください!
Discussion