【Unity】【C#】配列・リストをランダムソート(シャッフル)する

スポンサーリンク

今回は、C#で配列からランダムにデータを取得(シャッフル)する事を考えたいと思います。
Unityでの落ちゲー開発で、お邪魔ブロックを落下させる時に、どの列に落とすかを重複しないようにランダムで決める時に使ったりします。(列番号を配列にして、シャッフル後に先頭からブロックの数だけ取り出す)
カードゲームなら、カードのシャッフルとかに使います。

スポンサーリンク

古典的な方法(Fisher-Yates)

検索したら出てくるFisher-Yatesアルゴリズムです。と言うより、実質これしか無い気もしますね。
「配列からランダムに取り出して、後ろに持って行って固定」を繰り返して行く実装です。

元の配列やリストがランダムソートされてしまうので注意が必要です。
元の順番はそのままにしたい場合は、コピーしたもので行うようにしましょう。

using System;
using System.Collections;

// 配列のシャッフル
Random rand = new Random();
int[] numbers = { 1, 2, 3, 4, 5};
int n = numbers.Length;
for (int i = n - 1; i > 0; i--)
{
    int j = rand.Next(i + 1);
    int temp = numbers[i];
    array[i] = numbers[j];
    array[j] = temp;
}

// リストのシャッフル
List<string> words = new List<string> { "A", "B", "C", "D", "E" };
n = words.Count;
for (int i = n - 1; i > 0; i--)
{
    int j = rand.Next(i + 1);
    if (i == j) continue;
    string temp = words[i];
    words[i] = words[j];
    words[j] = temp;
}

Unityの場合は、System.Randomではなく、UnityEngine.Randomを使う方が一般的でしょうか。

using UnityEngine;
using System.Collections;

// 配列のシャッフル
int[] numbers = { 1, 2, 3, 4, 5};
int n = numbers.Length;
for (int i = n - 1; i > 0; i--)
{
    int j = Random.Range(0, i + 1);
    if (i == j) continue;
    int temp = numbers[i];
    array[i] = numbers[j];
    array[j] = temp;
}

// リストのシャッフル
List<string> words = new List<string> { "A", "B", "C", "D", "E" };
n = words.Count;
for (int i = n - 1; i > 0; i--)
{
    int j = Random.Range(0, i + 1);
    if (i == j) continue;
    string temp = words[i];
    words[i] = words[j];
    words[j] = temp;
}

古典的な手法ではありますが、これを次のように汎用メソッド化してしまえば十分使い勝手が良くなります。(Unityでの例)

// 配列のシャッフル
static public void ShuffleArray<T>(T[] array)
{
    int n = array.Length;
    for (int i = n - 1; i > 0; i--)
    {
        int j = Random.Range(0, i + 1);
        if (i == j) continue;
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
}
// リストのシャッフル
static public void ShuffleList<T>(List<T> list)
{
    int n = list.Count;
    for (int i = n - 1; i > 0; i--)
    {
        int j = Random.Range(0, i + 1);
        if (i == j) continue;
        T temp = list[i];
        list[i] = list[j];
        list[j] = temp;
    }
}

Linqを使う方法

これも検索すると出てくる方法です。
配列をデータとしてLinqで扱って、ランダムで与えた値でソートする方法です。

using System;
using System.Collections;
using System.Linq;

// 配列のシャッフル
int[] numbers = { 1, 2, 3, 4, 5};
int[] shuffledNumbers = numbers.ToList().OrderBy(x => Guid.NewGuid()).ToArray();
// リストのシャッフル
List<string> words = new List<string> { "A", "B", "C", "D", "E" };
List<string> shuffledWords = words.OrderBy(x => Guid.NewGuid()).ToList();

記述は1行に収まってシンプルですね。
ですが、Linqの扱いに慣れていないと少し分かりにくい実装ですし、Fisher-Yates手法に比べて処理は少し遅くなります。

RandomのShuffleメソッドを使う方法(C# 8.0/.NET8 以降)

C#8.0以降であれば、RandomクラスのShuffleメソッドが追加されたので、簡単にシャッフルできます。

元の配列がランダムソートされてしまうので注意が必要です。
元の順番はそのままにしたい場合は、コピーしたもので行うようにしましょう。

using System;
using System.Collections;

Random rand = new Random();

// 配列のシャッフル
int[] numbers = { 1, 2, 3, 4, 5 };
rand.Shuffle(numbers);

// リストのシャッフル (一度配列にして、シャッフル後にリストに戻す必要があるっぽい)
int[] words = new List<string> { "A", "B", "C", "D", "E" }.ToArray();
rand.Shuffle(words);
List<string> shuffledWords = words.ToList();

少し公式ドキュメントを見てみましたが、Shuffleの中身を見るとFisher-Yatesのアルゴリズムだったので、古典的な方法で出した実装とやっていることは変わりなさそうです。

ただ、このメソッドはUnityでは使えなかったです。
Geminiに聞いたら、Random.Shuffleがあるって回答があったけど、見つからなかった……
Unityの公式ドキュメントもFisher-Yatesの実装が紹介されてますし、どうやらUnityでは使えなさそうですね。

まとめ

個人的にはUnityでの実装を考えるなら、Fisher-Yatesの実装が良いと思います。
Linqを使っても良いのですけど、記述がスマートに見えるだけであまり利点が無い気がします。
(プロジェクトの中でLinqを多用しているのなら、Linqを使った実装でも良いとは思いますが…)
Shuffleメソッドを使えるのなら、それを使うのが一番でしょうか。(ただ、リストのまま処理できなかったのが少し微妙……)

スポンサーリンク