はじめに
疑似乱数について、もう少し考えてみましょう。
実は、ランダム、といっても状況によって欲しいランダム性は違ってきます。
- (A) 1~6のいずれかが出るサイコロを3回振るような状況
- (B) 1~6が書かれたカードが1枚ずつあって、シャッフルして3枚順番に引いていく状況
- (C) 6曲の音楽を、なるべく重複せずにループで繰り返し聞き続けたいとき
- (D) 発射した銃の弾丸が、ブレて着地がずれる度合い
これらを比べてみると「乱数」といっても必要な性質が案外違うことが分かります。 次では、それぞれに合った乱数の使い方や考え方を見ていきましょう
さて、疑似乱数はこの中でどういう位置づけになると思いますか?
疑似乱数は(A)の、サイコロにあたります。
(B)(C)(D)は、そのサイコロを利用して再現します。
(B)は、(A)を利用してシャッフルする方法を考えます。
(C)は、再生していない曲リストを保持しておいて、その中から1つ(A)を利用して抽出、リストを更新していって、リストを使い切るとリセットが定番でしょう。
(D)は、なるべく、銃の中央によりつつ最大で10pxズレるけれど、最大値にはなりにくいみたいにはどうしたらいいか、ですね。
(D)はいろんなやり方がありますが、一つはサイコロをたくさん使う方法です。1~6のサイコロを3個振った合計を使う、とするとどうでしょう。3~18の値になり、中央は10~11です。3や18などの極端な、端っこの数にはなりにくくなります。
それゆえ、ゲームでのランダムな数値のコアとして疑似乱数が大事である、ということが少し見えてくるかと思います。
そういうわけで、では、そんな疑似乱数の良し悪しや、使い分けについてみていきましょう。
周期が長い
疑似乱数は、計算を利用するため、周期というものが生まれてしまいます。
1,2,3,6,4,5...
と、この短いワンセットの繰り返しだと、ゲームとしては使えるものではありません。
1~6の数字が欲しい、としても、とっても長いパターンであってほしい、というわけです。
周期の短さがゲーム性につながることがある
疑似乱数の周期が長い方が優秀ではあるものの、ゲームで頭を捻るためには、周期が狭い、限られている方が良いことがあります。
ブラックジャックやポーカー、麻雀などは、残っているカードは何だろう、と考えながら遊ぶことができ、それが大切だったりします。
さっき「ハートのA」は手元に来た、だから、山札に残っているAはあと3枚で、と考えることができ、そうした可能性の偏りに対する嗅覚を鋭くしていくことが大事なゲームがあります。
残りのカードから、最適解を考えられるようにする、それでいて、ちょっと考えただけでは最適解が分からないし、最後の最後はほんの少し運がいる、そんな塩梅の大切さがあったりします。
独立しているか?
サイコロを2回振るとき、1回目と2回目で、どんな結末になるか、影響を与えることはありませんし、影響を与えてはいけません。
疑似乱数もそうした、一つ一つが独立しているほど品質が良い、とされます。
1回目に、3が出た、だから2回目は3がまず出ない、となると、それは2回目のサイコロの出目の出現率がおかしくなっています。
3が連続で出ることは珍しいけれど、2回目に3が出る確率は1回目がどうであれ、影響するのは良くないんです。
残念なRPGを考えてみましょう。
- 1~3のいずれかが出た次は、4~6が出る、そんなサイコロをイメージします。
- 攻撃であたったかどうかは、サイコロを振って出目が3以下だったとき。
- 与えるダメージは、続いてサイコロを振ってその値を使います。
そうすると困ったことが起きます。
与えるダメージは必ず4~6のいずれかになってしまうのです。
しっかり独立していないと、疑似乱数の性質によってバランス調整がうまくできなかったり、宝箱は出現するけれど絶対に取得できないアイテムなんてものも発生してしまいます。
宝箱でももう一度考えてみると
- 1~3のいずれかが出た次は、4~6が出る、そんなサイコロをイメージします。
- 戦いが終わると宝箱が出現するのは、サイコロを振って出目が3以下だったとき。
- 宝箱が出現したとき、つづいて中身の判定としてサイコロを振って、ドロップアイテム表に1~6それぞれアイテムが決まっていて、それを利用する。
この場合も、ドロップアイテム表の4~6のアイテムしかドロップしません。これは大問題ですね。
こうした対策の一つとして、攻撃用の疑似乱数、与えるダメージ用の疑似乱数と分けてしまうように工夫したりもできますが、すべての専用の疑似乱数を作るわけにもいきません。
(B) Fisher–Yates シャッフルの考え方
シャッフル(配列をランダムに並べ替える処理)の定番は Fisher–Yates シャッフル(Knuth shuffle とも呼ばれます)です。 ゲームでもトランプやカード山札の順番を決めるときに必須ですね。
Fisher–Yates シャッフルの考え方
- 配列の最後から順に「まだ並べ替えていない範囲」を決める。
- その範囲の中でランダムに1つインデックスを選ぶ。
- 現在の位置と選んだ位置の要素を入れ替える。
- 範囲をひとつ縮めて繰り返す。
こうすることで、すべての順列が等確率で出現します。 「すべての並べ替えが同じ確率で選ばれる」のがポイントで、これを満たさない方法(例: 単純にランダムな2要素をN回入れ替える)は偏りが出やすいです。
Goでの実装例
package main
import (
"fmt"
"math/rand"
"time"
)
func shuffle(slice []int) {
// 乱数の種を現在時刻で初期化
rand.Seed(time.Now().UnixNano())
// Fisher–Yates shuffle
for i := len(slice) - 1; i > 0; i-- {
j := rand.Intn(i + 1) // 0~i の範囲からランダムに選ぶ
slice[i], slice[j] = slice[j], slice[i]
}
}
func main() {
cards := []int{1, 2, 3, 4, 5, 6}
shuffle(cards)
fmt.Println(cards)
}
(C) の定番アプローチ
- 曲リストを Fisher–Yates シャッフルで並べ替えてから順番に再生。
- 全部再生し終わったら、もう一度シャッフルしてリストを作り直す。
(D) 弾のブレの定番アプローチ
1. 正規分布 (Gaussian/Normal)
ブレの中心(照準の中央)に近いほど出やすい。FPS など「リアル志向」でよく使われる。自然に「中央に集弾する」感じが出る。
狙いの中心に近いほど当たりやすく、端っこはめったに起きない――この“山なり”の出方をここでは中央寄せのランダムと呼びます。 イメージは「的に矢を撃つと、ど真ん中付近に矢が多く刺さる」感じです。
2. 複数乱数の合計 (近似正規分布)
サイコロを複数振るイメージ。
例:疑似乱数を6回生成して足して平均を取る。
中央寄りが出やすく、極端な値が出にくい。
3. あえて、一様分布
あえて、ブレがばらける感じにすることもできます。
-5~+5、の値を生成する疑似乱数を二回使って、それぞれX、Yに適応します。
4. ばらけかたを固定・パターンを使う
ブレのパターンを保持しておき、1発目、2発目、3発目と利用します。
1発目は(1,0)、2発目は(-1,-3)、3発目は(-2,-1)のように、ブレの幅を決めてしまいます。
銃弾のばらけかたも、銃の種類によってクセがある、というゲームを成立させ、クセを覚えて対応できるようにする、技術介入度を増やすようなこともできます。
最後に
今回は、疑似乱数の品質や具体的な用法について注目してみました。
そんな疑似乱数をどうやって作るのか、についてはまた別の機会にしたいと思います。
弾のブレの定番アプローチで提示したように、それっぽければよかったり、クセとしてあえて固定パターンにして乱数を使わないこともあります。
最高品質を必ず目指す必要はありませんが、**独立しているか?**で示したような残念なRPGの失敗例をしないためにも、一定の疑似乱数の知識が必要でしょう。