Перейти к содержимому

Как сделать рандом в юнити

  • автор:

Random — Выбрать случайное число из набора | Unity3d

Но не знаю, как сделать рандом из определённых чисел. Например, из чисел: 1, 5, 8, 14. Подскажите, пожалуйста, как это сделать.

Отслеживать
3,067 2 2 золотых знака 10 10 серебряных знаков 28 28 бронзовых знаков
задан 7 авг 2021 в 10:16
95 9 9 бронзовых знаков

1 ответ 1

Сортировка: Сброс на вариант по умолчанию

Как я понял, вам нужно выбрать случайно число из массива чисел. Это можно сделать так:

var array = new int[] < 1, 5, 8, 14 >; var randomElement = array[Random.Range(0, array.Length)]; 

Отслеживать
ответ дан 7 авг 2021 в 10:19
iikuzmychov iikuzmychov
3,067 2 2 золотых знака 10 10 серебряных знаков 28 28 бронзовых знаков
Гениально! Спасибо!
7 авг 2021 в 10:42

  • c#
  • unity3d
    Важное на Мете
Связанные
Похожие

Подписаться на ленту

Лента вопроса

Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.

Дизайн сайта / логотип © 2024 Stack Exchange Inc; пользовательские материалы лицензированы в соответствии с CC BY-SA . rev 2024.1.3.2953

Нажимая «Принять все файлы cookie» вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.

Перестаньте использовать UnityEngine.Random

Как часто вы используете конструкцию Random.value или Random.Range(float min, float max) ? А как много эту конструкцию использовали разработчики фреймворков или плагинов, которые вы встроили в проект?

В данной статье мы обсудим как сомнительное архитектурное решение от Unity может одной строкой изменить все случайное поведение, сделав его не случайным.

Выжимка для тех, кто спешит

UnityEngine.Random – static класс, который использует реализацию ГПСЧ написанную разработчиком движка Unity на C++.

Если в проекте мы захотим задавать seed, то этот seed применится ко всему объекту Random. В итоге, когда мы ожидаем «случайного» числа или последовательности чисел, они будут не случайны.

Все дело в том, что класс статический, поля и методы статичны. Если мы единожды зададим seed через Random.InitValue(int seed), то его значение будет справедливо для всего класса Random и для всего проекта автоматически.

Каждый раз, когда нужно получить число с использованием ГПСЧ, нужно создавать новый экземпляр класса, предоставляющим функционал ГПСЧ .

Предыстория

Все началось с того, что я заметил, как в игре, в которой уровни генерировались процедурно, противники начали вести себя одинаково каждый раз при восстановлении старого уровня (вышел из игры, при повторном заходе, тебе предложили продолжить). Когда уровень начинался заново у противников была одна и та же комбинация поведений: сначала налево, потом стреляют, потом от игрока и опять стреляют.

После долгого исследования плагина и кода поведения мобов я нашел проблемное место. Это была одна безобидная строчка в проекте с больше чем 100 сборками и больше чем 60000 строк кода.

Эта строка выглядела так:

По факту эта строка просто позволяла загрузить ранее сгенерированный уровень. Но неявно, так же, она проставляла один единственный seed для всего класса Random

Как такое произошло?

Все дело в том, что UnityEngine.Random является классом, который целиком и полностью состоит из статических методов и полей. Замечательное свойство статических членов (полей, свойств, методов, классов) в том, что мы можем получить к ним доступ без создания экземпляра данного класса. В Unity статические члены инициализируются самыми первыми (как и в .NET в целом). Под них выделяется отдельное место в памяти, в котором они хранятся с момента создания приложения до момента пока приложение не будет выгружено из памяти (закрыто).

Что все это значит?

Это значит, что единожды проставив значение seed’a в класс UnityEngine.Random , этот seed будет использоваться в этом классе до момента, пока мы не закроем приложение или пока не изменим его.

Попробую описать на реальном примере:

  • Вы написали генератор лабиринта, конфигурацию которого можно повторить если задать seed,
  • Вы используете механику стрельбы и разброса пуль. В итоге, все пули будут разлетаться в четком порядке для заданного seed’a,
  • Вы понимаете, что и генератор лабиринта, и механика разброса используют UnityEngine.Random.

Почему это очень плохо?

Как минимум — игроки заметят и в дело вступит эффект отмены.

