Zenn
🎲

【C#】NRandom - .NET / Unity向けの擬似乱数生成ライブラリ

2025/04/01に公開
8

以前RandomExtensionsとして公開していたライブラリですが、今回v2へのアップデートと共にNRandomという名称への変更を行いました。名称の変更と合わせて、新たな機能の追加や破壊的変更を含むAPIの調整を行っています。

https://github.com/nuskey8/NRandom

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であることもあって、実はこの内部はかなり悲惨なことになっています。

https://andantesoft.hatenablog.com/entry/2021/01/09/203050

詳細は上の記事が非常によくまとまっているのでそちらを読んでもらえると良いのですが、初期のSystem.Randomは欠陥だらけの実装であり、出力されるのは非常に低品質な乱数になっています。これは.NET 6で改善されxoshiro128/256**ベースの実装に変更されたのですが、seedを指定する場合は下位互換性の問題から実装を変更できず、今に至るまでそのままです。

またSystem.Randomは抽象化層として利用するにも設計的にかなりの難があります。公式ドキュメントではこれを継承して独自の乱数生成器を実装する例が挙げられていますが、System.Randomにはそれ自体に(上の記事によると)280byteほどの内部状態を抱えており、継承するともれなくこの無駄なメモリ領域がついてくることになります。

さらに内部状態がprivateで隠蔽されているため、シリアライズや再現が難しいという問題もあります。リフレクションを使えば操作できないこともないですが、そのくらいなら自前の実装を突っ込んだ方が早いでしょう。

これらの反省から、NRandomでは独自のインターフェースIRandomを用いて抽象化を行っています。シリアライズや内部状態の再現などを行いたい場合はNRandom.Algorithmsの構造体を使って独自の型を作成することも容易です。

UnityEngine.Randomの問題

またUnityにも標準で乱数生成のAPIがありますが、こちらもあまり使いやすいとは言い難い代物です。こちらも先程と同じ方の記事で解説されています。(この方の乱数の記事、毎度詳しく書かれていて面白いのおすすめです)

https://andantesoft.hatenablog.com/entry/2020/12/01/225931

そもそもAPIが貧弱で、範囲指定のfloat/intか円/球体内のランダムな点を取るくらいしかできません。また他のUnityのAPI同様、メインスレッド以外からアクセスしようとするとエラーを吐きます。

最大の問題はUnityEngine.Randomがstatic classである故にインスタンス化ができないことで、このせいで乱数の再現がかなり難しくなります。アルゴリズム自体は素直なxorshiftですが、こちらも内部状態が隠蔽されているため細かい状態の再現はできません。

なお、DOTS用の数学ライブラリであるUnity.Mathematicsには構造体ベースのRandomが用意されていたりします。内部的にはuintのラッパーとして動作するxorshiftの実装ですが、多くの型に対応した拡張メソッドが用意されていてかなり優秀です。

https://docs.unity3d.com/Packages/com.unity.mathematics@1.3/manual/random-numbers.html

簡単な用途であればこれで十分、と言えるだけの機能は揃っています。より豊富な機能や高品質な乱数が欲しければNRandomを使ってもらえると良いでしょう。

まとめ

NRandomの機能周りの紹介と、System.RandomやUnityEngine.Randomへの不満を色々と書いてみました。正直もう少しまともな乱数生成の機能が標準ライブラリでも欲しいな、という気はしないでもないですが...

というわけでNRandomではこの辺りの不満を解消してくれるライブラリになったのではないでしょうか。v2で扱いやすさもさらに向上しているはずなので、ぜひ試してみてください!

8

Discussion

ログインするとコメントできます