Programming

UnityでFisher-Yates Shuffleアルゴリズムを実装してみた

Programming
スポンサーリンク

最近は季節の変わり目なのか、寒暖の差が激しいですね。
ここしばらくブログの更新が滞っていました(汗)が、今日はUnity(というかC#で数学ネタ)を取り上げてみました。
ランダムシャッフルの実装です。

ちなみに、この記事の続きです。

Fisher-Yates Shuffle

ランダムなシャッフルって?

文字通り、リストの中身を無作為に並べ替えるということです。トランプの手札や数字の並べ替えなどが代表的な例ですね。

言葉にすれば簡単なのですが、では無作為なとはどういうことでしょうか?
要は並び方に意図や法則などが見当たらない、言ってしまえば毎回でたらめにパターンが出てくるというだけなのですが、言うは易し行うは難し、という典型例でこの「でたらめ」を作ることが本当に難しい。何せ、パターン自体の並び方はもとより、出現する組み合わせの順番にも気を付けなければならないからです。

例えば、1~4の4つの数字が入った配列を並び替えるとして、「1324, 4132, 2413,…」という結果が出たとします。
一見するとバラバラにも見えますが、よく見比べると「リストの要素を毎回右に一つずつずらす」という法則が分かります。そうするとこれはもうランダムとは言えなくなってしまいます。

どうやって作るのか?

プログラミングでランダムを生み出す際は、もっぱら乱数が使われます。

まあ、当然といえばそうなのですが、問題はその使い方。今回の場合、すぐに思いつくのは「乱数で適当な2要素の並べ替えを繰り返す」というものではないかと。何回か繰り返せばとりあえず中身が混ざるのではないかと考えてもおかしくはありません。

しかし、問題は並べ替えの回数。果たして何回行えば十分にシャッフルできたといえるでしょうか?例えば要素の数だけ実行したとして、実は不十分だったり、あるいは過剰に回しすぎているのかは分かりません。更には要素数が増えたらどうするかも不明です(ランダムシャッフル | Programming Place Plus アルゴリズムとデータ構造編【その他のアルゴリズム】 第2章 )。

Fisher-Yates Shuffle

そこで、ロナルド・フィッシャーとフランク・イェーツという人物が考案した手法を元にコンピュータ用へ改良したアルゴリズムが「Fisher-Yates Shuffle」です。疑似コードは以下の通り(Wikipedia)。

言い換えると「最後尾の要素から一つずつ順に、それより前の要素とランダムに並び替え」るという手法で、最大の利点はすべての要素が最低でも1回は並び替えの対象になるということです。交換対象の一つが最後尾から前に1個ずつ順番に選ばれるので、全要素が並び替えられる上、(前述の手法と比べて)乱数の生成回数も減るので処理の効率化にもつながります。

Unityで実装

では、このアルゴリズムを実装してみましょう。今回はUnityとC#を使ってみました。

空のオブジェクト作成

まずはC#スクリプトを作ります。空のゲームオブジェクト(Create Empty)を作成して、Add Componentボタンから「New Script」を選び、好きな名前を付けてから「Create and Add」ボタンで作成します。今回はRandomShuffleと名付けました。

C#スクリプトがAssetsフォルダに出てくるので、さっそくエディタで開いてみましょう。

C#スクリプトの編集

エディタでスクリプトを開いたら、C#でFisher-Yatesを実装します。下のコードをコピペすればOKです。
ちなみに、このコードは2種類のシャッフル方法(ランダム選択とFisher-Yates)を実装しています。

オブジェクトの配置と設定

スクリプトを編集したら、CGモデルを配置してみましょう。
今回はテストなのでプリミティブ(立方体、球体、カプセル、円錐)を配置してみました。場所はどこでも構いません。

ちなみにTest.csは関係ありません

そのあとはスクリプトの設定をします。設定項目と役割は以下の通りです。

  • Models: シャッフルの対象となるオブジェクト
  • Mode: シャッフル方法(ランダム交換 or Fisher-Yates)
  • Trials: シャッフルの試行回数
  • Cycle: 各試行のフレーム間隔(次のシャッフルまで何フレーム空けるか)

Modelsには、要素数を指定してから対象のゲームオブジェクトを割り当てます。今回は直前で配置したプリミティブを指定しましょう。また、プリミティブをRandomShuffle(が割り当てられたゲームオブジェクト)の子要素にしておくと後々扱いやすくなります。

Modeはシャッフル方法の選択です。ランダム交換の繰り返しとFisher-Yatesのどちらかをドロップダウンメニューで選べます。

また、Trialsで試行回数を、Cycleで各試行のフレーム間隔を指定します。試行回数が増えれば(Fisher-Yatesでの)パターンのばらつきが減少していき、フレーム間隔が増えればシャッフルがゆっくり実行されていきます。
なお、シャッフル時間 = Trials / (FPS / Cycle) と概算できるので、時間に注意しながら設定しましょう。

シャッフル方法の比較

プリミティブの割り当てと親子付けが終わったら、Ctrl+Dで複製します。また、複製したオブジェクトを少し上に移動しておきます。

上に移動したら、Modeをコピー元とは別のシャッフル方法にしておきます。

テスト

設定が完了したら、さっそく実行してみましょう。

UnityでFisher Yatesアルゴリズムを試してみた

プリミティブがランダムに並べ替えられていく様子が見られます。

シャッフルが終わると、各パターンの出現数と割合、そして最多と最少パターンの偏差を出してくれます。今回はランダム交換で約6.3%(=19回)、Fisher-Yatesで約4.3%(=13回)でした。

Fisher-Yates
ランダム交換の繰り返し

思ったより偏りが多いなと思われる方もいらっしゃるかもしれませんが、試行回数が300と少ないことに起因するものです。実際、Trialsを1000や10000と増やしていくとFisher-Yatesでは偏りがより少なくなる一方で、ランダム交換ではまだ偏差が出てしまうので、Fisher-Yatesの効果の高さがうかがえます。

カードゲームの作成などでシャッフル方法を探していた方は、ぜひ一度お試しあれ。

参考

ランダムシャッフル | Programming Place Plus アルゴリズムとデータ構造編【その他のアルゴリズム】 第2章

ランダムシャッフル | Programming Place Plus アルゴリズムとデータ構造編【その他のアルゴリズム】 第2章

C# でリストをシャッフルする (下のFisher-Yatesの項目を参照)

C# でリストをシャッフルする
残念ながら、C# でリストをシャッフルするために使用できる事前定義されたメソッドはありません。したがって、C# でリストをシャッフルするには、自己生成コードに依存する必要があります。

フィッシャー–イェーツのシャッフル (Wikipedia)

フィッシャー–イェーツのシャッフル - Wikipedia
タイトルとURLをコピーしました