Как максимум, придется изобретать костыль, чтобы вернуть seed в предыдущее состояние после вызова метода InitState() . Если вернуть значение нельзя, потому что «случайные» значения постоянно используются, то придется перед каждым использование Random принудительно проставлять seed.

var prevSeed = UnityEngine.Random.seed; // с учетом того что в 2019 Random.seed obsolete. UnityEngine.Random.InitState(seed: DateTime.UtcNow.GetHashCode()); var value = UnityEngine.Random.value; UnityEngine.Random.InitState(prevSeed);

Это замечательный пример того, как неправильное использование статических классов. В результате мы получим:

  1. Жесткую связность. Неявная завязка на реализацию одного конкретного класса. Он не передается через конструктор, не инжектится через свойство, а мы просто пишем Random.value
  2. Каскадное изменение поведения там, где это не нужно, но где используется статическая зависимость.

OrderBy в пространстве имен LINQ постоянно бы мешал коллекции, используя данные из предыдущего обращения к данному методу?

Но ведь Unity заботится об объеме используемой ОЗУ

Нет, не заботится. Это пример плохого архитектурного решения, когда решили использовать ГПСЧ внутри реализации самого движка и одновременно предоставить доступ из C#. Объект загрузится и на протяжении жизни приложения будет всего один экземпляр в памяти. Это параллельно привносит новое неочевидное поведение и обязывает нас использовать один экземпляр Random на весь проект.

Невозможно заранее предсказать какое именно поведение потребуется пользователям. Но дать им право выбора надо.

Вот еще список потенциальных проблем, с которыми можно столкнуться:

  • Если внутри движка используется UnityEngine.Random то, мы получим неожиданное поведение компонентов, предоставляемые Unity,
  • Скомпилированный в *.dll плагин устанавливает seed. И каждый раз перед вызовом придется проставлять seed. Решить это можно только через декомпиляцию (если финальная библиотека не обфусцирована).

Решение

Использовать System.Random

Пункт сразу отпадает, так как:

  • Все реализации полезных методов, которые предоставляет UnityEngine.Random , придется писать самому.
  • System.Random аллоцирует 280 байт (248 на массив из 56 элементов, 12 байт на класс и 20 байт на переменные внутри класса). 5 экземпляров — 1кб памяти.
  • По производительности System.Random чуть хуже чем UnityEngine.Random .

Собственная реализация

Лучшим решением будет сделать что-то простое, что занимает мало памяти и очень быстро работает.

В результате всех исследований и проведенных опытов, самым простым и быстрым вариантом стал — конгруэнтный мультипликативный алгоритм с модулем от числа 2^31.

Я не стал помещать класс в пространство имен, потому что считаю что то, что ниже — проблема архитектора, а не моя.

using Random = UnityEngine.Random; // или using Random = System.Random;

Генерацию seedá я сделал в нескольких вариациях:

  • Time — использует структуру System.DateTime и предоставляет достаточно надежное значение seedá, но аллоцирует 208 байт. Потому что DateTime (сюрприз) неуправляемая структура.
  • Guid — использует структуру System.Guid и System.Environment.TicksCount . Guid в отличии от System.DateTime работает медленнее, но аллоцирует всего 16 байт и является управляемой структурой (т.е. размещается на стеке и не копируется в кучу).
  • Crypto — использует System.Security.Cryptography.RandomNumberGenerator и аллоцирует 4 байта. По скорости сравним с Guid, но данный метод не тестировался на разных платформах. Так же, пара простых строчек, может прибавить к размеру билда, если пространство имен System.Security.Cryptography раньше не входило в ваш билд.
Тестирование производительности

Кол-во аллокаций неуправляемых объектов проверялся таким образом:

var start = GC.GetTotalMemory(true); var rnd = new System.Random(); var stop = GC.GetTotalMemory(true); Console.WriteLine(stop - start); // 280 для Random - соответственно

Значения в таблице — среднее время затраченное на заполнение двумерного массива размерностью NxN:

Внизу:
Кол-во итераций/размер двумерного массива

Справа: Тип используемого генератора

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

Случайно выбираемые значения или предметы являются важной частью множества игр. В этих разделах показаны способы использования встроенного в 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)

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. 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 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 > 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 > > 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; 

Как сделать рандом в юнити

  • Главная
  • C#
    • Базовый курс C#
      • Глава 1. Самое начало
      • Глава 2. Основы ООП
      • Глава 3. WinFroms
      • Начало
      • Окружение
      • Структура программы
      • Основы синтаксиса
      • Типы данных
      • Преобразование типов данных
      • C#
      • Алгоритмы
      • Всякая всячина…
      • Обучающий материал
      • Практикум
      • Технические новости сайта

      Генерация случайных чисел в C#

      Доброго времени суток! Эта статья носит практический характер и в ней я расскажу о том, как использовать генератор случайных чисел в C# и для чего это Вам может пригодиться. Хотя, если на чистоту, то речь пойдет о генераторе псевдослучайных чисел…

      Где нам могут понадобиться случайные числа, да на самом деле можно привести в пример много случаев, например, криптография, или механизмы разрешения различных коллизий. Поверьте, рано или поздно Вам с такой необходимостью придется столкнуться, если уже не пришлось, раз читаете эту статью. К счастью, в C# генератор случайных чисел разработан до нас, и единственное что нам нужно будет, большинстве случаев, это просто правильно им пользоваться. И так, для генерации случайных чисел в программах, написанных на C#, предназначен класс «Random».

      //Создание объекта для генерации чисел Random rnd = new Random(); //Получить очередное (в данном случае - первое) случайное число int value = rnd.Next(); //Вывод полученного числа в консоль Console.WriteLine(value);

      Как видите — ничего сложного! Сначала создаем объект типа «Random», потом вызываем его метод «Next», в результате чего и получаем случайное число. Если мы вызовем метод «Next» несколько раз, то получим разные числа. Такой код, даст нам четыре разных, случайных числа:

      //Создание объекта для генерации чисел Random rnd = new Random(); int value = rnd.Next(); //Получить очередное случайное число int value1 = rnd.Next(); //Получить очередное случайное число int value2 = rnd.Next(); //Получить очередное случайное число int value3 = rnd.Next(); //Получить очередное случайное число Console.WriteLine(value); //Вывод числа в консоль Console.WriteLine(value1); //Вывод числа в консоль Console.WriteLine(value2); //Вывод числа в консоль Console.WriteLine(value3); //Вывод числа в консоль

      А теперь, давайте слегка усложним задачу, представим, что нам нужно получить случайное число в определенном диапазоне. Но и эта задача выполняется довольно просто:

      //Создание объекта для генерации чисел Random rnd = new Random(); //Получить случайное число (в диапазоне от 0 до 10) int value = rnd.Next(0, 10); //Вывод числа в консоль Console.WriteLine(value);

      Как видите, и это выполняется несложно! Для этого мы всего лишь вызвали метод «Next» с двумя параметрами, первый из которых обозначает нижнюю границу диапазона, а второй — верхнюю.

      Кстати, ограничить можно только верхнюю границу диапазона, для этого нужно вызвать метод «Next» с одним параметром, который и будет обозначать верхнюю границу.

      Но это ещё не всё, есть еще один важный нюанс, на самом деле генератор случайных чисел является генератором псевдослучайных чисел, т.е. числа, которые он возвращает, не являются чисто случайными, они «вырабатываются» по определенным и четким законам, а «случайность» зависит от инициализации объекта, который генерирует числа (объекта класса «Random»). В примерах, приведенных выше, мы использовали конструктор по умолчанию, и в таком случае (судя по всему), «начальное значение» задается системой, и на основании системного времени. Но мы может задавать это самое «начальное значение» и сами:

      //Создание объекта для генерации чисел (с указанием начального значения) Random rnd = new Random(245); //Получить случайное число int value = rnd.Next();

      Только обратите внимание, начальное значение используется для генерации чисел, а не для возвращения первого числа, т.е. первое сгенерированное число не будет тем, что указано при создании объекта.

      Так вот если два (или более) разных объекта класса «Random» будут одинаково проинициализированы, то и возвращать они будут одни и те же числа, например, следующий код, выведет в консоль три одинаковых числа:

      class Program < //Этот метод должен возвращать случайное значение static int GetRandom() < //Создание объекта для генерации чисел (с указанием начального значения) Random rnd = new Random(245); //Получить случайное число int value = rnd.Next(); //Вернуть полученное значение return value; >static void Main(string[] args) < //Вывод сгенерированных чисел в консоль Console.WriteLine(GetRandom()); Console.WriteLine(GetRandom()); Console.WriteLine(GetRandom()); >>

      Как видите, не смотря на то, что в при каждом вызове метода «GetRandom» создается новый объект для генерации случайных чисел, инициализируются эти объекты одним и тем же значением. Так что, будьте внимательны!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *