(:3[kanのメモ帳]

個人ゲーム開発者kan.kikuchiのメモ的技術ブログ。月木更新でUnity関連がメイン。

Unityで乱数の再現【Unity】


このエントリーをはてなブックマークに追加

この記事でのバージョン
Unity 2018.2.8f1


はじめに

UnityではRandomクラスを使って簡単に乱数(ランダムな値)を生成する事が出来ます。

//0 ~ 1の間でランダムな数(float)を取得
float floatValue = Random.value;

//0 ~ 9の間でランダムな数(int)を取得 
int intValue = Random.Range(0, 10);


今回はこの乱数を再現させる方法の話です。


乱数の再現

まずは乱数の再現とはなんぞやという話からです。


そもそも乱数と言っても本当に完全にランダムな値を生成してるわけではなく、

一定のルールに沿って計算されたランダムっぽい値(疑似乱数)を使っています(Unityに限らず)。


なので、同じ初期化を行えば(シードを与えれば)毎回同じパターンを生成する事が可能なわけです。

(逆に言えば、違う初期化を行えば毎回違うパターンを生成する事も可能。)


具体的な例で言えば「0 ~ 9の間の乱数を生成する」という場合に

「毎回1, 5, 3から始まる(その後のパターンも同じ)」ように出来ると言った感じです。


この性質(機能?)を使えば、

ランダムに生成されるダンジョンや敵のパターンなどの再現や確認、共有が出来るわけです。


肝心のUnityのRandomクラスの初期化にはInitStateを使います。

//10という値(シード)で初期化
Random.InitState(10);

//前はseedだった
//Random.seed = 10;


InitStateの引数にはintの値を使いますが、

これが同じなら取得できる乱数のパターンが同じになり、乱数の再現が出来ます。

//10という値(シード)で初期化
Random.InitState(10);

//0 ~ 9の値を10個、ランダムに表示(シードが同じなので毎回同じパターン)
for (int i = 0; i < 10; i++) {
  Debug.Log(Random.Range(0, 10));
}

f:id:kan_kikuchi:20181025144318j:plain


ただし、乱数を取得する対象が複数ある場合は

取得するタイミングや回数も同じでなければ再現が出来ません。


例えば「サイコロの数をランダムに出す」処理と「ランダムなエフェクトを出す」処理がある場合、

サイコロ→エフェクト→サイコロとエフェクト→サイコロ→サイコロでは

同じシードでも2回目のサイコロの値が変わってしまいます。


とは言え、乱数を取得するタイミングや回数を毎回同じにするというのは、実質不可能だと思うので、

乱数を生成するインスタンス(乱数生成機みたいなもの)をサイコロとエフェクトで分けて作る

のが常套手段だと思います。


ここで一つ問題になるのが、UnityのRandomクラスは変数や関数はStaticだという事です。


f:id:kan_kikuchi:20181025144825j:plain


変数や関数がStaticという事は乱数生成機を別々に作って運用するという事は出来ません。

//サイコロ用の乱数生成器を作成(クラス自体はStaticではないので、一応エラーは出ない)
Random diceRandom = new Random();

//サイコロの数はエフェクトに影響されないようにサイコロ用の乱数生成器から取得(出来ない、エラー)
int diceNum = diceRandom.Range(0, 10);


そんな時に約立つのがRandom.Stateです。

乱数生成器の完全な内部状況を把握するために使用されます。


これを使えばRandomの状態を保持出来るので、乱数を別々に運用する事が可能になるという寸法です。

ちなみに具体的なコードは以下の講演で公開されていたりします。



using UnityEngine;

public class MyRandom {
  private Random.State state;

  public MyRandom() : this((int)System.DateTime.Now.Ticks){}

  public MyRandom(int seed) {
    setSeed(seed);
  }

  public void setSeed(int seed) {
    var prev_state = Random.state; Random.InitState(seed);
    state = Random.state; Random.state = prev_state;
  }

  public int Range(int min, int max) {
    var prev_state = Random.state; // 使用前の状態 
    Random.state = state; // 前回の状態にセット 
    var result = Random.Range(min, max); state = Random.state; // 現在の状態を記録 
    Random.state = prev_state; // 使用前の状態に 
    return result;
  }

}


使い方は以下のような感じ。

//サイコロ用の乱数生成器を作成(シードを固定してるので、毎回同じパターンになる)
MyRandom diceRandom = new MyRandom(10);
//MyRandom diceRandom = new MyRandom(); //シードを設定しない事も可能(毎回違うパターンになる)

//サイコロの数はエフェクトに影響されないようにサイコロ用の乱数生成器から取得
int diceNum = diceRandom.Range(1, 7);


なお、UnityのRandomを使わずに、自分で乱数生成用クラスを作るのも、もちろん有りです。

そしてなんと、そんな場合のコードも先程の講演で公開されていたりします。

(Rangeメソッドは追加しました。)

using System;

public class MyRandom {
  private uint x, y, z, w;

  public MyRandom() : this((uint)DateTime.Now.Ticks) {}

  public MyRandom(uint seed) {
    setSeed(seed);
  }

  public void setSeed(uint seed) {
    x = seed; y = x * 3266489917U + 1; z = y * 3266489917U + 1; w = z * 3266489917U + 1;
  }

  public uint getNext() {
    uint t = x ^ (x << 11); 
    x = y; 
    y = z; 
    z = w; 
    w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));
    return w;
  }

  public int Range(int min, int max) {
    return min + Math.Abs((int)getNext()) % (max - 1);
  }

}


使い方は以下のような感じ。(UnityのRandomを使う場合と全く同じ)

//サイコロ用の乱数生成器を作成(シードを固定してるので、毎回同じパターンになる)
MyRandom diceRandom = new MyRandom(10);
//MyRandom diceRandom = new MyRandom(); //シードを設定しない事も可能(毎回違うパターンになる)

//サイコロの数はエフェクトに影響されないようにサイコロ用の乱数生成器から取得
int diceNum = diceRandom.Range(1, 7);