[Unity] Перестаньте использовать UnityEngine.Random
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Как часто вы используете конструкцию 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,
- Вы используете механику стрельбы и разброса пуль. В итоге, все пули будут разлетаться в четком порядке для заданного 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);
Это замечательный пример того, как неправильное использование статических классов. В результате мы получим:
- Жесткую связность. Неявная завязка на реализацию одного конкретного класса. Он не передается через конструктор, не инжектится через свойство, а мы просто пишем Random.value
- Каскадное изменение поведения там, где это не нужно, но где используется статическая зависимость.
Рубрика: "А что если"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 раньше не входило в ваш билд.
Ссылка на скрипт 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/4096x40964726112231000/1024x1024284315Тестировалось на ноутбуке ASUS Zephyrus G15 в редакторе. Под разные платформы не компилировалось. Как установитьЕсли версия Unity выше чем 2019.3 в файл Packages/manifest.json добавить:
"fastrandom": "https://github.com/vangogih/Dont-Use-UnityEngine.Random.git",
Или просто скачать .unitypackage из раздел ReleasesКак дополнитьПредлагайте улучшение через Issue или через Pull Request.
===========
Источник:
habr.com
===========
Похожие новости:
- [Программирование, Разработка игр] Реализация паттерна проектирования (перевод)
- [Поисковые технологии, Законодательство в IT] Евросоюз потребовал от интернет-компаний объяснить алгоритмы ранжирования
- [Занимательные задачки, Алгоритмы, Учебный процесс в IT] Ищем максимальную разницу между соседями. User-friendly-разбор задачи по алгоритмам
- [Алгоритмы, Хакатоны] Участники хакатона разработали алгоритм для поиска информации в газетах военных лет
- [Программирование, Разработка игр, Unity] Реактивное программирование для разработчиков игр: Введение (перевод)
- [Беспроводные технологии, Разработка систем связи, Искусственный интеллект, Транспорт] Самообучающийся ИИ начал управлять навигацией раздающих интернет аэростатов Loon
- [Программирование, Разработка игр, Unity] Представляем Owlcat Mono Profiler для Unity
- [Разработка игр, Машинное обучение, Карьера в IT-индустрии] Семь талантливых стажеров AI@Unity 2020. Часть 1 (перевод)
- [Алгоритмы, Разработка робототехники, Робототехника] МТИ показал алгоритм оптимизации форм роботов для передвижения по различным поверхностям
- [Работа с видео, Работа с 3D-графикой, Unity] Сильные стороны трассировки лучей с непрямым освещением (перевод)
Теги для поиска: #_unity, #_algoritm (алгоритм), #_generatsija_sluchajnyh_chisel (генерация случайных чисел), #_random, #_unity, #_unity3d, #_gotovoe_reshenie (готовое решение), #_unity
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:19
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Как часто вы используете конструкцию 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 будет использоваться в этом классе до момента, пока мы не закроем приложение или пока не изменим его.Попробую описать на реальном примере:
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);
using Random = UnityEngine.Random;
// или using Random = System.Random;
var start = GC.GetTotalMemory(true);
var rnd = new System.Random(); var stop = GC.GetTotalMemory(true); Console.WriteLine(stop - start); // 280 для Random - соответственно Кол-во итераций/размер двумерного массиваСправа: Тип используемого генератораUnityEngine.Random (milliseconds)System.Random (milliseconds)FastRandom (milliseconds)25/4096x40964726112231000/1024x1024284315Тестировалось на ноутбуке ASUS Zephyrus G15 в редакторе. Под разные платформы не компилировалось. Как установитьЕсли версия Unity выше чем 2019.3 в файл Packages/manifest.json добавить: "fastrandom": "https://github.com/vangogih/Dont-Use-UnityEngine.Random.git",
=========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 18:19
Часовой пояс: UTC + 5