Version: 2019.3
Камеры
Cross-Platform Considerations

Добавление случайных элементов в игру

Случайно выбираемые значения или предметы являются важной частью множества игр. В этих разделах показаны способы использования встроенного в Unity функционала генерации случайных значений для реализации некоторых основных игровых механик.

Выбор случайного элемента в массиве

Выбор случайного элемента массива водится к выбору случайного значения в диапазоне от нуля до максимального значения индекса в массиве (который на 1 меньше длины массива). Это сделать довольно просто, используя встроенный метод Random.Range:-

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

Учтите, что диапазон, из которого метод Random.Range возвращает значение, включает первый аргумент, но не включает второй аргумент. Так что, если в качестве второго аргумента передавать 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 и тогда поиск никогда не найдёт случайно выбранную точку. Изменение строки

 if (randomPoint < probs[i])

.. на проверку меньше-или-равно (<=) позволит избежать дополнительного оператора возврата, но с другой стороны, позволит случайно выбирать элемент, даже если вероятность его выбора равна нулю.

Weighting continuous random values

The array of floats method works well if you have discrete outcomes, but there are also situations where you want to produce a more continuous result - say, you want to randomize the number of gold pieces found in a treasure chest, and you want it to be possible to end up with any number between 1 and 100, but to make lower numbers more likely. Using the array-of-floats method to do this would require that you set up an array of 100 floats (i.e. sections on the paper strip) which is unwieldy; and if you aren’t limited to whole numbers but instead want any number in the range, it’s impossible to use that approach.

A better approach for continuous results is to use an AnimationCurve to transform a ‘raw’ random value into a ‘weighted’ one; by drawing different curve shapes, you can produce different weightings. The code is also simpler to write:

//JS

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

//C#

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

A ‘raw’ random value between 0 and 1 is chosen by reading from Random.value. It is then passed to curve.Evaluate(), which treats it as a horizontal coordinate, and returns the corresponding vertical coordinate of the curve at that horizontal position. Shallow parts of the curve have a greater chance of being picked, while steeper parts have a lower chance of being picked.

A linear curve does not weight values at all; the horizontal coordinate is equal to the vertical coordinate for each point on the curve.
A linear curve does not weight values at all; the horizontal coordinate is equal to the vertical coordinate for each point on the curve.
This curve is shallower at the beginning, and then steeper at the end, so it has a greater chance of low values and a reduced chance of high values. You can see that the height of the curve on the line where x=0.5 is at about 0.25, which means theres a 50% chance of getting a value between 0 and 0.25.
This curve is shallower at the beginning, and then steeper at the end, so it has a greater chance of low values and a reduced chance of high values. You can see that the height of the curve on the line where x=0.5 is at about 0.25, which means there’s a 50% chance of getting a value between 0 and 0.25.
This curve is shallow at both the beginning and the end, making values close to the extremes more common, and steep in the middle which will make those values rare. Notice also that with this curve, the height values have been shifted up: the bottom of the curve is at 1, and the top of the curve is at 10, which means the values produced by the curve will be in the 1-10 range, instead of 0-1 like the previous curves.
This curve is shallow at both the beginning and the end, making values close to the extremes more common, and steep in the middle which will make those values rare. Notice also that with this curve, the height values have been shifted up: the bottom of the curve is at 1, and the top of the curve is at 10, which means the values produced by the curve will be in the 1–10 range, instead of 0–1 like the previous curves.

Notice that these curves are not probability distribution curves like you might find in a guide to probability theory, but are more like inverse cumulative probability curves.

By defining a public AnimationCurve variable on one of your scripts, you will be able to see and edit the curve through the Inspector window visually, instead of needing to calculate values.

This technique produces floating-point numbers. If you want to calculate an integer result - for example, you want 82 gold pieces, rather than 82.1214 gold pieces - you can just pass the calculated value to a function like 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 генерировался в каждой из точек. Это можно реализовать с помощью перебора последовательности элементов, решая для каждого случайным образом - быть ему добавленным в выбранный набор или нет. После “посещения” каждого элемента, вероятность того, что он будет выбран равна числу ещё требующихся элементов, разделённому на число оставшихся для выбора элементов.

В качестве примера, представьте, что существует десять точек генерации, но выбрать можно только пять. Вероятность выбора первого элемента будет равна 5 / 10 или 0.5. Если он выбран, то вероятность выбора второго элемента - 4 / 9, или 0.44 (то есть требуется ещё 4 элемента и ещё 9 доступно для выбора). Однако, если первый элемент не был выбран, то вероятность выбора второго элемента - 5 / 9, или 0.56 (то есть ещё требуется выбрать 5 элементов и ещё 9 доступно для выбора). Это продолжается до тех пор, пока набор не будет состоять из требуемых пяти элементов. Вы можете реализовать это в коде таким образом:-


//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);

Это даст вам точку в кубе с ребром длиной в одну условную единицу. Куб можно масштабировать просто умножая X, Y и Z компоненты вектора на требуемые длины сторон. Если одна из осей имеет нулевое значение, точка всегда будет лежать на плоскости. Например, получение случайно точки “на земле” обычно достигается с помощью получения случайных компонент X и Z с установкой Y компоненты в ноль.

Если объёмом является сфера (т.е., когда вы желаете разместить случайную точку в указанном радиусе от начала отсчёта), вы можете использовать значение Random.insideUnitSphere, умноженное на нужный радиус :-

 var randWithinRadius = Random.insideUnitSphere * radius;

Учтите, если вы установите одну из компонент получившегося вектора в ноль, вы не получите правильную случайную точку в круге. Хоть точка и действительно случайна, и расположена в области правильного радиуса, точка с большей вероятностью окажется ближе к краю окружности, так что точки будут распределены очень неравномерно. Для этой задачи следует использовать Random.insideUnitCircle:-

 var randWithinCircle = Random.insideUnitCircle * radius;
Камеры
Cross-Platform Considerations