
UEFN/Verse言語でシードあり擬似乱数生成器を実装する方法を教えて!
こういった悩みにお答えします.
本記事の信頼性
- リアルタイムシステムの研究歴12年.
- 東大教員の時に,英語でOS(Linuxカーネル)の授業.
- 2012年9月~2013年8月にアメリカのノースカロライナ大学チャペルヒル校(UNC)コンピュータサイエンス学部で客員研究員として勤務.C言語でリアルタイムLinuxの研究開発.
- プログラミング歴15年以上,習得している言語: C/C++,Python,Solidity/Vyper,Java,Ruby,Go,Rust,D,HTML/CSS/JS/PHP,MATLAB,Verse(UEFN), Assembler (x64,aarch64).
- 東大教員の時に,C++言語で開発した「LLVMコンパイラの拡張」,C言語で開発した独自のリアルタイムOS「Mcube Kernel」をGitHubにオープンソースとして公開.
- 2020年1月~現在はアメリカのノースカロライナ州チャペルヒルにあるGuarantee Happiness LLCのCTOとしてECサイト開発やWeb/SNSマーケティングの業務.2022年6月~現在はアメリカのノースカロライナ州チャペルヒルにあるJapanese Tar Heel, Inc.のCEO兼CTO.
- 最近は自然言語処理AIとイーサリアムに関する有益な情報発信や,Unreal Editor for Fortnite(UEFN)でゲーム開発に従事.
- (AI全般を含む)自然言語処理AIの論文の日本語訳や,AIチャットボット(ChatGPT,Auto-GPT,Gemini(旧Bard)など)の記事を50本以上執筆.アメリカのサンフランシスコ(広義のシリコンバレー)の会社でChatGPT/Geminiを訓練するプロンプトエンジニア・マネージャー・Quality Assurance(QA)の業務委託の経験あり.
- (スマートコントラクトのプログラミングを含む)イーサリアムや仮想通貨全般の記事を200本以上執筆.イギリスのロンドンの会社で仮想通貨の英語の記事を日本語に翻訳する業務委託の経験あり.
- UEFNで10本以上のゲームを開発し,フォートナイト上で公開(Fortnite,Fortnite.GG).
こういった私から学べます.
UEFNでランダムダンジョンなどを作成する場合,デバッグ時に同じ生成結果を再現したいことがあります.
本記事では,シードを指定できる擬似乱数生成器my_seeded_random_generatorをVerse言語で実装します.
ゲーム本番ではVerse標準のRandomモジュールを利用し,本実装は主にデバッグや再現テストで利用します.
【UEFN/Verse言語】シードあり擬似乱数生成器とは
シードあり擬似乱数生成器とは,シードと呼ばれる初期値から擬似乱数を生成する仕組みです.
シードは,擬似乱数生成器の内部状態(次の擬似乱数を計算するために保持する値)を初期化するために利用します.
同じシードを設定して,同じ順番で乱数を生成すると,同じ結果を再現できます.
例えば,あるシードから以下の値が生成された場合,
|
1 2 3 4 5 |
15 80 39 4 34 |
再度同じシードを設定すると,同じ乱数列を生成できます.
VerseのRandomモジュールには,以下の関数があります.
|
1 2 3 4 5 |
using { /Verse.org/Random } GetRandomInt() GetRandomFloat() Shuffle() |
GetRandomInt()とGetRandomFloat()で乱数を生成し,Shuffle()で配列をランダムに並べ替えられます.
一方で,任意のシードを設定する公開関数はありません.
そのため,同じ生成結果を再現したい場合は,シード付きの擬似乱数生成器を別に実装する必要があります.
シードあり擬似乱数生成器の用途
シードあり擬似乱数生成器の主な用途はデバッグです.
例えば,ランダムダンジョンで特定のシードだけ進行不能になる場合,シードを固定すると同じダンジョンを何度でも生成できます.
|
1 |
MyRandomGenerator.MySetSeed(12345) |
また,以下の用途にも利用できます.
- ランダムダンジョンの再現
- 敵やトラップ配置の再現
- アイテム抽選のテスト
- バグの再現
- 回帰テスト
- Daily Seedなどの固定マップ
ただし,ゲーム本番で再現性が不要な場合は,Verse標準のRandomモジュールを利用します.
|
1 2 3 4 5 |
デバッグ my_seeded_random_generator ゲーム本番 Verse標準Random |
メルセンヌ・ツイスタをVerseで実装するのが難しい理由
-

