Transform
Unity の回転と向き

ランダムなゲームプレイ要素の追加

ランダムに選ばれたアイテムや値の多くはゲームで重要になります。本項では Unity の内蔵 random 関数を使用してよくあるゲームシステムを実装する方法を取り上げます。

配列からランダムなアイテムを選択する

ランダムな配列要素を選ぶことはランダムな整数を 0 から配列の最大のインデックス値(配列の長さから 1 を引いた値)を選ぶことに要約できる。これは内蔵の Random.Range 関数により実現できます。

 var element = myArray[Random.Range(0, myArray.Length)];

異なる発生確率をもったアイテムを選択する

アイテムをランダムに選択しなければいけないが、あるアイテムは他よりも可能性が高い場合があります。例えば、NPC キャラクターはプレイヤーに遭遇したときにいくつかの反応を示す可能性があります:

  • 50% の確率で親しみやすい挨拶
  • 25% 確率で逃走
  • 20% 確率で急な攻撃
  • 5% 確率でお金をプレゼント

これらの異なる結果を分割した紙切れで表現することができて、おのおのの切れ端は紙切れの全長の一部を占めるとします。その占める長さが選択される結果の確率に相当します。選択を行うことは紙切れの長さ上の任意の点を選んで(例えばダーツを投げるように)、どの切れ端にあたるか確認することに相当します

スクリプトの中では紙切れは実際には float の配列で、おのおののアイテムに異なる確率を順に格納しています。ランダムな点は Random.value をすべての float の合計に掛け算することで得られます(足して 1 になる必要はなく、重要なことは異なる値の相対的な長さ)。点がどの配列要素に “入っているか” 調べるために、点の値が最初の要素の値より小さいか比較します。それであれば選択されるのは最初の要素です。そうでなければ、最初の要素の値を点の値から引き算して、それを二つめの要素の値より小さいか比較する、などを繰り返して最終的に正しい要素を見つけます。コードでは次のようになるはずです。


//JS 

function Choose(probs: float[]) {
    var total = 0;
    
    for (elem in probs) {
        total += elem;
    }
    
    var randomPoint = Random.value * total;
    
    for (i = 0; i < probs.Length; i++) {
        if (randomPoint < probs[i])
            return i;
        else
            randomPoint -= probs[i];
    }
    
    return probs.Length - 1;
}

//C#

    float Choose (float[] probs) {

        float total = 0;

        foreach (float elem in probs) {
            total += elem;
        }

        float randomPoint = Random.value * total;

        for (int i= 0; i < probs.Length; i++) {
            if (randomPoint < probs[i]) {
                return i;
            }
            else {
                randomPoint -= probs[i];
            }
        }
        return probs.Length - 1;
    }

Random.value は 1 の値を返す可能性があるため、最後の return ステートメントが必要であるということに注意してください。このケースでは、検索はランダムな点をどこにも見つけることができません。次の行を変更して、

 if (randomPoint < probs[i])

連続確立変数への重み付け

個別の結果を持っている場合、float 配列の手法は上手く動作しますが、より持続的な結果を生成したい状況もあります。すなわち、宝箱で見つかった金貨の枚数をランダム化したいときや 1~100 の間で値が変わるランダム値でもより低い値を取得する可能性が高いことを望む場合です。これを行うには float 配列の手法を使用すると、扱いにくい 100 float の配列を構成することが求められます(例えば、短冊に関するセクション)、また、すべての数 (whole numbers) に限られず範囲内の任意の値が欲しい場合にもそのアプローチを使うことは不可能です。

連続した結果に対するよりよいアプローチとして、ランダムな値 ‘raw’ を ‘weighted’ なものに変換するのに AnimationCurve を使用することです。違うカーブの形状を描画することで、異なる重み付けを生成することができます。コードも書くのはもっと簡単です。

//JS

function CurveWeightedRandom(curve: AnimationCurve) {
    return curve.Evaluate(Random.value);
}

//C#

float CurveWeightedRandom(AnimationCurve curve) {
    return curve.Evaluate(Random.value);
}

0 と 1 の間のランダムな値 raw は、Random.value から読み込むことで選択されます。次に、水平座標としてそれを処理する curve.Evaluate() に渡され、水平位置でカーブに対応する垂直座標を返します。カーブの浅い部分は選択される大きなチャンスがある一方、急な部分は選択されるチャンスが少ないです。

線形(直線)カーブはまったく値の重み付けを行いません。水平l 座標はカーブ上の各ポイントの垂直座標に対して等しいです。
線形(直線)カーブはまったく値の重み付けを行いません。水平l 座標はカーブ上の各ポイントの垂直座標に対して等しいです。
このカーブでは始まりのとき浅く、終わりになると急カーブになっているので、低い値で大きなチャンス、高い値で小さなチャンスとなります。0 と 0.25 の間の値を得る50%の可能性があることを示す X=0.5 (幅)で約 0.25(高さ)の行のカーブの高さを見ることができます。
このカーブでは始まりのとき浅く、終わりになると急カーブになっているので、低い値で大きなチャンス、高い値で小さなチャンスとなります。0 と 0.25 の間の値を得る50%の可能性があることを示す X=0.5 (幅)で約 0.25(高さ)の行のカーブの高さを見ることができます。
このカーブは始まりと終わりの両方が浅く、両極端に近い値をより一般的な値にしてレア値になる真ん中で急なカーブになります。また、このカーブで、高さの値が上部にシフトアップされたことに注意してください。前回のカーブのように 0~1 の値に代わってカーブで生成された値が 1~10 の範囲内でカーブの下部は1、カーブの最上部は10になります。
このカーブは始まりと終わりの両方が浅く、両極端に近い値をより一般的な値にしてレア値になる真ん中で急なカーブになります。また、このカーブで、高さの値が上部にシフトアップされたことに注意してください。前回のカーブのように 0~1 の値に代わってカーブで生成された値が 1~10 の範囲内でカーブの下部は1、カーブの最上部は10になります。

