Матчасть: Реверсный инжинеринг баланса (часть 1)
Ананькин Сергей, продюсер компании Pixonic, написал увлекательную (гигантскую) статью про обратное проектирование баланса в играх. С разрешения автора мы публикуем ее на страницах App2Top.ru. Мы настоятельно рекомендуем с ней внимательно ознакомиться, но будьте готовы к математике и построению графиков!
0. О предмете статьи
Разрабатывая и выпуская игру, мы неизбежно опираемся на опыт других команд и других проектов в каждом аспекте — от проектирования геймплея (flappy bird rules!) до выбора стратегии привлечения (к примеру, большая виральнось = низкий Acquisition Cost, виват Candy Crush).
Не является исключением и математическая модель, регулирующая игровую сложность и управляющая экономическими циклами в игре. В попытке создать идеальный баланс внутри своей игры, одним из ключевых факторов успеха является детальный разбор похожих успешных проектов, позволяющий понять их математическую суть — набор законов, управляющих игровой экономикой и геймплеем. Такие законы затем можно использовать в своем проекте, адаптируя их при необходимости под реалии своей игры. Процесс выявления этих математических законов мы и называем реверсным инжинерингом (от слов «инжинеринг» — т. е. проектирование, и «реверс» — т. е. обратное).
В этой статье мы постараемся разобраться, что же именно такое реверсный инжинеринг, как этот процесс устроен, чем нам приходится оперировать в результате этого процесса и каковы его плоды.
Как и всегда, статья не является конкретным руководством к действию и не содержит точных законов, а просто отражает наш личный опыт в данном вопросе и наши подходы к понимаю и реализации реверсного инжинеринга.
1. О формулах и числах
Представим себе две переменных, одна из которых зависит от другой. К примеру, символом E обозначим количество опыта, которое нужно заработать игроку для перехода на следующий уровень, а символом x — текущий уровень игрока. Пусть E зависит от x (т. е., например, находясь на первом уровне, игроку нужно набрать 10 опыта для перехода на следующий, второй, уровень, а находясь на пятом уровне — 100 опыта для перехода на следующий, шестой).
Зависимость E от x обычно записывают в виде E(x) и говорят, что E — это функция от аргумента x (поэтому E записана прописной буквой, а x — строчной). Такая зависимость может быть представлена в двух видах:
- Непрерывном, с помощью уравнения (например, E(x) = x2);
- Дискретном, с помощью таблицы (каждом конкретному значению x будет соответствовать значение E).
Основным отличием непрерывного вида от дискретного является следующее: если функция задана непрерывно, её значение может быть получено при любом значении аргумента (конечно, если для этого значения аргумента значение функции вычислимо в приципе). Но что это значит для нас?
Имея на руках табличное задание функции, вы знаете её значение только для выбранных заранее значений аргумента (в нашем примере это значения x = 1, 2, 3, 4, 5 и т. д.). Если у вас на руках уравнение, вы сможете определить значение функции не только для тех же самых x = 1, 2, 3, 4, 5 и т. д., но и для любых других — например, для x = -2 или x = 3.75. В нашем примере с уровнем и опытом значения x = -2 или x = 3.75 не имеют смысла (ведь уровень — целое положительное число!), однако задумайтесь: что есть ваша табличка заканчивается на значении x = 100, а вам потребовалось узнать, сколько опыта должен набрать игрок для перехода со 101 на 102 уровень? Для того, чтобы ответить на этот вопрос, потребуется уравнение.
Изначально, анализируя другой (чужой) проект, вы получаете только дискретную запись для каждой функции, имеющейся в игре. Представьте, что делая ферму, вы взяли за основу баланс популярной игры в этом жанре, начали набирать один уровень за другим и выписывать, сколько опыта потребуется для получения следующего уровня. Пройдя сто уровней, вы получите дискретную запись функции E(x) — табличку, в которой будет сто строк, по одной на каждое целое значение x начиная с 1.
К этой записи у вас возникнет множество вопросов. Например: каким будет значение E для x = 150? по какому принципу выбраны эти числа? насколько быстро с увеличением x они возрастают? возрастает или уменьшается скорость роста этих чисел?
Так мы подходим к главным задачам реверсного инжинеринга. Реверсный инжинеринг математики проекта предназначен в первую очередь для выявления зависимостей в игре, а во вторую — для получения непрерывной записи этих зависимостей. Выявление зависимостей позволит нам понять, какие из игровых переменных связаны друг с другом. Получение непрерывной записи (т. е. уравнения) даст нам возможность использовать эти зависимости по своему усмотрению, а также видоизменять их под наши нужды.
2. Дискретизация, аппроксимация и прочий стаф
Процесс перехода от непрерывной записи к дискретной, надо понимать, довольно прост. Имея на руках уравнение, вы можете последовательно подставить в него различные значения аргумента и получить соответствующие значения функции. В нашем примере, подставляя x = 1, 2, 3, 4, 5 и т. д. в уравнение E(x) = x2, получим значения для E = 1, 4, 9, 16, 25 и т. д. Такой процесс называется дискретизацией функции.
Обратный процесс (получение уравнения по табличке значений), называемый аппроксимацией, как правило, гораздо сложнее. Он-то и представляет для нас особый интерес. Перед обсуждением того, как мы будем аппроксимировать, давайте поймём, зачем конкретно это нужно.
Пользуясь правильной терминологией, можно обозначить следующие задачи, выполнить которые позволяет аппроксимация:
- Интерполяция, т. е. получение промежуточных значений функции (помните пример про нахождение E для x = 2.5, который в случае уровней не имеет большого смысла, но при расчёте других функций может стать головной болью, если у нас есть только табличная запись функции);
- Экстраполяция, т. е. получение значений за пределами изначально описанной области (эту задачу нужно решить, если табличка закончилась на x = 100, а вам нужно найти E для x = 150).
- Анализ, т. е. получение сведений о поведении функции (другими словами, получение картины происходящего). Например, что можно сказать о росте опыта «на следующий уровень», имея на руках формулу E(x) = x2? Очевидный вывод: E возрастает с ростом x, т. е. для большего уровня требуется больше опыта, чтобы перейти на следующий. Менее очевидный вывод: скорость роста E не только положительна, но и возрастает. Это значит, что чем дальше продвинулся игрок, тем на больший процент возрастает нужный для следующего уровня опыт. После получения функции в рамках её анализа можно будет также сравнивать её с другими функциями, чтобы понять, какие из них растут быстрее, какие медленнее, и тем самым видеть, как изменяется игровой баланс для игрока со временем.
3. Выявление зависимостей
Нашей первой задачей, ещё до того, как аппроксимация будет иметь смысл, является выявление тех зависимостей, непрерывную запись которых мы хотели бы получить. Как правило, игровые циклы строятся на множестве разных зависимостей, простых и не очень.
В нашем примере зависимость E от x выявляется «на глаз». Для игры в жанре «ферма» количество опыта, нужного для перехода на следующий уровень, вряд ли будет зависеть от текущей экипировки персонажа или количества его друзей.
В основном при анализе проекта вам встретятся более сложные зависимости. Подход к их выявлению может быть представлен следующим набором шагов:
- Сформировать первичный список игровых переменных, которые могут зависеть от других (для нашего примера цена покупки товара в магазине, возможно, зависит от его уровня или характеристик; или же всё сложнее — характеристики могут зависеть от его уровня, а цена — от характеристик);
- Сформировать первичный список игровых переменных, которые скорее всего ни от чего не зависят, либо закон их формирования предельно ясен (например, номер следующего уровня — всегда +1 от предыдущего, а награда за новый уровень — всегда +1 монета);
- Составить список всех зависимостей, которые потенциально возможны (к примеру, цена предмета от его уровня, цена предмета от его параметров, его уровень от его параметров и т. д.);
- Провести начальное исследование этих потенциальных зависимостей на предмет того, похожи ли они на зависимости или выглядят как случайные наборы чисел.
При составлении списка потенциальных зависимостей для исследования важно не бояться начать. Ключ к бесстрашию прост: нужно помнить, что если выбранная зависимость не окажется зависимостью вовсе, её анализ покажет вам это понять, и вы смело сможете исключить эту зависимость из своего списка.
Остановимся подробнее на пункте «начальное исследование», который, по идее, должен вызывать наибольшее беспокойство. Рассмотрим пример: пусть в исследуемой игре нужно выращивать растения и продавать их в своём магазине. Растения становятся доступны для выращивания на разных уровнях, имеют разное время созревания и продаются игроком за разное количество монет. Здесь возможны различные варианты зависимостей: цена продажи и время созревания могут зависеть от уровня, на котором эти растения становятся доступны, а могут — друг от друга.
В описанном случае я начал бы с параллельного анализа обоих вариантов. Представим, что в игре есть всего 10 растений. Данные по каждому из них описаны в таблице ниже.
Здесь в левой колонке каждому растению назначен порядковый номер (вместо названия). В последующих колонках для каждого растения приведены уровень его доступности для игрока, время созревания в минутах и цена продажи в магазин.
Попробуем проанализировать следующие зависимости:
- Время от уровня;
- Цена от времени.
Вы будете смеяться, но самым практичным способом выявления зависимости я считаю построение графика предполагаемой функции по её табличной записи. Строить такие графики просто. Мы выбираем две колонки таблицы, сортируем их по возрастанию значений в колонке аргумента, а затем откладываем по оси x значения в колонке аргумента, а по оси y — значения в колонке функции.
Вот что у нас получилось. В варианте слева в качестве функции выступают значения в колонке МИН, а в качестве аргумента — значения в колонке УРОВЕНЬ. Для варианта справа функцией выступает ЦЕНА, а аргумент представлен колонкой МИН.
Существует множество способов аппроксимации табличной записи уравнения определённых видов, но, как я уже сказал, на практике самым удобным является наблюдение за построенным графиком. Для того, чтобы понять, является ли полученный график представлением функции, а не случайным набором точек, посмотрите на него и задайте себе простой вопрос: могу ли я мысленно (хотя бы теоретически) продолжить график дальше? Мы видим, что в случае a) это вряд ли возможно (здесь мы имеем ломаную линию, которая идёт то вверх, то вниз, и предсказать её поведение у нас не получится). Тогда как в случае b) нам очевидно, что график далее пойдёт вверх, и скорость его роста будет замедляться. Это значит, что в случае а) мы имеет дело с отсутствием зависимости, а в случае b) — с её наличием.
Перед тем, как бросаться аппроксимировать функции и в полной мере пользоваться графиками, нужно заметить ещё кое-что. Позвольте сделать предсказание: даже если вы мастерски аппроксимируете набор значений, полученная функция никогда не воспроизведёт табличные данные на сто процентов точно. Это происходит по двум причинам:
- Округление. Формула, которой пользуется разработчик баланса в том или ином случае, не заботится о красоте выдаваемых ей чисел. Так квадратный корень из двух — это число с бесконечным количеством знаков после запятой. Поместить такое число в игру невозможно, так что приходится округлять его. Заметьте, что на малых числах округление вносит в данные особенно большой разброс. К примеру, тот же корень из двух, который равен 1.4142135… я могу округлить до 1.5, а если в игре должны быть только целые числа, то до 1 или 2. Отличие на единицу в данном случае — очень существенно. Например, различающиеся на единицу числа 100 и 101 по сути отличны лишь на 1%, тогда как 1 и 2 различаются на 100%!
- Ручной «тюнинг» — особенная головная боль при реверсном инжинеринге. Часто (и это правильно) разработчик использует применяемую им формулу лишь как отправную точку, т. е. с её помощью он строит только первичную версию баланса, которую затем в некоторых местах правит руками, исходя из таких критериев как его личное чутьё, статистика игры, пожелания игроков и т. д. Будучи настроенными вручную, числа могут не просто немного отклониться от формулы, но существенно запутать того, кто пытается выявить изначальный закон. Для демонстрации рассмотрим простой пример.
Предложим, мы выяснили, что к 18 уровню игрок начинает испытывать существенные затруднения с игрой (например, ему не хватает игровых денег), из-за этого устаёт играть и уходит, чтобы никогда не вернуться. Мы решили проблему просто — растению, которое выдаётся на 18 уровне (порядковый номер 6 в нашей таблице) мы искусственно завысили цену продажи до 50, чтобы игрок получил мощный механизм зарабатывания денег и испытал прилив сил (такой пример, конечно, утрирован, но в целях демострации вполне походит).
Ниже представлены два графика — изначальный и тот, который мы получим после такой ручной настройки баланса.
На графике справа явно видна точка, отстоящая от общего закона. В данном случае (а в общем-то, всегда) самым полезным, чтобы не терять общую картину, будет исключить подобные точки из рассмотрения. Если на графике справа мы уберём точку (80, 50) и соединим соседние точки прямой, мы почти так же ясно увидим график функции, как и слева.
Главные рекомендации могут звучать так: не дайте отдельным числам обмануть себя и не пугайтесь пиков, отстоящих от общего закона. По возможности исключайте их из рассмотрения, чтобы вернуться к их обоснованию позже.
4. Всякие разные типы функций
Итак, мы имеем табличное распределение, по которому мы уже построили график. Этот график мы мысленно можем продолжить, тем самым получая понимание того, что перед нами — функция. Как я и говорил, существует множество математических методов аппроксимации, но, как показывает практика, нас должен интересовать некоторый естественный алгоритм, который позволит нам не терять понимание того, что именно мы делаем.
Первый шаг в таком алгоритме — понять, каков тип функции, график которой мы видим. Тип функции задаёт базовую форму её графика, которую потом мы можем модифицировать (смещать, сжимать, растягивать) с помощью масштабирующих коэффициентов (т. е. всяких численных слагаемых и множителей, которые мы вводим в уравнение).
Существует множество различных типов функций. Некоторые — насколько сложны, что по их графику никогда не догадаешься, что это уравнение, а не случайный набор точек. По счастью, в 99% случаев мы не имеем дело с такими функциям. Ниже я попробую перечислить самые используемые типы функций и показать, как выглядят их графики. После изучения этого раздела у вас не должно возникнуть проблем с тем, чтобы по внешнему виду графика определить тип функции.
В дальнейшем для простоты мы будем использовать следующую запись:
- Аргумент функции будет обозначаться буквой x;
- Значение функции будет обозначаться буквой y;
- Запись y = f(x) будет обозначать, что переменная y представлена неким уравнением, где x выступает в качестве аргумента;
- Буквами a, b, c и т. д. будем обозначать константы, т. е. какие-то числа, которые входят в уравнение f(x) и не зависят от x.
4.1. Функция-константа
Это самый простой пример. В уравнении такой функции x и у на самом деле вовсе не зависят друг от друга!
Примеры: y = 4, x = 2.
На графике слева представлены две функции. Синяя описывается уравнением y = a (здесь функция y принимает значение a при любом x), а красная — уранением x = b (здесь аргумент фиксирован в значении b, а y принимает любые значения).
По сути если y = a, то это значит, что y не изменяется, каким бы ни был аргумент x. К примеру, если мы сказали, что количество восполняемой энергии игрока в минуту равно 1, и не важно, какой при этом уровень у игрока, это — пример функции-константы.
4.2. Линейная функция
Такая функция в общем случае описывается уравнением y = a*x + b.
Примеры: y = x, y = 2*x + 3, y = 5-x.
График такой функции — прямая линия, наклоненная под каким-то углом к осям. На картинке представлена функция y = 2*x, её график проходит через начало координат (потому что значение функции при x = 0 тоже равно 0). В общем случае эта прямая не обязана проходить через начало координат.
Основная особенность линейной функции — постоянная скорость роста (или убывания в случае a < 0). Представьте себе, что цена продажи растения линейно засит от времени его производства. В этом случае можно показать, что если растение А созревает в два раза дольше, чем B, то и стоить оно будет в два раза больше. Простые законы выгодно задавать линейной функцией из-за её простоты. К примеру, сказав, что максимальное кол-во энергии у игрока равно уровень, умноженный на одну треть, и округляя его каждый раз до целого числа в меньшую сторону, получим простой закон: каждые три уровня максимальное кол-во энергии возрастает на 1.
4.3. Степенная функция
Этот тип функции содержит в себе несколько подтипов, которые на практике удобно рассматривать отдельно. Каждый из таких подтипов представляется одной и той же формулой: y = k*xa + b, но отличается от остальных тем, в каком интервале лежит число a.
a = 1, a = 0
Как можно догадаться, линейная функция и функция-константа являются частными случаями степенной функции. В случае a = 1 мы имеем дело с линейной функцией (y = k*x + b), а в случае a = 0 — с константой (y = k + b).
a > 1
В этом случае график функции является кривой, называемой параболой.
Примеры: y = x3, y = 4*x3+3.
На графике выше представлены функции y = x2 (фиолетовая кривая) и y = x3 (зелёная кривая). Как правило нас будет интересовать исключительно правая верхняя четверть координатного пространства (там, где y и x положительны), однако необходимо понимать различие в поведении функций этого типа и на других четвертях. Заметьте, что кубическая парабола (зелёная) идёт вниз, когда x становится меньше нуля и продолжает уменьшаться, в то время как квадратная парабола (фиолетовая) увеличивается на том же отрезке. На самом деле, любая парабола, где a — чётное, никогда не примет отрицательного значения (потому что отрицательный x при возведении в чётную степень даст положительный результат), в то время как параболы с нечётными степенями могут принимать отрицательные значения (например, -3, возведённое в куб, даст -9).
Особенность такой функции в том, что она позволяет организовать рост с увеличивающейся скоростью. Как правило, такие функции применяют к росту игровой сложности, требований к игроку или увеличению дефицита. Очевидный пример, который мы уже рассматривали ранее — рост количества опыта, требуемого для достижения следующего уровня. Часто разработчики используют для задания этого роста степень 2 или 3. Другой пример — увеличение времени выращивания растений в зависимости от уровня. Здесь если уровень анлока растения А в два раза выше, чем у растения B, то время его производства будет больше времени произовдства B более чем в два раза.
0 < a < 1
График такой функции похож на параболу, повернутую на 90 градусов.
Примеры: y = √x, y = 2* x1/3.
На графике представлены квадратный (синий) и кубический (красный) корни из x. Снова заметьте, что на x < 0 функции ведут себя по-разному. Там квадратный корень из отрицательного числа не определён вовсе, а кубический — существует.
Необходимо понимать, что корень из x — это всё равно степенная функция. Наглядно продемонстрировать это можно на примере с квадратом и квадратным корнем. Так «x-квадрат» — это x в степени 2, а «квадратный корень из x» — это x в степени 1/2. Всякий раз степень x можно представить дробью, в которой числитель — это степень x, а знаменатель — это степень корня из x. Так x в степени 2/3 есть по сути кубический корень из х в квадрате.
Заметим, что значение степени само по себе определяет скорость роста функции. В случае с a > 1 — чем a больше, тем быстрее функция растёт (см. график к соотв. пункту, на котором куб возрастает быстрее, чем квадрат). Здесь ситуация ровно та же: 1/2 больше, чем 1/3, поэтому квадратный корень из x будет расти быстрее, чем кубический.
Функции такого типа нужны, когда мы хотим организовать замедление игрового прогресса. Посмотрите на наш пример с таблицей цен продажи растений в зависимости от времени их созревания. Цена продажи растёт с ростом времени, но этот рост замедляется. График, полученный на основе таблицы, формой очень похож на корень, не правда ли?
a < 0
Понять смысл отрицательной степени довольно просто. Она лишь говорит о том, что аргумент находится в знаменателе. Так, например, запись y = x-3 означает то же самое, что запись y = 1/x3.
График функции такого типа называют гиперболой.
На графике представлены функции для a = -1 (красная) и a = -2 (зелёная). Снова заметьте различия в поведении функций на разных участках координатного пространства. Функция для a = -1 существует в двух противоположных четвертях (т. е. знак y будет либо всегда совпадать с x, либо всегда быть противоположным, в зависимости от констант, входящих в формулу), а вот в случае a = -2 функция существует в одной половине (знак y будет либо всегда положительным, либо всегда отрицательным, в зависимости от констант в формуле).
Применить такую функцию можно для того, чтобы организовать убывание какой-нибудь величины в игре. Заметьте, что скорость такого убывания будет уменьшаться со временем.
Обобщение
Степенные функции наиболее часто употребимы в игровом дизайне. Они легко поддаются исследованию и позволяют организовать возрастание или убывание той или иной величины с чётко определённой скоростью, изменение которой также легко контроллировать. Все подтипы степенных функций тесно связаны друг с другом. Так, например, найдя, что y равен x в квадрате, вы сможете смело заявить (по крайней мере, в положительной четверти пространства), что х равен корню из y.
Существует более общая форма для степенной функции — так называемый полином степени n. Записывается он в таком виде: y = kn * xn + kn-1 * xn-1 + kn-2 * xn-2 + … + k0 * x0. Например, полиномом степени 4 является y = 2*x4+ 1*x3 + x1 + 3. Здесь k2 = 0, поэтому мы не встречаем члена x2.
На практике использование такой функции оправдано, т. к. позволяет производить более точную и тонкую настройку баланса (по сути у этой формулы больше «рычагов», которые можно крутить, чтобы подстроить баланс в ту или иную сторону), однако эта функция более сложна для исследования и требует более обстоятельного математического аппарата для настройки.
4.4. Показательная функция
Эта функция может быть записана в виде y = k * ax. Аргумент здесь выступает уже не в качестве основания степени (т. е. того, что возводится в степень), а её показателя (т. е. числа, показывающего, в какую степень мы возводим). В качестве основания выступает константа.
Ярким примером, представленным на графике слева, является популярная функция «экспонента», которую мы привыкли считать визитной карточкой баланса азиатских MMORPG. Экспонента, это y = ex, где e — это знаменитое особенное число, обладающее рядом примечательных математических свойств. Оно примерно равно 2.718281828 (запомнить легко — за цифрами 2 и 7 дважды следует год рождения Л. Н. Толстого =).
График такой функции внешне похож на параболу, но возрастает (или убывает, если a меньше единицы) значительно быстрее. При малых а (например, 1.000000001) показательная функция будет возрастать медленнее, но рано или поздно всё равно перегонит любую степенную функцию.
Показательную функцию используют, если хотят организовать очень резкий рост какой-либо величины (в азиатских MMO эту функцию применяют для увеличения количества опыта, нужного для достижения следующего уровня, чтобы организовать быстрое замедление прогресса игрока по уровням).
4.5. Логарифмическая функция
В расчётах я нечасто использую логарифмическую функцию, но это не остановит меня от её упоминания в статье. Вдруг столкнувшись с ней в балансе разбираемой нами игры, мы должны быть готовы узнать и эту функцию.
Логарифм с основанием a от числа x, это та степень, в которую нужно возвести a, чтобы получить x. Таким образом записать выражение y = logax — это всё равно что сказать, что ay = x.
Из этого определения следует, что логарифмическая функция обратна показательной, т. е. если мы знаем, что y — это логарифм по основанию a от x, то можно получить обратный закон: х — это a в степени y.
На рисунке логарифм по основанию e (красный) показан в сравнении с квадратным корнем (синий). Наиболее часто употребимыми являются логарифмы с основаниями 2 (двоичный), e (линейный) и 10 (десятичный), при этом чем больше основание логарифма, тем выше будет его график. Заметьте, что если основание a < 1, то логарифмическая функция убывает.
4.6. Тригонометрические функции sin и cos
Также редко используются при проектировании игрового баланса, но не сказать о них здесь было бы как-то неуважительно.
На рисунке представлены графики y = sin(x) (голубой) и y = cos(x) (зелёный). Их характерная особенность — периодичность. Вы можете использовать их в том случае, если хотите организовать в игре периодичность и повторяемость. К примеру, если в вашей игре есть времена года, то урожайность (или счастье нации) может изменяться по похожему закону (возрастает летом и падает зимой).
Как следует из сказанного, логарифмическая функция возникает, если две переменные связанны показательным законом. Этот закон — слишком резкий, и в большинстве случаев редко подходит в качестве базиса для построения игрового баланса. Однако он имеет смысл в случае, если вы хотите «резко затянуть гайки» на более поздних этапах игры, чтобы, например, растянуть время, за которое игрок исчерпает все остатки контента и уйдёт до следующего обновления игры. Насколько я помню, Blizzard частенько делал такое со своим World of Warcraft ещё на заре его существования.
Выводы и заключение вы найдете во второй часть статьи.