Каждый геймдизайнер рано или поздно сталкивается с необходимостью регулярной работы с таблицами. Базовых знаний Google Sheets или Excel обычно достаточно для того, чтобы вести простые расчеты и работать над игровым балансом. Но когда игра развивается и обрастает новыми сущностями, возникает потребность в том, чтобы организовать для себя удобную и эффективную структуру. При этом нужно иметь возможность легко в ней ориентироваться, дополнять и модифицировать ее, она должна быть устойчива к ошибкам.
В этой статье я систематизировал собственный опыт работы с таблицами и их дополнительными функциями, которые могут оказаться полезными как для начинающих, так и опытных геймдизайнеров. Примеры приведены в Google Sheets, поскольку они чаще используются командами разработки, но большая часть из них применима и к MS Excel.
Итак, представим себе, что мы делаем игру, в которой используется несколько видов оружия, и нам нужно создать баланс, который позже будет залит на сервер. Изначально у нас есть только обычный нож, обладающий двумя параметрами — урон (Damage) и количество атак в секунду (Attacks/Sec). На первом этапе для подсчета баланса нам достаточно этих параметров, и таблица выглядит так:
Со временем мы понимаем, что одного ножа нам мало, и добавляем еще несколько видов оружия: пистолет, пулемет и снайперскую винтовку. Обсуждаем каждую пушку с программистами, пишем документацию, начинаем разработку. В какой-то момент доходим до стадии, когда нужно внести значения для нового оружия в таблицу баланса. И тут выясняется, что для пушек с пулями был сделан отдельный параметр урона за пулю (Damage/bullet), а еще добавился новый параметр Reload_time, отвечающий за длительность перезарядки. В результате таблица баланса преобразуется так:
Все хорошо работает, мы довольны разнообразием, но игроки говорят, что им все равно как-то скучно. Значит, нужно добавить еще несколько пушек: например, ракетницу и лазерную установку, которые сильно отличаются от всего, что мы делали раньше. При этом, чтобы игроки не скучали, мы делаем быструю доработку снайперской винтовки, позволяя ее пулям отравлять врагов. Винтовка получает дополнительный параметр урона в секунду (Damage/sec). Этот же параметр начинает использовать наша лазерная установка. Ракетница же обретает свои, абсолютно новые параметры. Получается следующая таблица:
Время идет, и мы добавляем все больше и больше новых параметров. Таблица разрастается, но пока мы еще можем просматривать и редактировать ее без особых проблем:
Внезапно после очередного обновления мы узнаем от игроков, что они не понимают, какая пушка круче. Мы решаем закрыть эту проблему самым простым способом — отображая DPS каждого оружия в интерфейсе игры.
И тут возникают различные проблемы и неудобства. Например, чтобы посчитать DPS ножа для отображения в игровом UI, нужно перемножить значение Damage и Attacks/sec, для пистолета и пулемета — перемножить Damage/bullet и Attacks/sec, а для ядовитой пушки, способной отравить противника, — прибавить урон от отравления к основному урону. В итоге каждая пушка считает visual_DPS по собственной формуле, и об этом нужно помнить.
Но поскольку мы правильные геймдизайнеры, то мы достаточно ленивы, чтобы не хотеть помнить о таких вещах. Мы хотим получить одну формулу для расчета DPS, которую можно без лишних размышлений растянуть по всему столбцу таблицы. Конечно, кто-то может посмотреть на наш баланс и сказать: «Да что тут думать? Всего шесть пушек, все же просто, и даже если для каждой будет своя формула, не запутаешься». Но стоит помнить, что мы делаем вымышленную игру с вымышленным балансом, и наша таблица сейчас в разы проще, чем то, что используется в настоящих играх. Вот, например, небольшая часть баланса реального проекта, и это только один лист:
Но вернемся к нашему примеру. Мы пришли к тому, что решили упростить себе жизнь и сделать одну формулу расчета для отображаемого DPS. Можно пойти по пути «в лоб» и сделать сложную многоуровневую формулу, которая перемножает и складывает разные параметры в зависимости от наименования пушки. Это сработает, но потребует от нас доработки формулы каждый раз, когда добавляется новое оружие.
Пока мы думаем над решением этой проблемы, возникает еще одна, о которой мы не задумывались ранее: DPS, который отображается игроку, совсем не обязательно отражает то, что на самом деле происходит в игре. Оказывается, что фактический DPS для баланса нужно рассчитывать с учетом большого количества других параметров: от точности и скорости перезарядки до длины анимации вытаскивания оружия и эмпирического коэффициента сложности механики в некоторых совсем запущенных случаях.
Многие из таких параметров и коэффициентов совсем не нужны нам в финальном балансе и служат только для расчетов, но их нужно где-то хранить и редактировать. В этом нам могут помочь вспомогательные таблицы. В них мы будем вести сложные расчеты и добавлять для этого дополнительные параметры, нужные только нам. При этом в нашем финальном балансе, который мы собираемся загрузить на игровой сервер, в параметре visual_DPS мы сделаем функцию, которая будет обращаться к вспомогательной таблице и вытаскивать из нее готовое рассчитанное значение. Сама вспомогательная таблица при этом загружаться в игру не будет и останется служить нам геймдизайнерским инструментом.
Что такое вспомогательные таблицы и как их использовать
Для нашего примера с подсчетом DPS сделаем вспомогательную таблицу, в которой разделим пушки по типам, добавим необходимые расчетные коэффициенты и посчитаем итоговое значение — а в финальную таблицу баланса просто заберем это значение с помощью одной из функций Google Sheets уже без дополнительных расчетов.
То же самое мы можем сделать с ценой оружия в магазине. Обычно она тоже рассчитывается с учетом дополнительных коэффициентов, и параметры для ее расчета удобно хранить отдельно от боевых.
Получаются такие таблицы с вспомогательными параметрами. Я разместил их на отдельном листе и назвал его Calculations. Естественно, DPS тут считается не так, как это происходит в реальной игре, но сейчас для нас это не столь важно:
После того, как мы посчитали DPS во вспомогательной таблице, нам нужно забрать его в основную, которая будет в итоге заливаться в игру. Для этого мы используем VLOOKUP.
VLOOKUP
Функция VLOOKUP позволяет достать нужное значение из таблицы по значению в первом столбце этой таблицы и номеру столбца.
Простой пример
Допустим, мы хотим достать из таблицы фруктов значение Size для банана.
Search_key в нашем случае — Banana;
Range — вся таблица, кроме заголовков;
Index — 3, поскольку Size — это третий столбец таблицы;
Is_sorted — false, поскольку мы ищем точное совпадение.
В итоге наша функция будет выглядеть так:
Так мы получили значение long — как раз то, что искали.
В результате для того, чтобы прокинуть в основной баланс нужный нам параметр из вспомогательной таблицы, мы пишем в поле Visual_DPS формулу VLOOKUP и получаем следующий результат:
Так эта формула выглядит в нашей таблице баланса, которую мы собираемся использовать в игре:
A2 — название, которое мы будем искать во вспомогательной таблице (на скриншоте выше это Knife);
Calculations!$A$1:$O$10 — вспомогательная таблица без учета заголовков;
12 — номер столбца «DPS» во вспомогательной таблице;
false — для поиска по точному значению.
Вот мы и добились того, чего хотели: нашу итоговую формулу можно спокойно растягивать по всему столбцу, ведь все расчеты мы ведем в удобно организованном вспомогательном листе, а в основную таблицу просто подтягиваем нужные значения.
Подводные камни VLOOKUP — index
Базового функционала VLOOKUP хватает для того, чтобы сильно ускорить работу геймдизайнера, и в принципе многие могут остановиться уже на нем. Но если наш проект достаточно большой или развивается с высокой скоростью, всплывают проблемы, которые раздражают и повышают вероятность совершения ошибок.
Первая из них — параметр index, отвечающий за номер возвращаемого столбца. Когда основной баланс разрастается до 30-40 столбцов, базовых параметров во вспомогательных таблицах легко может оказаться в полтора раза больше. Кроме того, периодически возникает желание поменять формулы расчета и добавить новый коэффициент, а то и несколько. В итоге раньше мы своим VLOOKUP забирали параметр в 13-ом по счету столбце, и это был урон, а теперь в 13-ом столбце новый коэффициент, а урон переместился в 15-ый столбец.
Если мы это вовремя заметили, то можно поменять индекс вручную. Но что, если сместилось сразу 10-15 параметров? А если один и тот же параметр забирается и участвует в нескольких местах в основном балансе?
Получается, что основное неудобство для нас — это задание параметра index числом. Если бы можно было задавать вместо числа конкретный столбец вспомогательной таблицы по имени, было бы гораздо проще, ведь тогда нам не важно, какой он по счету. И для этого есть несложное решение — мы можем добиться нужного нам результата, заменив числовой index на функцию MATCH. Эта функция возвращает порядковый номер заданного значения в заданном диапазоне. При этом значение для поиска можно указать текстом, а это как раз то, что нам нужно.
Результирующее отображение нужного параметра теперь можно сделать визуально более понятным, ведь забираем мы его через функцию, в которой можно написать имя этого параметра. В нашем случае во вспомогательной таблице параметр называется DPS, и итоговый результат будет следующим:
Стоит обратить внимание, что задаваемые диапазоны в этих функциях разные. В VLOOKUP используется вся таблица, в которой происходит поиск. В функции MATCH используется только та часть, в которой мы ищем название параметра, то есть — строка с заголовками параметров, по которым происходит поиск.
Итак, наша формула стала длиннее и на первый взгляд сложнее, но теперь мы можем добавлять любые новые параметры и менять их местами во вспомогательной таблице — в основном балансе от этого ничего не сломается.
Первая сложность преодолена. Что еще мы можем улучшить в нашей структуре?
Подводные камни VLOOKUP — фиксированные диапазоны
Еще одна проблема, похожая на предыдущую, касается задаваемого в VLOOKUP диапазона поиска. Этот диапазон нужно указывать конкретным адресом из вспомогательной таблицы, и это мешает добавлению в нее новых строк и столбцов за пределами диапазона. Новые строки мы добавляем каждый раз, когда хотим добавить новую единицу оружия, а новые столбцы — когда появляются новые настраиваемые параметры. В итоге получается, что когда мы добавляем новое оружие в конец нашей настроечной таблицы, нам нужно увеличить на строку наш диапазон поиска в основном балансе.
Со столбцами это решается достаточно просто: мы можем задавать диапазоны внутри VLOOKUP и MATCH с запасом, вообще не используя буквы столбцов.
Так выглядит подобная формула в основной таблице:
Пунктиром выделены диапазоны, на которые она смотрит с помощью VLOOKUP и MATCH во вспомогательной таблице. Эти диапазоны не зависят от того, сколько столбцов используется.
Для строк это тоже можно решить одной хитростью: новые строки вспомогательной таблицы добавляются не в конец нужного диапазона, а вставляются между уже имеющихся строк. То есть, добавляя новую пушку во вспомогательную таблицу, мы ее записываем не под последней пушкой, а между последней и предпоследней, вставляя между ними новую строку.
В Google Sheets вставка строки или столбца посреди диапазона автоматически расширяет этот диапазон во всех местах, где он используется. Но для некоторых случаев это не очень удобно — к тому же, об этом нужно постоянно помнить. Кроме того, для каждой новой логической части вспомогательной таблицы нам нужно использовать свой отдельный диапазон. Поэтому, несмотря на то, что эти способы работают, при длительной работе с вспомогательными таблицами они становятся довольно-таки неудобными. Что же можно сделать, чтобы это исправить?
Функция FILTER
Мы определились, что основное неудобство для нас — это жестко заданные диапазоны. Но на самом деле функционал таблиц позволяет свести разные диапазоны к одной понятной универсальной записи. Для этого мы будем использовать функцию FILTER и пару дополнительных технических полей во вспомогательной таблице.
Функция FILTER позволяет нам отфильтровать любой диапазон по заданным условиям. При этом в качестве результата она возвращает полноценный диапазон, который можно подставлять в другие функции. Например, если мы хотим вытащить из основной таблицы баланса только ту часть, в которой пушки имеют DPS > 50, это будет выглядеть следующим образом:
При этом нам не обязательно фильтровать всю таблицу — главное, чтобы количество строк в искомом диапазоне и диапазоне условия было одинаковым.
Условий может быть несколько — например, если мы хотим вернуть диапазон названий оружия, оставив в нем только оружие с DPS > 50 и параметром StarterGear со значением false, это будет выглядеть так:
Но вернемся к нашей проблеме: как нам сделать VLOOKUP более удобным?
Для этого мы добавляем в нашу вспомогательную таблицу служебный столбец, обозначающий тип диапазона, и будем использовать фильтрацию по нему. В качестве диапазона поиска мы будем использовать всю нашу таблицу независимо от числа строк — от столбца A до Z. А в качестве параметра для фильтрации используем столбец с нашим новым параметром, разместим его в столбце B и назовем Tag.
Для того, чтобы приблизить наш пример к реальности и немного разнообразить его, добавим еще одну вспомогательную таблицу для случая, когда нужно использовать какие-то коэффициенты, исходя из редкости оружия. Так это будет выглядеть:
Теперь, если мы хотим использовать в нашей основной таблице какой-то параметр вспомогательной таблицы, обратимся к нему с использованием VLOOKUP, MATCH и FILTER следующим образом:
Да, формула стала еще длиннее и на первый взгляд из-за этого читается хуже, но на самом деле теперь мы с первого взгляда на нее можем понять, что в ней происходит. Если описать логику работы этой формулы словами, получится так: «Найди A2 во вспомогательной таблице с тегом weapons_battle и верни значение параметра DPS для него».
При этом теперь мы можем добавлять сколько угодно таблиц, помечая их уникальным тегом. Вот как это выглядит с подсветкой Google Sheets для нашего подсчета DPS:
На самом деле мы не исправили нашу проблему до конца, ведь диапазон в функции MATCH все еще задан конкретной строкой. А это значит, что нам нужно задавать его вручную для случаев, когда мы хотим использовать значения из вспомогательной таблицы weapons_economy или из нашей новой таблицы weapons_rarity. Чтобы не думать и об этом, сделаем аналогичный служебный столбец для заголовков наших вспомогательных таблиц. Чтобы не путаться, будем использовать тот же тег, который мы уже использовали. Так это будет выглядеть во вспомогательной таблице:
А так изменится основная формула:
В таблице баланса она будет выглядеть следующим образом:
Формула очень длинная, но, зная принципы ее работы, ее все еще легче понять, чем при задании диапазонов по отдельности. К тому же, мы можем смело растягивать ее на весь столбец с балансом и быть точно уверенными в том, что все данные правильно выгрузятся в основную таблицу, которую мы планируем загружать на сервер и использовать в игре.
Этой базовой структуры должно быть достаточно, чтобы удобно хранить данные, манипулировать ими и добавлять новые вспомогательные таблицы, соблюдая разметку вспомогательной таблицы с отдельными тегами для строк с данными и заголовков. Но эта запись все-таки довольно длинная, особенно если в основном балансе используются более сложные формулы — например, когда нам нужно забрать из вспомогательной таблицы два разных параметра и перемножить их в основном балансе. Длинную запись сложно читать и тяжело объяснять людям, которые впервые сталкиваются с таблицей. Но оказывается, что мы можем сильно упростить ее и сделать более понятной с помощью очень простых доработок.
Доработки структуры
Кроме того, что формула длинная, в ней осталась еще одна незакрытая проблема: нам нужно помнить, что столбец A:A отвечает за теги заголовков, C:C — за теги отдельных таблиц, а вся таблица с данными помещается в B:Z. Но бывают случаи, когда нужно забрать не всю таблицу, а только названия предметов — то есть, столбец B:B. Еще бывают случаи, когда нам перестает хватать таблицы B:Z, так как вспомогательных параметров становится больше.
Чтобы не думать об этом и не запоминать сочетания букв, мы будем использовать именованные диапазоны. Для этого в Google Sheets выделяем нужный диапазон и через меню по правой кнопке выбираем пункт создания именованного диапазона. В правой части экрана появится меню, где мы можем задать названия всем диапазонам, которые нам нужны.
В результате мы можем изменить нашу итоговую формулу так, что в ней не останется практически ни одного не именованного аргумента. Зная общую структуру хранения данных и помня названия диапазонов, подобные формулы можно писать вообще не заглядывая во вспомогательную таблицу. При этом, если нам нужно изменить размер этих диапазонов — например, в случае, когда у нас становится больше параметров, чем букв в английском алфавите, — мы точно так же вызываем меню с именованными диапазонами и делаем это прямо там — например, заменив Calculations!B:Z в диапазоне main на Calculations!B:AZ.
Так будет выглядеть наша формула после добавления именованных диапазонов:
Она стала короче, теперь ее намного проще читать и редактировать, а также использовать в более сложных составных формулах.
Для более живого примера немного усложним исходную задачу и добавим в основную таблицу баланса новое поле RarityPrice. Предположим, что в нем мы решили использовать формулу, которая перемножает стоимость пушки и коэффициент, зависящий от ее редкости. Используем для этого все, что мы создали ранее:
Формулы для извлечения параметров стали компактнее и понятнее: из них исчезли буквенно-численные обозначения диапазонов за исключением A2 (название пушки) и M2 (редкость пушки), необходимых для поиска во вспомогательной таблице. Так мы добились того, чего хотели: основная таблица, которую мы планируем загружать на сервер и использовать в игре, имеет понятные и читаемые формулы, которые можно растягивать по всему столбцу при добавлении новых пушек. При этом все расчеты вынесены в отдельную таблицу, которую мы можем смело редактировать.
Такую структуру и подход я использую до сих пор как для хранения расчетов баланса, так и для других калькуляторов, которые требуют вспомогательных данных. Есть еще несколько хитростей, которые используются точечно, но это достаточно частные случаи, поэтому я не буду разбирать их подробно.
Надеюсь, что эта статья поможет вам найти свой подход к таблицам. Возможно, кто-то из вас сможет предложить другие улучшения структуры или свой вариант, решающий большее число задач. В любом случае, я буду рад любой критике. Если есть вопросы, обязательно задавайте их в комментариях, постараюсь на все ответить.
yatanai
Надеюсь в реальном проекте люди будут учитывать не только параметры но и статистику попаданий в формулах + % от случайных попаданий (когда ты случайно отыгрываешь лучше чем играешь) + расчётную аудиторию игры, а не — «пушка стреляет, урон 100...(после тестеров) Не 100 много, поставлю 95».
А то потом оказывается что для большей массы игроков этот баланс работает, а у кого-то чей % попаданий выше 30% (для данного оружия) и начинает люто всех нагинать.
AndyWhitedondi Автор
Тут все сильно зависит от проекта. Есть игры с совсем простыми математическими взаимодействиями, например карточные игры, некоторые тактики (по-моему в Баннер Саге взаимодействие было чисто математическим, но я могу ошибаться), там достаточно простых параметров. А так все верно, баланс почти всегда исправляется под результаты аналитики по реальной базе игроков.
yatanai
Ну знаешь, простая математика тоже у большинства сбоит, и даже считается нормой делать типо — «если все имба, то никто не имба»,«Если он имба, то против имбы должна быть другая имба»(популярная «хардстоун» этим вечно грешила) Единственное где почти не косячат это — казуалки и остальная половина от сингл, ибо там цель просто почилить\приятно убить
геймпадвремя. А почему «остальная»? Потому что в остальных использую рандом и не балансят его, думаю прикольно чуствовать когда рандомные криты у врагов вылетают на 0-30% хп а у тебя на 70-100% хп ибо НУ ТАК ПОЛУЧИЛОСЬ, ПЕРЕЗАПУСТИ МБ ПО ЛУЧШЕ БУДЕТ.Не знаю, про какую именно ты базу говоришь? Как бы ты можешь балансить под про игроков, тоесть почти идеальный мат баланс, или под обычных людей, или ты можешь высчитать сложные формулы найдя что-то по середине. Думается мне что многие игры смотрят на обычных людей ибо они приносят деньги, а не про игроки
потому пускай у них сгорает от дизбаланса.PS: Я лишь про игрок и немного программист который делал игры для друзей по фану. А так на fpga занимаюсь.
AndyWhitedondi Автор
Простую математику я имел в виду в плане «У него 10 атаки, у меня 15 хп, он меня атаковал — у меня осталось 5 хп», без какого-то учета внешних факторов, типа точности, сложности прицеливания, разброса, итд. Я тут не хочу лезть в дебри именно порядка и способа расчетов, потому что это не тема статьи и в каждом проекте это делается по-своему.
По поводу рандома — рандом в большинстве случаев используется подкрученный, иначе получается как раз то, что ты описал. Честный рандом для игроков чаще всего выглядит нечестным.
По поводу баланса под разных игроков — да, забалансить под всех идеально не получится. Чаще всего этого и не требуется. Анализ базы игроков позволяет разбить их на когорты и скорректировать цифры по изначальным требованиям от баланса для каждой из когорт. В итоге для каких-то пушек может получиться что-то посередине, для каких-то больше упор на скиллкап и при высоком скилле игрок будет нагибать, а при низком — наоборот. Для стартовых пушек проектируются простые механики и консистентный баланс без скиллкаповых выбросов, для плавного ввода в игру. При этом стартовые пушки могут иметь низкий кап урона, чтобы побудить игроков менять пушку на более сложную и интересную. Опять же, тут очень много разных подходов и в зависимости от проекта и изначальных целей используются разные способы.
Про-игроки как раз часто приносят больше денег, чем основная масса, потому что это игроки, которые с большей вероятностью будут покупать новый контент и пользоваться новыми механиками. Плюс среди про-игроков гораздо больше элемент соревновательности и они готовы тратить больше, так как уже вложились. Это на самом деле касается даже игр без прямой покупки мощности, если кто-то играет часто и много — он скорее всего рано или поздно и скинов себе накупит больше, чем средний игрок.
yatanai
О, вполне себе профессиональный ответ, надеюсь вы делаете хорошие игры, ахах
Как по мне их приносят больше «заинтересованные игроки», у меня много знакомых на про, да они вкладывают деньги, но пропорционально своему времени и только на свои мейны. (Один задротит уже лет 10, и вложил в свою фармилку около 30К, что в пересчёте примерно 3.5$ в месяц, что эквивалентно подписке)
Но большую часть знакомых, которые даже и близко не к «про», завлекает именно красивости игры или её приятность. Самый точный для меня пример, про «мою» лигу лигенд. Много людей кто вкладывает туда деньги обычно «завлечены ей» играя раз в день или два постоянно и после этой стадии реагируют — «новый красивый скинчик, уву. Как задонить?». И тратят они в разы больше в пересчёте на месяц чем мои знакомые из ранга платины-алмаза. А в процентном соотношении про игроков от обычной массы (для лиги это примерно 98\2) такое утверждение кажется странным… То что они стабильно донатят? Ну мб, но другие типы игроков донатят больше.
Кхм, НО, то что про игроки завлекают других игроков, это да, согласен. Наблюдая «как вот тот чувак делает непонятную комбуху» хочется повторить и идёшь играть а потом и задонатить пока тренируешься можно. (это не про себя, я редко доначу)
UPD: Я вспомнил что можно дизайнерски обыграть средний ранг игры как про побуждая игрока к покупке, в той же лиге ранг «серебро-золото» это лишь зелёные нубасы которые просто научились играть, а уже считают себя про и активно играют увеличивая общие шансы задонатить в игру. «Разница между платиной и золотом огромная» — сказал мне один не скиловый друг из золота, а ведь там есть ещё и ещё ранги выше…
PS: И не буду лукавить, я знаком лишь с «небольшим» количеством онлайн игр, в основном файтинги и МОBA, и лишь малую часть шутеров.