company_banner

Автоматизация или смерть: как управлять тысячами единиц игрового контента с помощью гугл-таблиц

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

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

    Под катом на примерах расскажу, как автоматизировать работу с контентом, если в проекте его уже свыше тысячи единиц, как внедрить новые механики и фичи, не ломая при этом баланс и игровой опыт, и даже, как избавить 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-коды наглядно с помощью простой команды #:

    Полезные ссылки

    Lightmap
    Разработчик мобильных игр

    Комментарии 21

      0

      Как-то это очень костыльно, это невозможно читать, невозможно поддерживать.


      А вам, фактически, не нужны сложные табличные функции, вам нужен CRUD, совместная работа и взаимосвязи между таблицами. Которые наверняка можно написать проще и чище, чем вот это:


      =ЕСЛИ(ЕНД(ВПР(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; ЛОЖЬ);“НИМА ТАКОГО“))))

      и, тем более, вот это:


      =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))
        +3
        У нас составлен и зафиксирован набор таблиц баланса по разным типам контента: оружие, питомцы, гаджеты и так далее. Большинство данных для настройки и балансировки подтягивается из них как раз с помощью взаимосвязей и перекрестных ссылок. Базовые вещи давно настроены, а если требуется собрать с нуля уникальную таблицу, чтобы проверить какой-то конкретный случай, то это делается за 15-20 минут.
        Что касается сложных функций. Мы используем таблицы не только для баланса, но и для генерации нового контента, симуляции рандомов, дропа и многого другого. И нам для этого не нужно привлекать программистов и делать что-то прямо на проекте.
        А вообще основная идея в том, чтобы максимально все автоматизировать. У нас каждый сотрудник может прийти с предложением сделать проще. Например, если где-то можно написать одну формулу, которая затем сэкономит кучу ручной работы — отлично. Главное, чтобы процесс был автоматизирован и удобен для всех.
          0

          Вы можете обижаться и минусовать, но у меня один клиент недавно как раз просил автоматизацию с основой в Google Sheets. Я сделаль классный импорт, это всё работает -- до поры до времени. Так что отлично себе Но представляю рабочий процесс на этой базе. Вердикт -- это именно костыль , то есть в какой-то момент подломится и подставит.

          Ещё один клиент до сих пор работает небольшим офисом в одной огромной общей Excel-ке. И тоже считает, что процесс налажен, зачем что-то менять? Иногда экселька ломается, начинает весить несколько гигов и её приходится руками восстанавливать. Но процесс налажен...

          В этом опасность костылей, никогда не знаешь, когда с них грохнешься.

            +1
            Помимо создания админки отдел разработки занимается фичами и БД для маркетинга и аналитиков. У них довольно большой загруз, и мы таким образом снимаем часть работы. Для геймдизайнеров это способ самим решать промежуточные задачи, проверять гипотезы и так далее.

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

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

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

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

              Не говоря уж о том, что важная часть инфраструктуры лежит в Google Sheets, что само довольно ненадёжно.

                0
                Первое что приходит в голову при таких задачах — это база данных, таблицы с калькулируемыми полями, триггеры, вью, функции (и хранимые процедуры если хотите). База может (и должна) брать на себя часть бизнес-логики связанной только с данными и не требующая сложных расчетов, более того это позволяет делать полуавтоматическую балансировку (если подключить к данным еще и бд со статистикой), за которой надо только приглядывать одним глазом. Далее фронт на python/django+pandas+matplotlib для реализации простой вебморды поверх всего этого… опционально можно приделать докер. Максимально расширяемо, и легко поддерживать. Я не являюсь фанатом питона, у меня админка сделана на dotnet+extjs, но принцип тот же.
              0

              Соль на спину уж точно. Excel никак не отнять, все привыкли.

          +2
          как управлять.. с помощью гуглотаблиц, напомнило
          image


          :D

          Я думаю, нужен не этот хтонический ужас с формулами, а админпанель с БД внутри, таблицами снаружи, дополнительными проверками на валидность изменений и деплоем через git. Но я не настаиваю ;)
            0
            Разработка такого инструмента влетит в такую копеечку, что только держись. При чем сначала придется выловить из него огромное количество багов, которое в гуглшытсах уже давным-давно выловлено. Зато будет свое.
              0
              Принимали функционал, люди работали в огромных excel таблицах и даже с макросами у них это получалось крайне медленно. Нам понадобилось 1.5 человека на несколько месяцев, на перевод всего этого в БД, создание вебморды, и роботов. По итогу вот уже несколько лет с работой целого отдела справляются два младших сотрудника, которые нужны чтобы обрабатывать не автоматизируемые ситуации, находить агрегировать проблемы и передавать их разработчикам.
                0
                Во времена, когда Delphi было актуально, базовый функционал табличного процессора делался в 2 щелчка мыши. Бросанием на форму компонента с таблицей. Сейчас этот секрет, увы, утрачен.
              +1
              А если гугл что-то у себя улучшит — вы закроетесь?..
                0

                Тоже для себя открыл Excel когда играл в Eve Echoes и нужно было делать промышленный отдел в корпе (нужно было erp+crm+jira). Тоже писали скрипты, кнопки с автоматизацией, вебхуки, интеграция с discord.

                В итоге начали помогать китайцам писать webapp под это дело. Потому что Excel для другого и имеет лимиты.

                Сначала окрылились как вы, всё устраивало, но потом поняли что много воюем с экселем, вместо продуктивности.

                Для тестов шансов дропа, проверки гипотез - он хорош.

                Для управления вашими данными - не советую.

                  0

                  Делали почти так же, но с экспортом в CVS и потом в sqlite для клиента и MySQL для сервера. Все виды связей, валидации, дропдауны, изображения - это был кромешный ад и вереницы скриптов с формулами )

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

                    +1
                    Работа с таблицами у нас это скорее вспомогательный процесс. Он ускоряет создание ТЗ, позволяет проводить предварительные тесты до QA-отдела и прочее. В итоге мы получаем сильный профит на препродакшене.
                    0

                    Жутковато.
                    Примеры формул стоило бы убрать под спойлеры

                      +1
                      Согласен что отвратительная работа одновременно с множеством сущностей подталкивает к таким вот методам. Частично с этим можно бороться с пощью префабов и кастомных редакторов, но напрашивается какой-то простой язык запросов.
                        +7

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

                        Дальнейшая автоматизация нужна (админка и тп), если там кратный профит будет от этого.

                        Кстати, вопрос - а как вы в этой системе решили вопрос с версиями? Как откатиться, как понять в какой версии произошло то или иное изменение?

                          +2
                          Кстати, вопрос — а как вы в этой системе решили вопрос с версиями? Как откатиться, как понять в какой версии произошло то или иное изменение?
                          Это уже решается нашим конфиг-менеджером. При обновлении версии конфига на сервере сохраняется кто, когда и что залил, там же есть инструмент, показывающий разницу. При необходимости всегда можно поднять полную историю изменений.
                            0
                            Все говорят про кратный профит, но никто не смотрит и сравнивает затраты на поддержку и дальнейшее оперирование. Потому что «комфортно».
                            0
                            Бесспорно, гугл таблицы это круто и помогают быстро автоматизировать многие вопросы. при этом дают возможность одновременно использовать их нескольким людям, и бла бла бла
                            НО! Кому вы душу продали чтоб они у вас не тупили ?!)))

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

                            Самое читаемое