【Unity】再現可能な乱数を扱う方法

こじゃらこじゃら

ランダムな地形を毎回同じパターンで生成する方法はないの?

このはこのは

いくつか方法があるわ。Unityの乱数は疑似乱数と呼ばれ、計算によって乱数を求めるから可能なの。

再現可能な乱数を得る方法の紹介です。

本記事の内容を実践すると、毎回同じパターンの乱数生成が実装できるようになります。

乱数にはUnity標準のUnityEngine.Randomクラスのほか、Mathematicsパッケージ提供のUnity.Mathematics.Random構造体が存在します。

両者とも乱数の内部状態を持っており、状態が一緒なら次に求まる乱数値は毎回同じ(再現可能)です。

また、後者のUnity.Mathematics.Random構造体はメインスレッド以外でも使用可能というメリットがあります。

本記事では、UnityEngine.RandomクラスおよびUnity.Mathematics.Random構造体それぞれにおいて再現可能な乱数として扱う方法を解説していきます。

動作環境
  • Unity 2022.1.7f1

スポンサーリンク

乱数の内部処理について

乱数再現の実装を理解するにあたって、まず既存の乱数の内部処理について軽く触れておきます。不要な方は手順の解説までスキップしてください。

Unityが扱う乱数は疑似乱数であり、ある規則に従って乱数が計算されます。

例えば、UnityEngine.RandomクラスXorshift 128アルゴリズムをにより疑似乱数を実現しています。

参考:Random – Unity スクリプトリファレンス

乱数の内部状態が同じなら、次に求まる乱数も同じです。

疑似乱数では、初期状態をシード値によって決定することで、毎回同じ乱数列を得ることができます。

Unityが提供する乱数には、最初から使えるUnityEngine.Randomクラスのほか、Mathematicsパッケージをインポートして使えるUnity.Mathematics.Random構造体が存在します。

どちらも再現可能な乱数を得られますが、後者のUnity.Mathematics.Random構造体のほうがマルチスレッドに対応している独立した乱数生成器として扱うのが簡単というメリットがあります。

// 乱数インスタンス生成
Unity.Mathematics.Random random = new Unity.Mathematics.Random(1234);

・・・(中略)・・・

// 乱数取得
var randValue = random.NextInt(0, 1000);

Mathematicsパッケージが使える環境ならUnity.Mathematics.Random構造体を使う方が実装も手軽で得られるメリットも大きいです。

シード値を指定して乱数を再現する

UnityEngine.Randomクラスでは、シード値を指定して状態を初期化することができます。これはUnityEngine.Random.InitStateメソッドを通じて行います。

public static void InitState(int seed);

引数にはシード値を指定します。このシード値が同じなら、得られる乱数列も同じになります。

参考:Random-InitState – Unity スクリプトリファレンス

実際の使い方は以下の通りです。

RandomExample.cs
using UnityEngine;

public class RandomExample : MonoBehaviour
{
    // 乱数シード
    [SerializeField] private int _seed = 1234;

    private void Start()
    {
        // 指定されたシードで内部状態初期化
        UnityEngine.Random.InitState(_seed);
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            // 0~999の範囲の乱数取得
            var randValue = UnityEngine.Random.Range(0, 1000);

            // 乱数をログ出力
            Debug.Log($"乱数値 : {randValue}");
        }
    }
}

指定されたシードで乱数を初期化し、スペースキーが押されるたびに乱数値をログ出力する例です。

上記スクリプトをRandomExample.csという名前で保存し、適当なゲームオブジェクトにアタッチすると機能するようになります。

必要に応じて、インスペクターからシード値を設定します。

実行結果

この方法の問題点

例で示した方法では、複数の目的で乱数を使用した場合、乱数の再現ができなくなる可能性がある欠点があります。

例えば、毎回同じ地形のパターンを生成するときに、不定期に発生するエフェクト類などで乱数を活用した場合、異なる地形が生成されてしまうという問題が生じます。

これは、取得する乱数の順序が異なってしまうためです。

次に、この問題を解決する方法について解説します。

独立した乱数列を再現する

UnityEngine.Randomクラスはシングルトンであり、提供されているメソッドやプロパティはすべてstaticです。

そのため、複数の目的で再現可能な乱数を扱えるようにしたい場合、そのまま乱数取得では上手くいかず、乱数の状態を別々で保持しておく必要があります。

乱数の内部状態は、UnityEngine.Random.stateプロパティとしてアクセスできます。

参考:Random-state – Unity スクリプトリファレンス

このように複数の再現可能な乱数を扱いたい場合、乱数の使用箇所で次のようなステップを踏む必要があります。

実装の流れ
  • UnityEngine.Random.stateプロパティの値を一時退避
  • UnityEngine.Random.stateプロパティに内部状態を一時的に設定
  • 乱数取得
  • UnityEngine.Random.stateプロパティに一時退避した値を戻す