-
【C言語】rand関数と自作関数で乱数の生成【モンテカルロ法で円周率の計算】
こういった悩みにお答えします. こういった私から学べます. 乱数 乱数はランダムな数を意味します. 乱数の特徴として,偏りがないことや再現性がないことが要求されますが,コンピュータの計算による乱数生成 ...
続きを見る
上記のC言語の記事で紹介したメルセンヌ・ツイスタは高品質な擬似乱数生成器ですが,以下のようなビット演算を多用します.
|
1 2 3 4 5 |
XOR AND 左ビットシフト 右ビットシフト ビットマスク |
Verseでは,C/C++のような整数のビット演算をそのまま利用しにくいため,これらを独自に再現する必要があります.
ビットごとのループ処理が必要になるため,実装が複雑になり,処理量も多くなります.
シードあり擬似乱数生成器にMRG32k3aを選定した理由
MRG32k3aは,1999年にピエール・ルクワイエが提案した,2つの多重再帰型擬似乱数生成器を組み合わせた方式です.
主な特徴は以下になります.
- ビット演算が不要
- 整数の乗算,減算,剰余で実装できる
- 内部状態は整数6個
- 周期は約\(2^{191}\)
- Verseで比較的軽量に実装できる
ランダムダンジョンのデバッグ用途では,十分な周期と乱数品質があります.
メルセンヌ・ツイスタと比較すると,以下になります.
| 項目 | MRG32k3a | メルセンヌ・ツイスタ |
|---|---|---|
| 主な演算 | 乗算,減算,剰余 | ビット演算 |
| 内部状態 | 6個 | 624個 |
| Verseでの実装 | 比較的簡単 | 複雑 |
| 周期 | 十分に長い | 非常に長い |
Verseで利用する場合は,MRG32k3aの方が実装しやすいと考えました.
my_seeded_random_generatorのVerse実装
my_seeded_random_generatorのVerse実装を紹介します.
実装するクラスと関数は以下になります.
Verse標準のRandomモジュールと区別するために,関数名にはMyを付けています.
|
1 |
my_seeded_random_generator |
|
1 2 3 4 |
MySetSeed() MyGetRandomInt() MyGetRandomFloat() MyShuffle() |
私の実装は以下になります.
MyGetRandomInt()では,生成する整数の範囲がMRG32k3aの1回の出力範囲以内であることを想定しています.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# # Author: Hiroyuki Chishiro # License: 2-Clause BSD # using { /Verse.org/Verse } my_seeded_random_generator<public> := class<concrete>: # MRG32k3aで使用する2個の法です. # Modulus1 = 2^32 - 209 # Modulus2 = 2^32 - 22853 Modulus1<private>:int = 4294967087 Modulus2<private>:int = 4294944443 # MRG32k3aの内部状態です. # 2個の生成器がそれぞれ3個の状態を保持します. var State10<private>:int = 1 var State11<private>:int = 1 var State12<private>:int = 1 var State20<private>:int = 1 var State21<private>:int = 1 var State22<private>:int = 1 # シードから内部状態を初期化します. MySetSeed<public>(Seed:int)<transacts>:void = # Modulus2はModulus1より小さいため, # この範囲なら6個すべての内部状態に利用できます. if (NormalizedSeed := Mod[Seed, Modulus2 - 1]): StateValue := NormalizedSeed + 1 set State10 = StateValue set State11 = StateValue set State12 = StateValue set State20 = StateValue set State21 = StateValue set State22 = StateValue # MRG32k3aの生の擬似乱数を生成します. NextRaw<private>()<transacts>:int = # MRG32k3aで定義されている2個の漸化式を計算します. if: Component1 := Mod[ (1403580 * State11) - (810728 * State10), Modulus1 ] Component2 := Mod[ (527612 * State22) - (1370589 * State20), Modulus2 ] then: # 内部状態を1個進めます. set State10 = State11 set State11 = State12 set State12 = Component1 set State20 = State21 set State21 = State22 set State22 = Component2 # 2個の生成器の結果を組み合わせます. if (Component1 <= Component2): return Component1 - Component2 + Modulus1 return Component1 - Component2 return 1 # 0~Count - 1の整数を生成します. # 拒否法を利用して剰余による偏りを防ぎます. NextBounded<private>(Count:int)<transacts>:int = if (Count <= 1): return 0 if (Count > Modulus1): return 0 if (Remainder := Mod[Modulus1, Count]): Limit := Modulus1 - Remainder loop: Candidate := NextRaw() - 1 if (Candidate < Limit): if (Result := Mod[Candidate, Count]): return Result return 0 # Low~Highの整数を両端を含めて生成します. MyGetRandomInt<public>( Low:int, High:int )<transacts>:int = Minimum := if (Low <= High): Low else: High Maximum := if (Low <= High): High else: Low Count := Maximum - Minimum + 1 if (Count <= 0 or Count > Modulus1): return Minimum return Minimum + NextBounded(Count) # Low~Highの浮動小数点数を生成します. MyGetRandomFloat<public>( Low:float, High:float )<transacts>:float = UnitValue := (1.0 * (NextRaw() - 1)) / (1.0 * (Modulus1 - 1)) return Low + ((High - Low) * UnitValue) # Fisher–Yates法で配列をシャッフルします. MyShuffle<public>( Input:[]t where t:type )<transacts>:[]t = var Result:[]t = Input var CurrentIndex:int = Result.Length - 1 loop: if (CurrentIndex <= 0): break SwapIndex := MyGetRandomInt(0, CurrentIndex) if: CurrentValue := Result[CurrentIndex] SwapValue := Result[SwapIndex] set Result[CurrentIndex] = SwapValue set Result[SwapIndex] = CurrentValue set CurrentIndex = CurrentIndex - 1 return Result |
my_seeded_random_generatorの使い方
my_seeded_random_generatorの使い方を紹介します.
以下の例では,同じシードから同じ値を生成できることを確認した後,MyGetRandomInt(),MyGetRandomFloat(),MyShuffle()を利用します.
FirstValueとSecondValueには同じ値が出力され,その後に整数,浮動小数点数,シャッフルした配列が出力されます.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# # Author: Hiroyuki Chishiro # License: 2-Clause BSD # using { /Fortnite.com/Devices } using { /Verse.org/Simulation } using { /UnrealEngine.com/Temporary/Diagnostics } my_seeded_random_generator_device := class(creative_device): MyRandomGenerator:my_seeded_random_generator = my_seeded_random_generator{} OnBegin<override>()<suspends>:void = # 同じシードから同じ値になることを確認します. MyRandomGenerator.MySetSeed(12345) FirstValue := MyRandomGenerator.MyGetRandomInt(0, 100) MyRandomGenerator.MySetSeed(12345) SecondValue := MyRandomGenerator.MyGetRandomInt(0, 100) Print("FirstValue = {FirstValue}") Print("SecondValue = {SecondValue}") # 0~100の擬似乱数整数を生成します. RandomInt := MyRandomGenerator.MyGetRandomInt(0, 100) Print("RandomInt = {RandomInt}") # 0.5~3.0の擬似乱数floatを生成します. RandomFloat := MyRandomGenerator.MyGetRandomFloat(0.5, 3.0) Print("RandomFloat = {RandomFloat}") # 配列をシャッフルします. RoomOrder := MyRandomGenerator.MyShuffle( array{ "Entrance", "Combat", "Trap", "Treasure", "Boss" } ) for (Index -> Room : RoomOrder): Print("RoomOrder[{Index}] = {Room}") |
本実装で同じシードと同じ呼び出し順を利用した場合,実行結果は毎回以下になります.
|
1 2 3 4 5 6 7 8 9 |
FirstValue = 26 SecondValue = 26 RandomInt = 79 RandomFloat = 1.308464 RoomOrder[0] = Trap RoomOrder[1] = Entrance RoomOrder[2] = Treasure RoomOrder[3] = Combat RoomOrder[4] = Boss |
Print関数の表示は,メッセージログのVerseの診断で確認できます.
ファイルパス,行番号,タイムスタンプ等は省略しています.
my_seeded_random_generatorの使用時の注意点
my_seeded_random_generatorを利用する場合は,擬似乱数を生成する順番に注意してください.
同じシードを設定しても,乱数関数の呼び出し順や回数が変わると,その後に生成される擬似乱数も変わります.
例えば,以下の順番で擬似乱数を利用しているとします.
1.部屋
2.敵
3.トラップ
途中に装飾用の乱数処理を追加すると,以下になります.
1.部屋
2.装飾
3.敵
4.トラップ
乱数関数の呼び出しが1回増えるため,敵やトラップに利用される擬似乱数も変化します.
そのため,大規模なランダムダンジョンでは,用途ごとに擬似乱数生成器を分ける方法がおすすめです.
|
1 2 3 4 5 6 7 8 |
TopologyRandomGenerator:my_seeded_random_generator = my_seeded_random_generator{} EncounterRandomGenerator:my_seeded_random_generator = my_seeded_random_generator{} DecorationRandomGenerator:my_seeded_random_generator = my_seeded_random_generator{} |
また,同じmy_seeded_random_generatorインスタンスに対する乱数生成処理はリエントラント(処理の実行途中に同じ処理が再び呼び出されても,互いに干渉せず正しく動作できる性質)ではありません.
擬似乱数を生成するたびに内部状態を書き換えるため,同じインスタンスを複数の並行処理から利用すると,乱数を生成する順番によって結果が変化する可能性があります.
並行処理で利用する場合は,処理ごとに異なるmy_seeded_random_generatorのインスタンスを利用しましょう.
参考:Epic Developer Communityの擬似乱数生成器との比較
Epic Developer Communityには,TyrantKingBenが実装したシードあり擬似乱数生成器があります.
こちらは文字列シードとSFC32を利用した実装です.
SFC32自体は軽量な擬似乱数生成器ですが,ビット演算を利用します.
そのため,Verse実装では以下のような処理を独自に再現しています.
|
1 2 3 4 5 6 |
LeftShift RightShift XOR OR UInt32 IMul |
ビット演算をループで再現するため,Verse上では処理量が多くなります.
比較すると以下になります.
| 項目 | 本記事のMRG32k3a実装 | Epic Developer CommunityのSFC32実装 |
|---|---|---|
| シード | int | string |
| 主な演算 | 乗算,減算,剰余 | XOR,シフト,OR |
| ビット演算の再現 | 不要 | 必要 |
| Verse上の処理量 | 比較的少ない | 比較的多い |
| 文字列シード | 非対応 | 対応 |
| デバッグ用途 | 適している | 適している |
文字列をそのままシードにしたい場合は,SFC32の実装が便利です.
一方で,整数シードで問題なく,Verse上でビット演算の再現を避けたい場合は,MRG32k3aが適しています.
まとめ
UEFN/Verse言語で,シードあり擬似乱数生成器を実装する方法を紹介しました.
ランダムダンジョンなどのデバッグではmy_seeded_random_generatorを利用し,ゲーム本番ではVerse標準のRandomモジュールを利用するように使い分けましょう.
私がUnreal Editor for Fortnite(UEFN)で開発した,フォートナイトで遊べるゲームをしたいあなたはこちらからどうぞ.
-

-
【無料】私がUnreal Editor for Fortnite(UEFN)で開発した,フォートナイトで遊べるゲーム
こういった私から学べます. 私がUnreal Editor for Fortnite(UEFN)で開発した,フォートナイトで遊べる無料ゲームを紹介します. 是非,私のゲームで遊びましょう! まずはUE ...
続きを見る

