Хабрахабр

[Перевод] Избавляем игрока от раздражения: правильное использование случайных чисел

image

Если вам доведётся пообщаться с фанатом RPG, то вскоре вы услышите жалобы на рандомизированные результаты и лут, а также о том, насколько раздражающими они могут быть. Многие геймеры высказывают свою досаду, и пока некоторые разработчики придумывают инновационные решения, большинство по-прежнему заставляет нас проходить приводящие в ярость проверки на усидчивость.

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

Генератор случайных чисел и его применение

Случайные числа встречаются нам повсюду и используются для добавления вариативности в ПО. В общем случае RNG обычно применяются для моделирования хаотических событий, демонстрации непостоянства или являются искусственным ограничителем.

Они используются в научных опытах, видеоиграх, анимациях, искусстве и практически в каждом приложении компьютера. Скорее всего, вы ежедневно взаимодействуете со случайными числами или результатами их действий. Например, RNG скорее всего реализован в простых анимациях в вашем телефоне.

Теперь, когда мы поговорили немного о RNG, давайте взглянем на их реализацию и узнаем, как применять их для улучшения игр.

Стандартный генератор случайных чисел

Почти в каждом языке программирования среди прочих функций есть стандартный RNG. Его работа заключается в возврате случайного значения в интервале двух чисел. В разных системах стандартные RNG можно реализовать десятками разных способов, но в общем случае они имеют одинаковый эффект: возвращают случайное число из интервала, каждое значение в котором может быть выбрано с одинаковой вероятностью.

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

Если вы хотите поэкспериментировать с редкостью или разными степенями рандомизации, то вам больше подойдёт следующий способ.

Взвешенные случайные числа и слоты редкости

Этот тип RNG является основой для любой RPG с системой редкости предметов. В частности, он применяется, когда вам нужен рандомизированный результат, но некоторые значения должны выпадать с меньшей частотой, чем остальные. При изучении вероятностей в пример часто приводят мешок с шариками. При взвешенном RNG в мешке может быть три синих шарика и один красный. Так как нам нужен всего один шарик, мы получим или красный, или синий, но с большей вероятностью он будет синим.

Давайте в качестве примера возьмём внутриигровые события SimCity. Почему может быть важна взвешенная рандомизация? То есть с одинаковой вероятностью вам бы предлагали открыть новое казино или происходило бы землетрясение. Если бы каждое событие выбиралось невзвешенными способами, то вероятность совершения каждого события статистически было бы одинаковым. Добавив веса, мы можем сделать так, чтобы эти события происходили в пропорциональной вероятности, обеспечивающей хороший геймплей.

Виды и применения

Группировка одинаковых предметов

Во многих книгах по информатике такой способ называется «мешком». Имя говорит само за себя — классы или объекты используются для создания визуального представления мешка в буквальном смысле.

Возвращаясь к нашему примеру с шариками, можно сказать, что наш мешок будет содержать синий, синий, синий и красный шарики. По сути, это работает так: есть контейнер, в который можно поместить объекты, функция для помещения объекта в «мешок» и функция для случайного выбора предмета из «мешка».

Если мы упростим результаты до шкалы от «Очень плохо» до «Очень хорошо», то получим гораздо более приятную систему по сравнению с той, когда игрок может получить ненужную последовательность нежелательных результатов (например, результатов «Очень плохо» 20 раз подряд). С помощью этого способа рандомизации мы можем приблизительно задавать частоту возникновения результатов, чтобы усреднить игровой процесс для каждого игрока.

Мы рассмотрим способ, который для снижения количества нежелательных результатов заходит немного дальше. Однако статистически получить серию плохих результатов всё равно возможно, просто вероятность этого снизилась.

Вот краткий пример того, как может выглядеть псевдокод класса мешка:

Class Bag //Добавляем предмет в мешок, передавая объект (а затем просто записываем его в массив) Function addItem (Object item) { itemsInBag.push(item); } //Для возврата случайного предмета используем встроенную функцию random, возвращая предмет из массива Function getRandomItem () { return(itemsInBag[random(0,itemsInBag.length-1)]); }
}

Реализация слотов редкости

Слоты редкости — это способ стандартизации для задания частоты выпадания объекта (обычно используемый для упрощения процесса создания дизайна игры и вознаграждений игрока).

Вместо задания частоты каждого отдельного предмета в игре мы создаём соответствующую ему редкость — например, редкость «Обычный» может представлять вероятность определённого результата 20 к X, а уровень редкости «Редкий» — вероятность 1 к X.

Этот способ не сильно изменяет функцию самого мешка, зато может использоваться для увеличения эффективности на стороне разработчика, позволяя быстро назначить статистическую вероятность экспоненциально большому количеству предметов.

Оно позволяет быстро и без необходимости возиться с числами понять, как часто должно происходить событие, чтобы игрок не терял интереса. Кроме того, разделение редкости на слоты полезно для изменения восприятия игрока.

Вот простой пример того, как мы можем добавить в наш мешок слоты редкости:

Class Bag { //Создаём массив для всех элементов, находящихся в мешке Array itemsInBag; //Добавляем предмет в мешок, передавая объект Function addItem (Object item) { //Отслеживаем циклы относительно разделения редкости Int timesToAdd; //Проверяем переменную редкости предмета //(но сначала создаём эту переменную в классе предмета, //предпочтительно перечислимого типа) Switch(item.rarity) { Case 'common': timesToAdd = 5; Case 'uncommon': timesToAdd = 3; Case 'rare': timesToAdd = 1; } //Добавляем экземпляры предмета в мешок с учётом его редкости While (timesToAdd >0) { itemsInBag.push(item); timesToAdd--; } }
}

Случайные числа с переменной частотой

Мы рассказали о некоторых из самых распространённых способов работы со случайностями в играх, поэтому давайте перейдём к более сложному. Концепция использования переменных частот начинается аналогично мешку из приведённых выше примеров: у нас есть заданное количество результатов, и мы знаем, насколько часто хотим их возникновения. Разница в реализации в том, что мы хотим изменять вероятность результатов при их возникновении.

Возьмём, например, игры с собиранием. Зачем нам это может понадобиться? Проблема возникает тогда, когда мы учитываем несколько вытягиваний из мешка. Если у нас есть десять возможных результатов для получаемого предмета, когда девять являются «обычными», а один — «редким», то тогда вероятности очень просты: 90% времени игрок будет получать обычный предмет, а 10% времени — редкий.

Давайте посмотрим на наши шансы получения серии обычных результатов:

  • При первом вытягивании вероятность получения обычного предмета равна 90%.
  • При двух вытягиваниях вероятность получения обоих обычных предметов равна 81%.
  • При 10 вытягиваниях по-прежнему существует вероятность 35% всех обычных предметов.
  • При 20 вытягиваниях всё равно есть вероятность в 12%.

То есть хотя изначальное соотношение 9:1 казалось нам идеальным, на самом деле оно соответствует только средним результатам, то есть 1 из 10 игроков потратит на получение редкого предмета вдвое больше желаемого. Более того, 4% игроков потратят на получение редкого предмета в три раза больше времени, а 1,5% неудачников — в четыре раза больше.

Как эту проблему решают переменные частоты

Решение заключается в реализации в наших объектах интервала случайности. Для этого мы задаём максимальную и минимальную редкость каждого объекта (или слоты редкости, если вы хотите соединить этот способ с предыдущим примером). Например, давайте дадим нашему обычному предмету минимальное значение редкости 1, а максимальное — 9. Редкий предмет будет иметь минимальное и максимальное значение 1.

При первом вытягивании есть вероятность 90% получения обычного предмета. Теперь по показанному выше сценарию у нас будет десять предметов, девять из которых являются экземплярами обычного, а один — редким. При переменных частотах после вытягивания обычного предмета мы снижаем его значение редкости на 1.

После каждого результата с обычным предметом случайность этого предмета падает, что увеличивает вероятность вытягивания редкого, пока мы не останемся с двумя предметами в мешке, одним обычным и одним редким. При этом в следующем вытягивании у нас будет в целом девять предметов, восемь из которых обычные, что даёт вероятность 89% вытягивания обычного.

Вероятность граничных результатов, таких как вытаскивание 20 обычных предметов подряд, снижается до 0,5%, а дальше становится и того меньше. Таким образом, вместо вероятности 35% вытягивания 10 обычных предметов подряд у нас останется вероятность всего в 5%. Это создаёт постоянные результаты для игроков, и защищает нас от граничных случаев, в которых игрок постоянно вытягивает плохой результат.

Создание класса переменных частот

Самой простой реализацией переменной частоты будет извлечение предмета из мешка, а не просто его возвращение:

Class Bag { //Создаём массив для всех элементов, находящихся в мешке Array itemsInBag; //Заполняем мешок предметами при его создании Constructor (Array startingItems) { itemsInBag = startingItems; } //Добавляем предмет в мешок, передавая объект (а затем просто записываем его в массив) Function addItem (Object item) { itemsInBag.push(item); } Function getRandomItem () { //pick a random item from the bag Var currentItem = itemsInBag[random(0,itemsInBag.length-1)]; //Снижаем количество экземпляров этого предмета, если он выше минимума If (instancesOf (currentItem, itemsInBag) > currentItem.minimumRarity) { itemsInBag.remove(currentItem); } return(currentItem);
}
}

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

Развитие идеи

Мы описали основную идею переменных частот, но в собственных реализациях нам нужно рассмотреть ещё довольно много аспектов:

  • Удаление предметов из мешка позволяет добиться постоянных результатов, но со временем возвращает нас к проблемам стандартной рандомизации. Как мы можем изменить функции таким образом, чтобы и увеличение, и уменьшение предметов позволяли этого избежать?
  • Что происходит, когда мы имеем дело с тысячами или миллионами предметов? В этом случае решением может быть использование мешка, заполненного мешками. Например, можно создать мешок для каждой редкости (все обычные предметы в одном мешке, редкие в другом) и поместить каждый из них в слоты внутри большого мешка. Это обеспечит широкие возможности для манипуляций.

Менее скучные случайные числа

Во многих играх по-прежнему для создания сложности используется стандартное генерирование случайных чисел. При этом создаётся система, в которой половина игроков отклоняется в одну или другую сторону от ожидаемых результатов. Если оставить это без внимания, возникнет возможность граничных случаев со слишком большим количеством повторяющихся неудачных результатов.

Ограничивая интервалы разброса результатов, мы можем обеспечить более целостный игровой процесс и позволить получать от неё удовольствие большему количеству игроков.

Подводим итог

Генерация случайных чисел — один из столпов хорошего геймдизайна. Для улучшения игрового процесса тщательно проверяйте свою статистику и реализуйте наиболее подходящие виды генерирования.

Теги
Показать больше

Похожие статьи

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

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

Кнопка «Наверх»
Закрыть