毎回この操作を行うのは面倒なので、メソッド化しておくなどで使いやすくしておくと良いでしょう。

以下、複数の再現可能な乱数を使用できるようにするヘルパークラスの実装例です。

ReproducibleRandom.cs
public struct ReproducibleRandom
{
    private UnityEngine.Random.State _state;

    /// <summary>
    /// 指定されたシード値で初期化するコンストラクタ
    /// </summary>
    /// <param name="seed">シード値</param>
    public ReproducibleRandom(int seed)
    {
        var prevState = UnityEngine.Random.state;

        UnityEngine.Random.InitState(seed);

        _state = UnityEngine.Random.state;
        UnityEngine.Random.state = prevState;
    }

    /// <summary>
    /// 指定されたint型の範囲の乱数取得
    /// </summary>
    /// <param name="minInclusive">下限(この値も範囲に含まれる)</param>
    /// <param name="maxExclusive">上限(この値は範囲に含まれない)</param>
    /// <returns>乱数値</returns>
    public int Range(int minInclusive, int maxExclusive)
    {
        var prevState = UnityEngine.Random.state;
        UnityEngine.Random.state = _state;

        var result = UnityEngine.Random.Range(minInclusive, maxExclusive);

        _state = UnityEngine.Random.state;
        UnityEngine.Random.state = prevState;

        return result;
    }
    
    // float型の乱数などを使いたい場合、同様にメソッドを実装する
}

使用の際は、上記スクリプトを適当な場所に保存しておきます。

呼び元からは以下のようにして使います。

ReproduceRandomExample.cs
using UnityEngine;

public class ReproduceRandomExample : MonoBehaviour
{
    // 乱数1のシード値
    [SerializeField] private int _seed1;

    // 乱数2のシード値
    [SerializeField] private int _seed2;

    // 再現可能な乱数の内部状態を保持するインスタンス
    private ReproducibleRandom _random1;
    private ReproducibleRandom _random2;

    private void Start()
    {
        // 再現可能な乱数を初期化
        _random1 = new ReproducibleRandom(_seed1);
        _random2 = new ReproducibleRandom(_seed2);
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            // 乱数1の次の値取得
            var rand1Value = _random1.Range(0, 1000);

            Debug.Log($"乱数1の値 : {rand1Value}");
        }

        if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            // 乱数2の次の値取得
            var rand2Value = _random2.Range(0, 1000);

            Debug.Log($"乱数2の値 : {rand2Value}");
        }
    }
}

2つの乱数をそれぞれキーボードの1キーと2キーで独立して取得し、ログ出力する例です。

上記スクリプトをReproduceRandomExample.csという名前で保存し、適当なゲームオブジェクトにアタッチすると機能するようになります。

必要に応じてシード値を設定します。

実行結果

それぞれの乱数で同じ順序の値を返していることを確認できました。

Unity.Mathematics.Randomで独立した乱数を管理する

Mathematicsパッケージをインストールする必要がありますが、Unity.Mathematics.Random構造体を使うと、簡単に独立した乱数を扱うことができます。

更に、メインスレッド以外からも使用可能というメリットも存在します。

参考:Struct Random| Mathematics | 1.2.6

Unity.Mathematics.Random構造体はUnityEngine.Randomクラスとは違い、シングルトンではありません。使用時にはインスタンス化して使います。

以下、Unity.Mathematics.Random構造体を使って乱数を取得する例です。

MathRandomExample.cs
using UnityEngine;

public class MathRandomExample : MonoBehaviour
{
    // 乱数1のシード値
    [SerializeField] private uint _seed1;

    // 乱数2のシード値
    [SerializeField] private uint _seed2;

    // 再現可能な乱数の内部状態を保持するインスタンス
    private Unity.Mathematics.Random _random1;
    private Unity.Mathematics.Random _random2;

    private void Start()
    {
        // 再現可能な乱数を初期化
        _random1 = new Unity.Mathematics.Random(_seed1);
        _random2 = new Unity.Mathematics.Random(_seed2);
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            // 乱数1の次の値取得
            var rand1Value = _random1.NextInt(0, 1000);

            Debug.Log($"乱数1の値 : {rand1Value}");
        }

        if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            // 乱数2の次の値取得
            var rand2Value = _random2.NextInt(0, 1000);

            Debug.Log($"乱数2の値 : {rand2Value}");
        }
    }
}

スクリプトの使い方は先述の例と一緒です。

実行結果

同様に乱数の再現が実現できていることを確認できました。

さいごに

再現可能な乱数を使用する方法を解説しました。

乱数を使いたい場所で、それぞれ状態を持ったインスタンスを作成して管理できるようにすれば実現できます。

差支えが無ければ、Unity.Mathematics.Randomを使用するのが手軽で確実です。

関連記事

参考サイト

スポンサーリンク