これらのカーブは、確率論のガイドで見つかるかもしれないような確立分布曲線ではなく、もっと逆累積分布曲線のようであると気付いてください。

複数あるスクリプトのうち1つにパブリックの Animation Curve 変数を定義することにより、値を計算する必要性に代わって、視覚的に Inspector ウィンドウを通してカーブを見て編集することができます。

このテクニックは浮動小数点を生成します。整数の結果を計算したい場合、例えば、82.1214 金貨ではなく82 金貨を望むときは Mathf.RoundToInt() のような関数に対して単に計算された値を渡すことができます。

リストをシャッフルする

よくあるゲームシステムに、知られたアイテムのセットからランダムな順番で選択されるというものがあります。例えば、カードの山は一般的にシャッフルして予想できる順番とならないようにします。アイテムを配列でシャッフルするためには、各アイテムを参照して、他のランダムな配列インデックスの要素と入れ替える必要があります。

//JS

function Shuffle(deck: int[]) {
    for (i = 0; i < deck.Length; i++) {
        var temp = deck[i];
        var randomIndex = Random.Range(0, deck.Length);
        deck[i] = deck[randomIndex];
        deck[randomIndex] = temp;
    }
}

//C#

    void Shuffle (int[] deck) {
        for (int i = 0; i < deck.Length; i++) {
            int temp = deck[i];
            int randomIndex = Random.Range(0, deck.Length);
            deck[i] = deck[randomIndex];
            deck[randomIndex] = temp;
        }
    }

繰り返しなしでアイテムのセットから選択する

良くある作業として、アイテムをセットからランダムに選択して、繰り返し同じものを一回以上選択しないようにするということがあります。例えば、一定数の NPC キャラクターをランダムな位置で生成したけれども、ひとつの NPC だけがある位置で生成されるようにしたい場合などです。これは順番にアイテムを反復して、各アイテムについて選択したセットに追加するかどうかランダムな判断を行います。各アイテムを参照するたび、それが選択される確率は、必要なアイテムの数をまだ選べる数で割ったものです。

サンプルとして、10 個の生成点が利用可能であるけれども、5 個のみから最初のアイテムが選択される確率は 5 / 10 または 0.5 です。もし、それが選ばれた場合、二つめのアイテムが選ばれる確立は 4 / 9 または 0.44 (すなわち、4 個がまだ必要で、9 個からまだ選べます)。これを、セットが必要な 5 個のアイテムを含むまで繰り返します。次のようなコードによりこれを実現できます:


//JS


var spawnPoints: Transform[];

function ChooseSet(numRequired: int) {
    var result = new Transform[numRequired];
    
    var numToChoose = numRequired;
    
    for (numLeft = spawnPoints.Length; numLeft > 0; numLeft--) {
        // Adding 0.0 is simply to cast the integers to float for the division.
        var prob = (numToChoose + 0.0) / (numLeft + 0.0);
        
        if (Random.value <= prob) {
            numToChoose--;
            result[numToChoose] = spawnPoints[numLeft - 1];
            
            if (numToChoose == 0)
                break;
        }
    }
    
    return result;
}


//C#

    Transform[] spawnPoints;

    Transform[] ChooseSet (int numRequired) {
        Transform[] result = new Transform[numRequired];

        int numToChoose = numRequired;

        for (int numLeft = spawnPoints.Length; numLeft > 0; numLeft--) {

            float prob = (float)numToChoose/(float)numLeft;

            if (Random.value <= prob) {
                numToChoose--;
                result[numToChoose] = spawnPoints[numLeft - 1];

                if (numToChoose == 0) {
                    break;
                }
            }
        }
        return result;
    }

選択がランダムであるにも関わらず、選択されたセットのアイテムは最初の配列と同じ順序であることに注意してください。もしアイテムが順番に一回づつ使用されるとしたら、順番によって予想可能となってしまうため、配列を使用する前にシャッフルする必要があるかもしれません。

空間におけるランダムな点

立方体の体積におけるランダムな点は、Vector3 の各々のコンポーネントを Random.value により返される値でセットすることで選択できます :

 var randVec = Vector3(Random.value, Random.value, Random.value);

体積が球である場合(すなわち、原点から一定の半径に含まれるランダムな点が必要な場合)、Random.insideUnitSphere を希望の半径に掛け算できます:

 var randWithinRadius = Random.insideUnitSphere * radius;



 var randWithinCircle = Random.insideUnitCircle * radius;

… 値を <= 記号に変更することで余分な return ステートメントを回避できますが、確率が 0 の場合でもアイテムを選択することも許容してしまいます。 これにより辺の長さが 1 単位である立方体の中の点が返されます。立方体を拡大/縮小するにははベクトルの X、Y、Z 要素を希望する辺の長さに掛け算するのみです。もし軸のひとつが 0 にセットされる点はひとつの平面上に置かれます。例えば、ランダムな地点を “地面” から選ぶ必要がある場合は、X、Z をランダムに選択して、Y は 0 にセットすることになります。 もし結果のベクトルの要素のひとつを 0 にしても、円の中のランダムな点を正しく得ることには「できません」。点は確かにランダムで、半径は希望する範囲内にありますが、確率が円周に近づくにつれ大きく偏りができていて、点は不均等に並びます。その状況では変わりに Random.insideUnitCircle を使用すべきです: Random.Range が最初の引数の含むけれども二つめの引数は除かれるため、myArray.Length とすることが正しいことに注意してください。

Transform
Unity の回転と向き