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

в 15:21, , рубрики: random number generator, rarity slotting, генератор случайных чисел, разработка игр, рандомизация
image

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Class Bag {
    //Создаём массив для всех элементов, находящихся в мешке
	Array itemsInBag;

	//Заполняем мешок предметами при его создании
	Constructor (Array startingItems) {
		itemsInBag = startingItems;
	}

	//Добавляем предмет в мешок, передавая объект (а затем просто записываем его в массив)
	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% вытягивания обычного. После каждого результата с обычным предметом случайность этого предмета падает, что увеличивает вероятность вытягивания редкого, пока мы не останемся с двумя предметами в мешке, одним обычным и одним редким.

Таким образом, вместо вероятности 35% вытягивания 10 обычных предметов подряд у нас останется вероятность всего в 5%. Вероятность граничных результатов, таких как вытаскивание 20 обычных предметов подряд, снижается до 0,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);
}
}

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

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

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

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

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

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

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

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

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

Автор: PatientZero

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js