Привет!
Недавно в ранний доступ в Steam вышла наша игра Clayers: Prologue. Это рогалик в глиняном стиле, где нужно подбирать и смешивать цвета, чтобы убивать врагов. В этой статье разберём наш подход к генерации волн с учётом сложности противников.
Немного об игре

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

Оранжевый получается из красного и жёлтого, зелёный — из синего и жёлтого, фиолетовый — из синего и красного. При выстреле смешанной пулей расходуется по 0,5 объёма пули из каждого бака с краской.

Алгоритм
Генерация волн не процедурная, а заранее подготовленная для каждой локации. Для этого был создан простой алгоритм, который по заданным параметрам (типы врагов, цвета и максимальное количество противников в волне) генерирует псевдослучайный набор.
Задаём предельное количество врагов и общую сложность волны.
Исходя из текущего набора ресурсов (объём красок для выстрелов), рассчитываем сложность убийства каждого противника.
В цикле добавляем разных врагов, пока суммарная сложность пачки не приблизится к целевому значению. Например, сначала жадно выбираем самого лёгкого врага, учитывая цвет и тип, а затем корректируем выбор для разнообразия игрового опыта.
Первый и третий пункты достаточно очевидны, поэтому основное внимание в статье уделим формуле сложности врага.
Формула сложности следующего врага в волне
1. Вероятность попадания нужным цветом
Для начала определим вероятность того, что игрок при выстреле попадёт краской определённого цвета.
можно вычислить как среднее по забегам игроков.
2. Функция урона выстрела
Функция урона зависит от цвета пули и врага. В нашем случае враг получает тройной урон, если цвет пули совпадает с его цветом:
3. Мат. ожидание урона
Можно вычислить математическое ожидание урона для n-го выстрела по каждому цвету, так как мы имеем дело с дискретной случайной величиной.
4. Максимальный возможный урон
Чтобы определить, хватит ли ресурсов для ликвидации врага, необходимо вычислить максимальный возможный урон
Мы можем спокойно использовать это значение, так как слагаемые других красок в математическом ожидании обнулятся, а значения для оставшихся выстрелов будут корректно рассчитаны с учётом остатка краски.
5. Вероятность убийства врага
Зная максимальный урони здоровье врага
врага, вычисляем вероятность гарантированного убийства:
6. Коэффициент ресурсной сложности врага
Вычисляем коэффициент ресурсной сложности врага, показывающий, насколько мало ресурсов нужно потратить, чтобы его убить:
Где — общее количество краски у игрока.
7. Итоговая сложность убийства врага
Где - коэффициент уклонения врага (учитывает возможность его убить без стрельбы, например, для взрывающихся врагов типа Камикадзе).
При этом , и
Возвести в степень
важно, чтоб "заединичить"
. Если
, то не хватит ресурсов убить врага.
В итоге предел дает нам верхнюю границу, необходимую для подсчета сложности волны.
Итог
В результате у нас получился простой и стабильный алгоритм, который:
Обеспечивает сбалансированную сложность волн.
Учитывает ресурсную сложность и вероятность убийства врага.
Позволяет варьировать состав врагов для разнообразия геймплея.
Теперь нам не нужно вручную контролировать разнообразие врагов в волнах — можно просто наслаждаться процессом! Надеемся, что статья была интересной. Оцените волны в игре, предлагайте свои идеи по улучшению алгоритма и задавайте вопросы. За крутые идеи и пожелания можем скинуть ключик!