2.6K Views
October 22, 17
スライド概要
2017/10/8に開催されたUnity道場スペシャル 2017札幌の講演スライドです。
講師:安原 祐二(ユニティ・テクノロジーズ・ジャパン合同会社)
講演動画:https://youtu.be/VfJAgRZ338k
優れたゲームプログラム、その違いが現れるのは処理速度だけではありません。細かなテクニックを使用することで表現にも差がつきます。今回は乱数にスポットをあてて、その特徴や注意点、そして応用例についてお話しします。乱数はゲームプログラムの基本中の基本です。みなさんが作成中のゲームにも、すぐに使えるテクニックを身につけられます!
こんな人におすすめ
・ゲームプログラムの中級者を目指す方
・乱数やノイズの応用例を知りたい方
得られる知見
・乱数の数学的背景
・乱数の注意点
・乱数やノイズの応用例
Unityのイベント資料はこちらから:
https://www.slideshare.net/UnityTechnologiesJapan/clipboards
リアルタイム3Dコンテンツを制作・運用するための世界的にリードするプラットフォームである「Unity」の日本国内における販売、サポート、コミュニティ活動、研究開発、教育支援を行っています。ゲーム開発者からアーティスト、建築家、自動車デザイナー、映画製作者など、さまざまなクリエイターがUnityを使い想像力を発揮しています。
乱数完全マスター ユニティ・テクノロジーズ・ジャパン合同会社 フィールド・エンジニア 安原 祐二
今回のおはなし 前半 後半 仕組み 使いかた
今回のおはなし 前半 後半 仕組み 使いかた
コンピュータ計算のきほん
0か1しか入れられない箱 1?0 ビット(bit)と数える
32ビットの場合 32個 二進数で32桁 32 2 -1 まで数えられる (4,294,967,295)
32ビットの場合 32個 二進数で32桁 限界を越えちゃったらどうなる? 32 2 -1 まで数えられる (4,294,967,295)
繰り上がったぶんは無視 無視! 計算結果 32個 2 で割った余りになっている 32
3桁しかないデジタル表示は 0 9 9 9 999の次は000 3 1000(10 )で割った余りになっている
例えば限界のところに1加えると・・ 無視! 計算結果 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
無視! 計算結果 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ゼロにもどる 循環する
乱数を作ってみよう
乱数使用例 var a = Random.value; そのたびに異なる値になる 擬似乱数とは 計算でバラバラの数を作ること! 簡単な乱数を作ってみよう
0〜7の範囲で乱数を作りたい 前回の値に1を加える a←a+1 新しい値 前回の値
0〜7の範囲で乱数を作りたい 前回の値に1を加える a←a+1 範囲を越えた 番目 値 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8
番目 値 0 0 1 1 2 2 3 3 a←(a+1)%8 4 4 5 5 0〜7の範囲になる 6 6 7 7 8 0 0〜7の範囲で乱数を作りたい 前回の値に1を加えて8で割った余り 循環している
0〜7の範囲で乱数を作りたい 前回の値に2を加えて8で割った余り a←(a+2)%8 0246しか出てこない… 番目 値 0 0 1 2 2 4 3 6 4 0 5 2 6 4 7 6 8 0
0〜7の範囲で乱数を作りたい 前回の値に3を加えて8で割った余り a←(a+3)%8 番目 値 0 0 1 3 2 6 3 1 4 4 5 7 6 2 7 5 8 0
0〜7の範囲で乱数を作りたい 前回の値に3を加えて8で割った余り a←(a+3)%8 すべての数字がまんべんなく現れた 乱数の完成! 番目 値 0 0 1 3 2 6 3 1 4 4 5 7 6 2 7 5 8 0
プログラム例 class Random { int s = 0; int get() { s = (s+3)%8; return s; } }
まんべんなく現れるためには 3を加えて8で割った余り」 「前回の値に ふたつの数が互いに素であればよい
マメ知識 互いに素とは ふたつの数の共通の約数が存在しない 3 ーという分数が約分できない 8
ちょっとまって! まんべんなく出ればいいって… 乱数ってそういうもの? 同じ値が続けて出ることもないと… 大きな値で循環させて 必要な範囲を取り出す
a←(a+37)%50 50で循環させて 番目 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 値 0 37 24 11 48 35 22 9 46 33 20 7 44 31 18 5 値 0 1 0 5 0 5 4 3 4 3 2 1 2 1 0 5 a%6 0~5を取り出す
32 0~2 -1の範囲で乱数を作りたい 32 前回の値にQを加えて2 で割った余り
32 0~2 -1の範囲で乱数を作りたい 32 前回の値にQを加えて2 で割った余り 32 a←(a+Q)%2
32 0~2 -1の範囲で乱数を作りたい 32 前回の値にQを加えて2 で割った余り 32 a←(a+Q)%2 32 Qは2 と互いに素のものを選ぶ Qは奇数であればいい
32 0~2 -1の範囲で乱数を作りたい 32 前回の値にQを加えて2 で割った余り 32 a←(a+Q)%2 a←a+Q 32 Qは2 と互いに素のものを選ぶ Qは奇数であればいい 32ビットなら余りを計算する必要なし!
32 0~2 -1の範囲で乱数を作りたい 32 前回の値にQを加えて2 で割った余り 32 a←(a+Q)%2 a←a+Q 32 Qは2 と互いに素のものを選ぶ Qは奇数であればいい 32ビットなら余りを計算する必要なし! 32ビットで循環できた ただ奇数を足すだけで乱数ができた
乱数のきほん
Unity の Random クラス 1. Random.InitStateで初期化 2. Random.valueなど 呼ぶ度に異なる値を返す
乱数のしくみ シード 151 シードで初期値を与える
乱数のしくみ シード 260 151 260 シードで初期値を与える 乱数を取得できる
乱数のしくみ シードで初期値を与える あとは取得し続ける シード 260 37 151 260 37
乱数のしくみ シード 260 37 151 260 37 シードで初期値を与える あとは取得し続ける ある周期で循環する 180 周期
乱数の特徴 周期がある (同じパターンを繰り返す) 再現する (シードが同じなら同じパターン)
乱数を使うときは 再現させるかどうか 常に意識する ユーザ 起動 ごとに プレイ 同じ 別の 乱数を発生させたい
再現させたい場合 シードを固定する 再現させたくない場合 シードを変化させる 現在時刻 システムクロック ボタンを押すまでの時間
シードはだいじ シードを設定せずに 乱数を使わない! ぐらいの気持ちで
いろいろな生成法
LCG(線形合同法) 19??年 高速 品質(バラバラ度)に問題あり 例 a←a×1103515245+12345 さすがに使われなくなってきた
メルセンヌ・ツイスタ 1997年 十分に高速(数百回に一度ちょっと重くなる) 品質は最上級 周期が超絶の2 19937 -1 メモリ使用大きめ(4.8KiB)
Xorshift 2003年 シンプル&高速 質も高い 32 64 128 周期は2 -1版、2 -1版、2 -1版 例 a = a ^ (a<<13); a = a ^ (a>>17); a = a ^ (a<<15);
PCG 2014年 高速 極めて質が高いらしい 決定版かもしれない? 今後採用事例が増えるかも
Unityで生成器を自作
実装の理由 生成法を掌握 系統を分ける
あちこちで使用すると… エフェクト サイコロ 敵キャラ 再現不能
系統をわける var randA = new MyRandom(); var randB = new MyRandom(); ふたつ作る UnityEngine.Random はこれができない
ゲーム進行用 エフェクト用 影響を受けない 再現可能に
そうは言っても 自作はなあ… UnityEngine.Randomには便利関数がそろっている 作るのは面倒だしバグも怖い
組み込みRandomを オブジェクト化する例 Random.state を 保存・復活する 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; }
参考
Xorshift
C#実装例
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;
}
}
間違った用法
良さげな結果 乱数っぽい
良さげな結果 乱数っぽい 失敗例 どうしてこうなった
良さげな結果
for (var y = 0; y < texture.height; ++y) {
for (var x = 0; x < texture.width; ++x) {
texture.SetPixel(x, y,
Random.Range(0, 2) == 0 ? white : black);
}
}
すべての座標で白か黒を
乱数で決定
失敗例
var i = 0;
for (var y = 0; y < texture.height; ++y) {
for (var x = 0; x < texture.width; ++x) {
Random.InitState(i); シードを設定
++i;
texture.SetPixel(x, y,
Random.Range(0, 2) == 0 ? white : black);
}
}
シードを毎回設定している
シードを単純にセットしまくるのは 想定していない生成法が多い とはいえ。
シードの設定が必要なゲームの例 100面あるダンジョンの… 迷路や敵の強さを… その都度生成しつつ… パターンを固定したい。
シード設定が一度だけの場合 シード ステージ1 ステージ2 ステージ3 ステージ4 ステージジャンプで問題
シードB シードC シードD シードA ステージ1 ステージ2 ステージ3 ステージ4 各ステージでのシード設定は必要
最初にボスの性別を決めるとする
失敗例のコード(再掲)
var i = 0;
for (var y = 0; y < texture.height; ++y) {
for (var x = 0; x < texture.width; ++x) {
Random.InitState(i);
++i;
texture.SetPixel(x, y,
Random.Range(0, 2) == 0 ? white : black);
}
}
自然にこうなる
何がいけないのか
var i = 0;
for (var y = 0; y < texture.height; ++y) {
for (var x = 0; x < texture.width; ++x) {
Random.InitState(i);
++i;
←ここ。単純な増加がダメ
texture.SetPixel(x, y,
Random.Range(0, 2) == 0 ? white : black);
}
}
シードも乱数にしてみよう
var i = 0;
var rand = new System.Random(1234);
for (var y = 0; y < texture.height; ++y) {
for (var x = 0; x < texture.width; ++x) {
Random.InitState(rand.Next()); ←乱数
texture.SetPixel(x, y,
Random.Range(0, 2) == 0 ? white : black);
}
}
さあ、結果は?
良さげ!
良さげ! しかし、これはダメ なぜかというと…
シードを乱数にしてしまうと 衝突が起きる (同じ値が出る) 32 周期が 2 より大きい場合、 衝突は起きる 可能性は低いが、 予測するのは困難
衝突しないバラバラの値が欲しい そんなときは ハッシュ関数 xxHash がおすすめ (Yann Colletさん作)
参考 xxHash の実装例 int xxhash(int data, int seed) { uint v = (uint)seed + 374761393U + 4U; v += (uint)data * 3266489917U; v = ((v << 17) | (v >> 15)) * 668265263U; v ^= v >> 15; v *= 2246822519U; v ^= v >> 13; v *= 3266489917U; v ^= v >> 16; return (int)v; ※入力を32bitにした }
xxHash は衝突しないのか? 大丈夫。 調べてみました https://qiita.com/yuji_yasuhara/items/adefc967c51a6becca08
xxHashでシードを作る
var i = 0;
for (var y = 0; y < texture.height; ++y) {
for (var x = 0; x < texture.width; ++x) {
Random.InitState(xxhash(i));
xxHashを呼ぶ
texture.SetPixel(x, y,
Random.Range(0, 2) == 0 ? white : black);
}
}
さあ、結果は?
良さげ! ひと安心
擬似乱数の恐ろしさ 問題に気付きにくい
擬似乱数の恐ろしさ 問題に気付きにくい 「偶数と奇数が交互に出る」 開発中に気づくのは困難 ましてやステージボスの性別にパターンが生じるなど… 恐さを知っておこう!
Perlinノイズ
Perlin ノイズ
ホワイトノイズ ただの乱数 Perlinノイズ とても便利!
Mathf.PerlinNoiseの使用例 第1引数に現在時刻(秒) 第2引数にゼロ void Update() { var x = Mathf.PerlinNoise(Time.time, 0f); var pos = new Vector3(x, 0f, 0f); transform.position = pos; }
Mathf.PerlinNoise(Time.time, 0f); の意味 Time.time ゼロ 上の縁をなぞる
16.0 この画像は16x16 16x16 16.0
2.0 1.0 1.0x1.0 1.0 8x8 2x2 4x4 2.0 16x16 32x32
別系統のノイズ(シードのかわり) Time.time ゼロ Mathf.PerlinNoise(Time.time, 0f); 8.0 Mathf.PerlinNoise(Time.time, 8f); 2以上離せばOK
大きなノイズ
細かいノイズ
足す
デモ:PerlinNoiseアニメーション
参考
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PerlinNoiseTest : MonoBehaviour {
Perlinノイズアニメーション
で使用したコード
Quaternion rotation_;
float c0_;
float c1_;
float c2_;
void Start () {
rotation_ = transform.localRotation;
c0_ = Random.Range(0f, 1f);
c1_ = Random.Range(2f, 3f);
c2_ = Random.Range(4f, 5f);
}
void Update () {
var freq0 = Time.time*0.4f;
var freq1 = Time.time*1f;
var ratio0 = 0.8f;
var ratio1 = 0.1f;
transform.localRotation = Quaternion.Euler(new Vector3((Mathf.PerlinNoise(freq0, c0_)-0.5f)*ratio0 + (Mathf.PerlinNoise(freq1, c0_)-0.5f)*ratio1,
(Mathf.PerlinNoise(freq0, c1_)-0.5f)*ratio0 + (Mathf.PerlinNoise(freq1, c1_)-0.5f)*ratio1,
(Mathf.PerlinNoise(freq0, c2_)-0.5f)*ratio0 + (Mathf.PerlinNoise(freq1, c2_)-0.5f)*ratio1) * 10f) * rotation_;
}
}
PerlinNoiseの周期は? どう作ってあるか次第 Unityの実装では256.0で周回
256.0で繰り返しておく void Update() { m_Time += Time.deltaTime; m_Time = Mathf.Repeat(m_Time, 256f); var x = Mathf.PerlinNoise(m_Time, 0f); … これで永遠に動く
〜作ってみよう〜 噴出花火
正規乱数 一様乱数 正規乱数
Particle System Particle System +正規乱数
デモ:Particle System 実験
正規乱数のつくりかた ボックス・ミューラー法 float getND() float x = float y = float v = return v; } { Random.value; Random.value; Mathf.Sqrt(-2f * Mathf.Log(x)) * Mathf.Cos(2f * Mathf.PI * y); 処理速度も問題ない
参考
正規乱数パーティクルの
コード
public class MyParticleEmitter : MonoBehaviour {
float getBoxMuller()
{
float x = Random.value;
float y = Random.value;
float v = Mathf.Sqrt(-2f * Mathf.Log(x)) * Mathf.Cos(2f * Mathf.PI * y);
return v;
}
void emit()
{
var ps = GetComponent<ParticleSystem>();
var ep = new ParticleSystem.EmitParams();
ep.position = Vector3.zero;
var vx = getBoxMuller()*6f;
var vy = getBoxMuller()*6f;
ep.velocity = new Vector3(vx, vy, Random.Range(45f, 55f));
ps.Emit(ep, 1);
}
void Update()
{
for (var i = 0; i < 8; ++i) {
emit();
}
}
}
〜作ってみよう〜 カード配り
デモ:カード配り
摩擦つきの移動(外力なし)
摩擦つきの移動(外力なし) 速度がある値より低くなったら
摩擦つきの移動(外力なし) 速度がある値より低くなったら 乱数でカード上の一点を選択
摩擦つきの移動(外力なし) 速度がある値より低くなったら 乱数でカード上の一点を選択 その点への摩擦力を追加 回りながら進み、止まる
実際に起きていることを考察する Rigidbody.AddForce Rigidbody.AddTorque Rigidbody.AddForceAtPosition 物理を使っておくと 条件の変更に強い
参考
カード配りの
コード
public class Card : MonoBehaviour {
bool stopping_ = false;
Vector3 position_;
static float height = 0f;
void Start() {
var rb = GetComponent<Rigidbody>();
transform.position = new Vector3(0f, height, 0f);
height += 0.1f;
rb.velocity = new Vector3(Random.Range(-2f, 2f), 0f, Random.Range(38f, 42f));
}
void FixedUpdate() {
var rb = GetComponent<Rigidbody>();
if (!stopping_ && rb.velocity.magnitude <= 4f) {
const float RANGE = 2f;
position_ = new Vector3((Random.Range(-RANGE, RANGE)+Random.Range(-RANGE, RANGE))/2,
(Random.Range(-RANGE, RANGE)+Random.Range(-RANGE, RANGE))/2,
0f);
stopping_ = true;
}
if (stopping_) {
rb.AddForceAtPosition(-rb.velocity * 2f, transform.TransformPoint(position_));
}
}
}
〜作ってみよう〜 いろいろな動き
ガタガタ道を進むクルマ どう実現しよう?
ときどき発生させる void Update() { if (Random.Range(0f, 1f) < 0.2f) { 処理 } 0~1 の乱数が0.2未満 } 1/5 の確率
ときどき発生させる void Update() { if (Random.Range(0f, 1f) < 0.2f) { 処理 } 0~1 の乱数が0.2未満 } 1/5 の確率 1秒に何回発生?
ときどき発生させる void Update() { if (Random.Range(0f, 1f) < 0.2f) { 処理 } 0~1 の乱数が0.2未満 } 1/5 の確率 1秒に何回発生? 60fpsなら12回 30fpsなら6回 わかりにくい
ときどき発生させる(改) void Update() { if (Random.Range(0f, 1f) < 6f*Time.deltaTime) { 処理 } } Δt を掛けて 秒あたりの期待値に 1秒に6回発生(期待値)
回転バネと強めのダンパーを設置
回転バネと強めのダンパーを設置 ときどきトルクを加える
ガタガタ道を進むクルマ 完成
ふわふわ飛ぶUFO どう実現しよう?
位置バネと弱めのダンパーを設置
位置バネと弱めのダンパーを設置 おっとっと 目標位置を通り過ぎる
位置バネと弱めのダンパーを設置 ときどき乱数で 支点を変える 通り過ぎてしまうままならなさ ドローンを操縦しているとこんな感じ
ふわふわ飛ぶUFO 完成
デモ:てきとうに盛ってみた
おしまい
おまけ 線形合同法
復習 an = an 1 +Q 32 Qが2 と互いに素なら まんべんなく循環する Qが奇数なら まんべんなく循環する 32 周期は2
もうちょっと工夫しよう an = an 1 +Q なにか掛けてみようかな an = P an 1 +Q 線形合同法 と呼ばれる乱数生成法 例:x = x*1103515245 + 12345;
ところで線形合同法は an = P an 1 +Q 偶数と奇数が 交互に出る らしいぞ!? 調べてみよう
参考 線形合同法の一般項 an (2)の両辺にPを掛けて (1)+(3)より P an 1 +Q …(1) = P an 2 +Q …(2) + PQ …(3) + (P + 1)Q …(4) 1 = P 2 an an = P 2 an 同様に次の項を調査 2 = P an +Q …(5) + P 2Q …(6) 3 2 = P 3 an an = P 3 an 3 + (P 2 + P + 1)Q (P n 1 2 an 2 2 an (5)の両辺にP^2を掛けて P (4)+(6)より 繰り返すことで次式を得る(検証略) n an = P a0 + 1 an = P an + Pn 2 3 + · · · + P + 1)Q an = P n a0 + Q n X1 k=0 n P an = P a0 + Q P n Pk 1 1 …(7)
an = P an n an = P a0 + (P n 1 +P 1 +Q n 2 + · · · + P + 1)Q
P:奇数 Q:奇数 a0:偶数 n an = P a0 + (P 偶数 n 1 奇数 +P n 2 奇数 + · · · + P + 1)Q 奇数 奇数
P:奇数 Q:奇数 a0:偶数 n an = P a0 + (P 偶数 n 1 奇数 +P n 2 奇数 + · · · + P + 1)Q 奇数 nが奇数(奇数回目)→anも奇数 nが偶数(偶数回目)→anも偶数 奇数
P:奇数 Q:奇数 a0:偶数 n an = P a0 + (P 偶数 確かに 偶数と奇数が 交互に出ますね n 1 奇数 +P n 2 奇数 + · · · + P + 1)Q 奇数 nが奇数(奇数回目)→anも奇数 nが偶数(偶数回目)→anも偶数 奇数
おまけ ボツスライド集
参考 max は含むの?含まないの? int Range(int min, int max) float Range(float min, float max) 間違えやすい int Range(int inclusive_min, int exclusive_max) float Range(float inclusive_min, float inclusive_max) 含む・含まないを明記
正規分布乱数の作り方 〜その2〜 float getND() { float v = 0f; for (var i = 0; i < 12; ++i) { v += Random.value; 0~1 の乱数 } return v-6f; 中心をゼロに } 12回足すだけ!!
12回でなくても 2回 8回 4回 12回 正規分布っぽい
おしまい