Автоматизация или смерть: как управлять тысячами единиц игрового контента с помощью гугл-таблиц
Несколько лет назад в нашем онлайн-шутере столкнулись с немного абсурдной проблемой: контента стало так много, что мы уже не могли с ним работать вручную. Сотни единиц оружия, карт, механик, гаджетов и много чего еще — все нужно балансировать между собой, иначе геймплей развалится на части. Можно нанять больше людей, но лучше прибегнуть к алгоритмам, формулам и автоматизации.
Звучит сложно, но на деле хватило обычных гугл-таблиц. Ну, как обычных — с тысячами строк и формулами, которые не помещаются на экран монитора. Пришлось немного повозиться с их организацией, зато теперь всей игрой мы управляем из нескольких документов.
Под катом на примерах расскажу, как автоматизировать работу с контентом, если в проекте его уже свыше тысячи единиц, как внедрить новые механики и фичи, не ломая при этом баланс и игровой опыт, и даже, как избавить QA-отдел и программистов от лишней работы.
Баланс в онлайн-шутере крайне важная штука. Нужно, чтобы геймплей нравился всем игрокам — не было тех, кто не может вообще никого убить или, наоборот, тех, кто доминирует и фрустрирует всех остальных.
На первых годах жизни Pixel Gun 3D использовалась простая схема: весь контент добавлялся и редактировался вручную. Нужно поменять урон пушке? Заходишь в Unity, открываешь нужный файл и правишь руками. Дело на пару минут.
Потом индустрия начала переходить на freemium. От рынка отставать не стали и проект тоже изменил модель монетизации — переработали экономику, прогрессию и многое другое, но сейчас не об этом. В результате DAU полетело вверх, а мы усиленно начали добавляли больше режимов, оружия, персонажей и так далее.
Проблемы начались, когда только оружия в каждой категории накопилось по 20–30 единиц, появились гибридные механики, а данных стало так много, что их уже невозможно было разобрать руками. Вместе с этим повысился порог входа в процессы для новых сотрудников — только что пришедший в команду геймдизайнер не мог ориентироваться в контенте, так как банально не знал его на уровне названий.
Нужно было глобально что-то менять.
Сначала просто хотели сэкономить немного времени на балансировке контента, а в итоге автоматизировали большинство процессов, избавились от лишней ручной работы и разгрузили сотрудников. И все это с помощью гугл-таблиц.
Из гугл-таблиц в Unity
В первую очередь стали разбираться, как упростить работу с данными и можно ли их загружать в Unity из обычных гугл-таблиц. Оказалось, можно. Это был ключевой момент и первый шаг на пути к автоматизации — назвали его удаленным контролем.
Для получения данных с таблиц мы используем Google Apps Script. Первое время заводили отдельные скрипты на каждую таблицу, в которых обрабатывали данные в JSON. Затем, получая в редакторе JSON, применяли их по назначению.
Сейчас пришли к тому, что опубликовали один скрипт. С его помощью получаем сырые данные с таблиц и уже в проекте конвертируем их в необходимый формат на С#. Этот подход более быстр и удобен.
Так выглядит наш скрипт:function doGet(e)
{
if (e === undefined || e.parameter === undefined)
{
return FailWithMessage("nullable parameters");
}
var tableId = e.parameter["table"];
var listName = e.parameter["list"];
if (listName !== undefined && listName !== "" && listName !== "null")
{
var startRow = parseInt(e.parameter["startRow"]);
var startColumn = parseInt(e.parameter["startColumn"]);
var numRow = parseInt(e.parameter["numRow"]);
var numColumn = parseInt(e.parameter["numCol"]);
return GetSigleList(tableId, listName, startRow, startColumn, numRow, numColumn);
}
else
{
return GetAllTable(tableId);
}
}
function GetSigleList(tableId, listName, startRow, startColumn, numRow, numColumn)
{
var ss = SpreadsheetApp.openById(tableId);
if (ss == null)
{
return FailWithMessage("table with name: " + tableId + "not found");
}
var sheet = ss.getSheetByName(listName);
if (sheet == null)
{
return FailWithMessage("list with name: " + listName + "not found");
}
if (numRow < 1) numRow = sheet.getLastRow();
if (numColumn < 1) numColumn = sheet.getLastColumn();
var range = sheet.getRange(startRow, startColumn, numRow, numColumn);
var data = range.getValues();
var str = JSON.stringify(data);
var resultObject = {
"resultCode": 2,
"message": str
};
var result = JSON.stringify(resultObject);
return ContentService.createTextOutput(result);
}
function GetAllTable(tableId)
{
var ss = SpreadsheetApp.openById(tableId);
if (ss == null)
{
return FailWithMessage("table with name: " + tableId + "not found");
}
var result = {};
var listModes = ss.getSheets();
for(var i = 0; i< listModes.length; i++)
{
var sheet = listModes[i];
var sheetName = sheet.getSheetName();
var range = sheet.getRange(1, 1, sheet.getLastRow(), sheet.getLastColumn());
var data = range.getValues();
result[sheetName] = data;
}
var str = JSON.stringify(result);
var resultObject = {
"resultCode": 2,
"message": str
};
var result = JSON.stringify(resultObject);
return ContentService.createTextOutput(result);
}
function FailWithMessage(message)
{
var result = {
"resultCode": 1,
"message": message
};
var str = JSON.stringify(result);
return ContentService.createTextOutput(str);
}
Для его использования необходимо создать новый скрипт, вставить содержимое из примера, опубликовать и в разрешениях указать, что запускать могут все, даже анонимные пользователи.
После публикации получится ссылка такого формата:
https://script.google.com/macros/s/WwlCZODTDRXJaHhdjfwFRcKtHRQOHqzYisjndduZzDihMpXehLrNxdi/exec
Ее нужно использовать для запуска скрипта. Чтобы скрипт знал, с какой таблицы нужны данные, в get-запрос подставляем ID таблицы. Получить его можно из URL таблицы. Например, в https://docs.google.com/spreadsheets/d/example_habr/edit#gid=0, ID будет — example_habr.
Если нужно получить данные с конкретного листа, можно в запрос добавить его название — тогда данные вернутся только по нему. То же самое и с диапазоном интересующих ячеек.
Полный запрос будет выглядеть так:
https://script.google.com/macros/s/WwlCZODTDRXJaHhdjfwFRcKtHRQOHqzYisjndduZzDihMpXehLrNxdi/exec? table=example_habr&list=MyList&startRow=1&startColumn=2&numRow=10&numCol=5
У Google есть ограничения на количество запросов и время выполнения. Поэтому нельзя использовать этот инструмент, например, для подтягивание конфигов напрямую в клиент у пользователей — при превышении лимита скрипт начнет выдавать ошибку.
Теперь большинство параметров контента редактируется не внутри проекта, а во вне. Движок просто получает данные из таблицы в реальном времени. Например, нужно всем пушкам добавить 5 урона: вбиваем в ячейку, протягиваем по столбцу и нажимаем «загрузить». Готово —все данные ушли в клиент. По сути, мы с помощью нескольких гугл-таблиц управляем всей игрой.
Сейчас таких таблиц баланса уже много, все с закрытыми доступами на нужных специалистов. Отдельно по оружию, питомцам, гаджетам, экономике уровней и другим вещам. Но принцип работы один — ничего не делается по отдельности руками на клиенте.
Но мы пошли дальше.
Из Unity в гугл-таблицы
Когда пользователь играет матч, вся статистика отправляется на наш сервер: по ID игрока в базу данных сохраняется, с какой пушкой он ходил, какие результаты показал и прочее. Таких необработанных данных очень много по каждому пользователю и единице контента: киллрейты, результаты матчей, частота взятие пушек и другое. Их мы давно использовали для аналитики —, но все было вручную, долго и сложно.
Почему бы не использовать таблицы не просто для загрузки данных в Unity, но и в обратную — для обработки собираемой статистики и метрик или поиска проблемных мест? Да и вообще для генерации самого контента.
Начали с простеньких задачек. Например, мы часто превышали лимит в 500 символов на описание апдейта в Google Play. Стор такое отклоняет, нужно переписывать и отправлять заново. Задались вопросом, а есть ли формула для подсчета символов в ячейке? Разумеется, в гугл-таблицах большой перечень базовых формул, которые можно комбинировать как угодно и решать практически любые задачи. Написали в ячейке, чтобы описание апдейта автоматически проверялось на количество символов — =ДЛСТР (номер ячейки). Теперь проблемы нет.
Гугл-таблицы закрывают большинство наших потребностей. Формулы состоят из простых операторов, но сочетания такие, что иногда не помещаются на экран 27-дюймового монитора. Есть отдельные специалисты, которые их составляют, знают разные фишки, специальные округления, сложные погрешности, выборки.
В гугл-таблицах огромное количество функций, ознакомиться со всеми можно на сайте саппорта Google.
Например, решили составить коллекции для галереи оружия (это такие подборки оружия по схожим признакам: цвет, сеттинг, сезон). Чтобы составить одну коллекцию, нужно оценить визуал, смотрятся ли они вместе, схожий ли у них класс и другие детали. Для этого сделали таблицу, в которую из базы данных выгрузили список всех пушек, написали формулу и подгрузили превьюшки, с которыми можно визуально работать. А уже из таблиц по балансу подтягиваем названия, теги и остальную информацию.
В результате получили таблицу, которую можно подгрузить в любую другую и взаимодействовать с пушками наглядно — они автоматом подтягиваются по тегу. Например, по тегу Weapon970 сразу подгружается ее название, превью и так далее. С этим уже можно спокойно работать.
Еще пример. Решили поменять баланс дробовиков (скорострельность, дальность и так далее). Надо понимать, что механик много, а позиций еще больше. Каждую нужно учитывать в формулах в виде отдельных коэффициентов. Геймдизайнер не может зайти и перепечатать параметры руками — их придется менять у каждого дробовика, которых может быть несколько десятков.
В формулах есть множество коэффициентов. Есть даже коэффициент «крутости» — добавляем его, если, например, текстура пушки получилась особенно красивой. В таком подходе мы уже думаем через переменные, а не через частные случаи. Уже не хочется вручную переделывать руками 40 позиций, хочется иметь один коэффициент, который сделает все сам.
В итоге таблицы экономят колоссальное количество времени. На подготовку уйдет максимум пару часов (если совсем что-то нестандартное), зато потом можно ворочать огромным количеством данных или сущностей. Вручную те же задачи могут спокойно занять неделю.
Где используем автоматизацию
Мы применяем ее практически везде, где это возможно. И всегда ищем новые способы, чтобы сделать себе жизнь проще и переложить часть работы на формулы.
Балансировка контента
Самые базовый способ применения, о котором уже упоминали выше. Так мы ищем проблемные места, добавляем новый контент, не ломая старый, нерфим или бафаем оружие и так далее.
Если есть ошибка в балансе пушки, то апдейтим ее прямо на сервере, после того, как пересчитали в таблице. Такие правки стараемся переводить в формат удаленного контроля, чтобы не выпускать отдельные патчи в сторы.
Например, если на сервере не контролируется размер обоймы, а в коде добавили лишний нолик и у пулемета стало 50 тысяч патронов — без патча такое не пофиксить. Пока он выйдет, пулемет купит большинство, баланс посыпется, появится куча негатива в соцсетях. Но если сделать это на сервере, то достаточно убрать одну цифру — все наладится, а игроки даже не заметят. По максимуму вводить удаленный контроль — очень хорошая практика.
Генерация сущностей
Автоматизацию можно использовать не только для баланса, но и для генерации нового контента. Так, в каждом батлпассе у нас есть порядка 140 разных челленджей (например, сыграть 10 игр в определенном режиме или убить 30 противников из пистолета). Каждый раз придумывать это вручную — долго и нудно, поэтому сделали генерацию. Собрали подробный список условий и прямо из гугл-таблицы создаем квесты — теперь одна формула придумывает нам все задачи на каждый сезон.
Естественно, со сгенерированным пулом работает уже человек и настраивает кривую сложности. Игрок должен плавно войти в ивент, тем самым повысив конвертацию в платеж или в более длинную игровую сессию.
Подобным образом мы генерируем задачи и для клановых войн.
Пример формулы:=ifs (I2=«EndMatch»; ifs (AE2<=TasksData!$O$34+TasksData!$N$34;"TeamDuel";AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34;ЕСЛИ(A2=0;"TeamDuel";"Spleef");AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34;"Duel";AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34;ЕСЛИ(A2=0;"Duel";"BattleRoyale");AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34+TasksData!$J$34;ЕСЛИ(A2=0;"TeamFight";"DeadlyGames");AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34+TasksData!$J$34+TasksData!$I$34;"CapturePoints";AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34+TasksData!$J$34+TasksData!$I$34+TasksData!$H$34;"FlagCapture";AE2<=TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34+TasksData!$J$34+TasksData!$I$34+TasksData!$H$34+TasksData!$G$34;"Deathmatch";AE2>TasksData!$O$34+TasksData!$N$34+TasksData!$M$34+TasksData!$L$34+TasksData!$K$34+TasksData!$J$34+TasksData!$I$34+TasksData!$H$34+TasksData!$G$34; «TeamFight»); I2=«killPlayer»; ifs (AE2<=TasksData!$N$35;"TeamDuel";AE2<=TasksData!$N$35+TasksData!$L$35;"Duel";AE2<=TasksData!$N$35+TasksData!$L$35+TasksData!$K$35;ЕСЛИ(A2=0;"TeamFight";"BattleRoyale");AE2<=TasksData!$N$35+TasksData!$L$35+TasksData!$K$35+TasksData!$J$35;"DeadlyGames";AE2<=TasksData!$N$35+TasksData!$L$35+TasksData!$K$35+TasksData!$J$35+TasksData!$I$35;"CapturePoints";AE2<=TasksData!$N$35+TasksData!$L$35+TasksData!$K$35+TasksData!$J$35+TasksData!$I$35+TasksData!$H$35;"FlagCapture";AE2<=TasksData!$N$35+TasksData!$L$35+TasksData!$K$35+TasksData!$J$35+TasksData!$I$35+TasksData!$H$35+TasksData!$G$35;"Deathmatch";AE2>TasksData!$N$35+TasksData!$L$35+TasksData!$K$35+TasksData!$J$35+TasksData!$I$35+TasksData!$H$35+TasksData!$G$35; «TeamFight»); I2=«killPet»; ifs (AE2<=TasksData!$G$36;"Deathmatch";AE2>TasksData!$G$36; «TeamFight»); I2=«killPlayerThroughWall»; ifs (AE2<=TasksData!$I$37;"CapturePoints";AE2<=TasksData!$I$37+TasksData!$H$37;"FlagCapture";AE2<=TasksData!$I$37+TasksData!$H$37+TasksData!$G$37;"Deathmatch";AE2>TasksData!$I$37+TasksData!$H$37+TasksData!$G$37; «TeamFight»); I2=«killPlayerFlying»; ifs (AE2<=TasksData!$I$38;"CapturePoints";AE2<=TasksData!$I$38+TasksData!$H$38;"FlagCapture";AE2<=TasksData!$I$38+TasksData!$H$38+TasksData!$G$38;"Deathmatch";AE2>TasksData!$I$38+TasksData!$H$38+TasksData!$G$38; «TeamFight»); I2=«ramEscort»; «Siege»; I2=«escortDestroyGate»; «Siege»; I2=«winBrNoChest»; «BattleRoyale»; I2=«crashChest»; «BattleRoyale»; I2=«winBrInParty»; «BattleRoyale»; I2=«flagCapture»; «FlagCapture»; I2=«pointCapture»; «CapturePoints»)
Пример таблицы с вводными для генератора:
Пример формулы (в ней представлена часть с генерацией описаний задач, Key_номер — это ключи локализаций, из которых составляются задачи):
=IFS (I2=«endMatch»;(ЕСЛИ (T2=0; «Key_7220»; «Key_7234»)); I2=«killPet»; ЕСЛИ (W2=«None»; «Key_7228»; «Key_7224»); I2=«killPlayer»; ЕСЛИ (Q2=1; «Key_7227»;(ЕСЛИ (W2=«NONE»; ЕСЛИ (R2=1; «Key_7232»; ЕСЛИ (S2=1; «Key_7233»; «Key_7221»)); «Key_7216»))); I2=«killPlayerFlying»; «Key_7225»; I2=«killPlayerThroughWall»; «Key_7226»; I2=«ramEscort»; «Key_7235»; I2=«escortDestroyGate»; «Key_7236»; I2=«winBrNoChest»; «Key_7229»; I2=«crashChest»; «Key_7230»; I2=«winBrInParty»; «Key_7231»; I2=«flagCapture»; «Key_7237»; I2=«pointCapture»; «Key_7238»; I2=»;»)
И еще более эпичная, уже проходящая через ячейки параметров и рандомайзеров:
=ifs (L3=«DeadlyGames»;0; L3=«BattleRoyale»;0; L3=«TeamDuel»;0;1=1; ЕСЛИ (I3=«killPlayer»; ifs (A3=0; ЕСЛИ (СЧЁТЕСЛИ ($I$2: I2; «killPlayer»)>=(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38)*6; ЕСЛИ (СЧЁТЕСЛИ ($I$2: I2; «killPlayer»)<(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38+TasksData!$B$47)*6;1;0);0);A3=1;ЕСЛИ(СЧЁТЕСЛИ($I$2:I2;"killPlayer")>=(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38+TasksData!$B$47+TasksData!$B$48+TasksData!$C$34+TasksData!$C$36+TasksData!$C$38)*6; ЕСЛИ (СЧЁТЕСЛИ ($I$2: I2; «killPlayer»)<(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38+TasksData!$C$34+TasksData!$C$36+TasksData!$C$38+TasksData!$B$47+TasksData!$B$48+TasksData!$C$47)*6;1;0);0);A3=2;ЕСЛИ(СЧЁТЕСЛИ($I$2:I2;"killPlayer")>=(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38+TasksData!$B$47+TasksData!$B$48+TasksData!$C$34+TasksData!$C$36+TasksData!$C$38+TasksData!$C$47+TasksData!$C$48+TasksData!$D$34+TasksData!$D$36+TasksData!$D$38)*6; ЕСЛИ (СЧЁТЕСЛИ ($I$2: I2; «killPlayer»)<(TasksData!$B$34+TasksData!$B$36+TasksData!$B$38+TasksData!$B$48+TasksData!$C$34+TasksData!$C$36+TasksData!$C$38+TasksData!$D$34+TasksData!$D$36+TasksData!$D$38+TasksData!$B$47+TasksData!$C$47+TasksData!$C$48+TasksData!$D$47)*6;1;0);0));0))
Симуляция процессов
Другой пример. Добавляем в игру сундук с предметами: игрок открывает и получает приз. У сундука есть множество правил, как и что будет выпадать. Можно написать код и отдать QA-отделу, чтобы это проверить, но есть способ лучше. Прямо в гугл-таблице делаем симулятор дропа: вносим правила, условия, открываем 1000 сундуков и корректируем всю модель под нужный исход. Для этого не нужен ни код, ни программист — достаточно таблицы и самой концепции.
Пример части вводных данных:
Пример использованных формул в ячейках:
=СЛУЧМЕЖДУ (1;100)
=СРЗНАЧ (13;15)*6
=СУММ (B4: F4)
=IFS ($A32<=G32;"Mythic";$A32<=F32;"Legend";$A32<=E32;"Epic";$A32<=D32;"Rare";$A32<=C32;"Common")
Сами по себе формулы простые, но при правильном построении связей ячеек и порядка выполнения действий — получается нужный результат.
Пример результатов дропа по номерам открытий:
Разумеется, в самом проекте все пишет программист, но проверить это можно заранее. По сути все, что есть в игре, можно симулировать в таблице. И если результаты устраивают — передавать в разработку.
Воркфлоу создания таблицы
Рассказать, как именно происходит работа с данными и создание таблиц, лучше всего на примере. Допустим, мы решили выяснить, какие пушки сейчас самые популярные в игре. А для этого нужно подтянуть множество дополнительных данных.
Расскажу подробнее по шагам (цифры в примере изменены в рамках конфиденциальности):
Эти же данные можно брать из таблицы баланса. Разница в том, что таблица баланса дает актуальные параметры (и не дает метрики), а на сервере информация хранится с пометкой даты — ее можно выгрузить за нужный период (и вместе с метриками).
2. Затем создается гугл-таблица, в которой будет происходить основная работа. И в нее загружается полученный CSV-файл.
3. Для удобства из другой таблицы с помощью формулы подтягиваются картинки пушек. Арты предварительно загружены на сервис для получения прямой ссылки в формате (важно именно расширение в конце ссылки):
http://адрес_сервиса/путь/номер/имя картинки.jpg
Для добавления в таблицу используем простую функцию:
=IMAGE («https://files.fm/u/wdrhemgnk#/view/special_offer_pixelman_reward_big.png»)
Пример формулы для подтягивания артов с перебором:
=ЕСЛИ (ЕНД (ВПР (A4; importrange («имя_таблицы»; «Лист1»! B: F»);5; ЛОЖЬ))=ЛОЖЬ; ВПР (A4; importrange («имя_таблицы»; «Лист1»! B: F»);5; ЛОЖЬ); ЕСЛИ (ЕНД (ВПР (A4; importrange («имя_таблицы»; «Лист1»! C: F»);4; ЛОЖЬ))=ЛОЖЬ; ВПР (A4; importrange («имя_таблицы»; «Лист1»! C: F»);4; ЛОЖЬ); ЕСЛИ (ЕНД (ВПР (A4; importrange («имя_таблицы»; «Лист1»! D: F»);3; ЛОЖЬ))=ЛОЖЬ; ВПР (A4; importrange («имя_таблицы»; «Лист1»! D: F»);3; ЛОЖЬ); ЕСЛИ (ЕНД (ВПР (A4; importrange («имя_таблицы»; «Лист1»! E: F»);2; ЛОЖЬ))=ЛОЖЬ; ВПР (A4; importrange («имя_таблицы»; «Лист1»! E: F»);2; ЛОЖЬ); «НИМА ТАКОГО»))))
Теперь можно работать не с абстрактными строками, а наглядно. Если пушек 30 штук, можно зайти в игру и посмотреть, но когда их 700 — так уже не сделаешь.
Важный момент: после подгрузки данных мы их вырезаем (ctrl+x) и вставляем без привязки к формуле (ctrl+x+v). Формулу затем удаляем, иначе после каждого обновления страницы она будет пересчитывать все строки. В данном случае более 800.
Далее одну базу можно использовать для всех задач, где требуется отображение картинок оружия.
4. При желании можно выгрузить название пушки в игровом магазине (а также все необходимые для задачи параметры, например, дополнительные свойства).
5. Также используем форматирование ячеек. Оно может выполнить множество полезных преобразований. Например, покрасить ячейки, в которых значение больше 2 или меньше 1.
В применении к данным о киллрейте пушки (соотношение числа убийств из пушки к смертям игрока с ней) — видно превышение обозначенного предела, а также низкие значения, которые явно являются поводом передать эти данные в отдел балансирования.
6. Добавляем столбец с категориями оружия (у нас их 6) и прибегая к тому же условному форматированию — красим каждый в свой цвет. Стало видно, что в топе нет пушек категории Special, что уже наталкивает на анализ механик этой категории оружия.
В итоге, чтобы собрать такую таблицу с категориями, свойствами, превью и прочим — пришлось вытащить эту информацию из нескольких разных мест. И на это ушло около 15 минут, а не весь день. Так как формулы операций типовые, а таблицы с рендерами и данными есть уже готовые.
Теперь можно сортировать, строить графики, добавлять продажи или другую информацию, чтобы быстро и обоснованно сделать нужные выводы или проверить теории.
Для наглядности на основе данных можно построить и график. Делается это через простую операцию добавления: добавить → диаграмма (сразу можно выделить нужный диапазон ячеек).
График популярности оружияЖизнь после автоматизации
На самом деле сплошные профиты. Проблемы дисбаланса значительно сократились, стало проще следить за контентом и предугадывать возможные проблемы, потому что общая картина всегда перед глазами, а проблемные места подсвечиваются.
Теперь легче что-то поменять, перезалить или вообще обновить целый класс оружия, в котором 100+ позиций. Достаточно вбить новые параметры — все автоматически и с учетом механик пересчитается под норму (а часть параметров пересчитаются сами под новые вводные).
Мы избавились от мелкого ручного менеджмента, освободили время сотрудников для более интересных задач, сняли дополнительную нагрузку на технический отдел и теперь программисты не отвлекаются от написания кода. И что еще важно — новички в команде теперь легко вливаются в процессы и намного быстрее разбираются с проектом на реальных задачах.
Переход к автоматизации — непрерывный эволюционный процесс, и во многом непаханное поле. Теперь постоянно думаем, как оптимизировать ресурсы и упростить задачи, пробуем новые варианты, формулы и всегда ищем более удобные решения. Например, сейчас работаем над добавлением всех этих процессов прямо в админку.
P.S. Данный подход меняет сам принцип работы с различными инструментами. Например, в том же Slack можно видеть BB-коды наглядно с помощью простой команды #: