Перестаньте использовать 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 и для всего проекта автоматически.

Решение:

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

Ссылка на github

Предыстория

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

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

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

UnityEngine.Random.InitState(seed);

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

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

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

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

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

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

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

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

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

Рубрика: "А что если"

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

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

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

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

Решение

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

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

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

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

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

Ссылка на исходный код

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

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

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

Ссылка на скрипт RandomSeed

Тестирование производительности

Код примитивного бенчмарка

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

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

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

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

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

UnityEngine.Random (milliseconds)

System.Random (milliseconds)

FastRandom (milliseconds)

25/4096x4096

472

611

223

1000/1024x1024

28

43

15

Тестировалось на ноутбуке ASUS Zephyrus G15 в редакторе. Под разные платформы не компилировалось.

Как установить

Если версия Unity выше чем 2019.3 в файл Packages/manifest.json добавить:

"fastrandom": "https://github.com/vangogih/Dont-Use-UnityEngine.Random.git",

Или просто скачать .unitypackage из раздел Releases

Как дополнить

Предлагайте улучшение через Issue или через Pull Request.

Источник: https://habr.com/ru/post/531042/


Интересные статьи

Интересные статьи

Сегодня я хочу рассказать вам о таком приложении как ISH. ISH - это проект с открытым исходным кодом, позволяющее в виде приложения эмулировать полноценный линукс на ваше...
В этой статье мы рассмотрим, как система управления 1С-Битрикс справляется с большими нагрузками. Данный вопрос особенно актуален сегодня, когда электронная торговля начинает конкурировать по обороту ...
Прим. перев.: Новая статья с критикой полюбившейся многим Git Flow получила столь заметное внимание, что даже оригинальный автор модели обновил публикацию 10-летней давности, актуализировав свой ...
Привет, Хабр! Представляю вашему вниманию перевод статьи «On let vs const» автора Дэна Абрамова. Мой предыдущий пост содержит такой параграф: let vs const vs var: Обычно, все что вам нужно,...
Обезьяна (шимпанзе) достает термитов из термитника при помощи палки. Навторой фотографии горилла использует палку для сбора нужной ей травы Разработчики из США создали специализированный ал...