Приветствую всех друзья! В прошлой статье мы обзорно рассмотрели источники света и камеры в Unity. Сегодня мы поговорим о способах добавления случайных геймплейных элементов в Unity. В этой статье совсем без кода никак, поэтому я буду приводить примеры кода на языке C Sharp (к сожалению символ решетки ставить тут не получается).
Думаю, многие из нас играя в игры замечали насколько важны случайные элементы в них. Это может быть случайный лут выпавший из босса в ММОРПГ, или же случайные уровни в рогаликах. Ниже я расскажу, как с помощью встроенных функций в Unity можно сделать подобную вариативность геймплея.
Выбор случайного элемента из массива.
Вызов случайного элемента из массива сводится к выбору случайного целого числа между 0 и максимальным значением индекса массива (который равен длине массива минус 1). Это легко сделать с помощью, встроенной в Unity функции рандома Random.Range():
var element = myArray[Random.Range(0, myArray.Length)];
Следует знать особенность работы данной функции с целыми числами – она уже построена таким образом, чтобы брать начальный элемент (в коде это 0) и конечный минус 1. То есть если длина нашего массива равна двум, то функция вернет либо 0, либо 1.
Таким способом можно сделать самый простой рандом (случайность), например, для появления различных препятствий в простеньком раннере.
Выбор предметов с различной вероятностью.
Иногда нам нужно выбирать предметы наугад, но с определенной вероятностью. Например, когда мы делаем босса для рейда, нам необходимо чтобы с 70% вероятностью с него выпадали редкие вещи, с 20% вероятностью легендарные и только с 10% реликвии. Именно в этот момент нам и пригодятся вероятности.
Мы можем представить себе вероятность как полосу, поделенную на секции, где секции занимают площадь в процентном соотношении от этой полосы. А выбор вероятности эквивалентен выбору случайной точки на этой полосе – как если бы мы метнули дротик и случайно куда-то попали.
В скрипте такая полоса представляет собой массив чисел с плавающей точкой (float) который содержит в себе различные вероятности для предметов по порядку. Чтобы найти случайную точку (метнуть дротик из примера выше), необходимо перемножить Random.value на сумму всех вероятностей из массива чисел с плавающей точкой. Нужно помнить, что сумма всех вероятностей не должна превышать единицу (так как она является эквивалентом 100%). Далее в цикле мы сравниваем результат случайной точки с вероятностью из массива, так вот если случайное число меньше вероятности из массива – мы возвращаем индекс элемента массива, в котором находится вероятность, иначе мы вычитаем из случайной точки значение только что проверенной вероятности из массива и повторяем процесс.
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.
Взвешивание непрерывных случайных величин.
Метод массива с вероятностями описанный выше хорошо работает с дискретными результатами, но есть также ситуации, когда вы хотите получить более непрерывный результат – скажем вы хотите сделать чтобы из сундука, выпадало от 1 до 100 монет, и при этом, чтобы более низкое значение монет было более вероятным для выпадения. Чтобы сделать такое методом вероятностей, описанным выше, вам потребуется настроить массив с 100 значениями, что является громоздким и неоправданно долгим.
Лучшим подходом для получения непрерывных результатов является использование AnimationCurve (Кривая анимации) для преобразования «сырой» случайной величины в «взвешенную». Рисуя различные формы кривых, вы можете получить различные веса. Код при этом методе также проще:
float CurveWeightedRandom(AnimationCurve curve)
{
return curve.Evaluate(Random.value);
}
«Сырое» случайное значение выбирается в диапазоне от 0 до 1 выбирается путем считывания Random.value. Затем это случайное число передается методу curve.Evaluate() – этот метод рассматривает это число как координату по горизонтали на этой кривой и возвращает ее вертикальное значение. Мелкие участки этой кривой имеют большую вероятность быть выбранными по сравнению с крупными.
Объявив переменную типа AnimationCurve как публичную, или же приватную, но с атрибутом SerializeField – можно визуально (как на картинке выше) настраивать кривые анимаций в инспекторе.
Этот метод дает вам числа с плавающей запятой, если вам нужно получить целочисленные значения (как в примере выше про монеты), можно воспользоваться методом округления Mathf.RoundToInt().
Метод перестановки в листе.
Одной из механик игры, которую вам возможно предстоит реализовать – это перетасовка карт. Это можно сделать с помощью перебора массива и случайной замены предметов внутри него, вот код позволяющий это сделать:
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. Это можно сделать путем последовательного перебора элементов, принимая случайное решение для каждого из них относительно того, будет ли он добавлен в набор выбранных элементов. При посещении каждого элемента массива, вероятность того что он будет выбран, равна количеству все еще необходимых элементов, деленному на число оставшихся для выбора. Вот пример кода:
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;
}
Стоит понимать, что хотя выбор и является случайным, сама же последовательность в итоговом массиве будет повторять последовательность исходного массива. Поэтому если необходимо, используйте перетасовку массива о которой я написал выше.
Случайная точка в пространстве.
Чтобы получить случайную точку в кубическом пространстве используйте Random.value для создания Vector3:
var randVec = Vector3(Random.value, Random.value, Random.value);
Если вам необходимо масштабировать куб – просто умножьте все его стороны на масштабирующий коэффициент. Также если есть необходимость найти случайную точку, например, на земле – можно обнулить координату по Y, а X и Z сделать случайными.
Если вы хотите найти случайную точку внутри сферы (то есть получить случайную точку в заданном радиусе от начала координат), можно воспользоваться Random.insideUnitSphere помноженным на радиус:
var randWithinRadius = Random.insideUnitSphere * radius;
Стоит отметить, что если вы обнулите одну из составляющих результирующего вектора, вы не получите правильную случайную точку внутри круга. Хотя вероятность действительно верна и лежит в радиусе круга, она будет смещена ближе к краю круга, из-за чего случайность будет крайне неравномерной. Чтобы избежать этого используйте Random.insideUnitCircle вместо Random.insideUnitSphere:
var randWithinCircle = Random.insideUnitCircle * radius;
На сегодня это все. В следующей статье мы будем говорить о кроссплатформенности Unity, а именно о том какие части игры могут оставаться неизменными для всех платформ, а какие придется переписывать, а также затронем типичные проблемы и их решения. Спасибо всем, кто дочитал эту статью до конца, подписывайтесь на канал, ставьте лайки, а для тех, у кого появились вопросы - спрашивайте в комментариях! А если вы хотите помочь данному каналу в развитии – делитесь этой статьей с друзьями в социальных сетях!