Оптимальный процесс разработки онлайн игр

или как заменить 10+ разработчиков всего одним

f103c36865aee39f3f5df565269a8a31.jpeg

Введение в проблему

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

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

Многоплатформенность

Первая мера, которая лежит на поверхности — это использование кросс-платформенного языка программирования. К таковым относятся: C++, TypeScript, Haxe, Kotlin, Rust и другие. Сейчас популярны такие платформы:

  • web (Canvas, WebGL),

  • mobile (iOS, Android),

  • desktop (Windows, Mac, Linux),

  • console (PS, Wii, Xbox).

И если для каждой из них писать отдельный порт игры, то можно разориться, так и не дойдя до релиза. Допустим, одна команда делает веб-версию на TypeScript, другая — iOS и Mac на Swift или Objective-C, третья — Android на Java, четвертая — десктоп и консоли на C++, то мы вынуждены оплачивать до четырех команд по 3–5 человек в каждой. Если они используют разные языки, то и действовать они будут совершенно независимо, повторяя друг от друга. Но если изначально выбрать правильный язык, то нам достаточно всего одной команды, плюс, несколько человек для адаптации под разные платформы. Экономия в несколько раз!

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

Второй вид платформ — это социальные сети и другие площадки со своим API. Для них мы создаем в коде отдельный слой сервисов с единым унифицированным интерфейсом для всех таких платформ. Логика нашего приложения вызывает метод getFriends (), а то как эти друзья будут получены, зависит уже от конкретной реализации данного метода. Тогда, если мы сделали игру для VK и хотим перенести ее еще и на OK, то нам не нужно переписывать приложение, а достаточно всего лишь написать новый класс сервиса и подставить его вместо старого. И что особенно приятно, каждый сервис пишется только один раз и используется во всех последующих приложениях. То есть мы получаем значительное расширение аудитории, начиная уже со второй игры, абсолютно бесплатно.

Слои, модули и команды

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

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

Появление логики клиента из отображенияПоявление логики клиента из отображения

На первом этапе приложение разделяется на слои логики (Logic) и отображения (View). Затем само отображение делится на логику отображения (View Logic) и код непосредственной визуализации (Display). Последний обычно представлен популярными графическими библиотеками высокого уровня: OpenFL, PixiJS, Phaser и другие. 

Сюда же относятся различные редакторы, вроде Adobe Animate и Construct. Так, OpenFL может использовать ассеты скомпилированные в .swf, а PixiJS — ассеты экспортированные с помощью специального расширения Pixi Animate Extension. В результате нам не нужно заниматься хард кодом графики для расстановки элементов на сцене, а можно поручить все непосредственно художникам, аниматорам, левел-дизайнерам. У нас создается четкое разделение труда, и программисты не отвлекаются на суету вокруг графики, а продолжают писать движки для игр.

Далее. Логика отображения оперирует объектами графических библиотек (DisplayObject, Sprite, MovieClip) и состоит на простейшем уровне из базовых UI-компонентов (Button, Label, List, ComboBox), а на более сложном — из скринов, диалогов и панелей. Первые непосредственно манипулируют визуальными объектами, а вторые — объединяют простые компоненты в единое целое и соединяют их с бизнес-логикой. 

Под бизнес-логикой мы понимаем то место, где находятся правила игры и принимаются решения. К ее классам относятся контроллеры и отчасти модели. Отделив логику отображения от бизнес-логики, мы можем подставлять разные правила игры для одной и той же визуальной части. Для этого отображение должно быть реализовано в универсальном виде. Например, для карточных игр компоненты должны просто перемещать карты на столе вне зависимости от того, какой контроллер к ним приставлен: преферанса, девятки или дурака. Тогда достаточно создать новый контроллер с правилами, как мы это увидим ниже, и новая игра готова!  

Общая MVC-схема нашего клиентаОбщая MVC-схема нашего клиента

В этом не сложно убедиться, если провести небольшое исследование игровых жанров, выстроив их по мере увеличения сложности реализации. Обнаруживается, что всех их можно разбить на две основные группы: игры на произвольном поле и игры на упорядоченном поле (условно говоря, поле в клетку). Первые дальше можно разделить на подгруппы: квесты, игры на ставки, карточные. Ко вторым относятся такие игры, как крестики-нолики, шашки, 3-в-ряд, пошаговые стратегии и другие. Нетрудно заметить, что для каждой такой группы можно реализовать один и тот же тонкий клиент! То есть нам достаточно написать клиент для квестов, и останется лишь добавлять классы контроллера, чтобы реализовать новый жанр, вроде поиска предметов, раскраски или одевалки. Логика отображения у них одна и та же.

Классы бизнес-логика не хранят и не оперируют данными напрямую. Для этого существуют модели. Если контроллеру нужно изменить какие-либо данные, он должен вызвать соответствующий метод модели. Отделение данных от остальных частей приложения дает нам возможность управлять целостностью и персистентностью данных в едином месте. Так, все данные можно хранить в общей для нескольких приложений базе данных, вроде Redis, или в виде единого словаря со всеми игровыми сущностями, который можно одним махом периодически сохранять на диск и загружать в случае поломки. Интерфейс модели скрывает от остального приложения все эти детали.

Подробное устройство клиентаПодробное устройство клиента

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

Также перевод всех взаимодействий на команды позволяет легко отслеживать, записывать и с точностью воспроизводить действия игрока и вообще весь игровой процесс. Это может быть полезно не только для игрока, который может посмотреть повтор своей игры и проанализировать ошибки, но и разработчикам при отлове редких багов. Не говорю уже об аналитиках и специалистах по ИИ и нейронным сетям.

Для сетевого режима один раз реализуется прослойка классов-сервисов (Service), задача которых отправлять и получать команды по сети, чаще всего с помощью сокетов или HTTP-запросов. Внутри сервисы состоят из других подслоев: Protocol, Transport, Parser. Если нужно поменять формат данных (JSON, YAML, XML), достаточно заменить парсер; если нужен другой сетевой протокол (HTTP, TCP, UDP), то берется другая реализация траспорта; если необходимо подправить имя команды, название или значение полей, то переопределяется класс протокола. Таким образом, легко подстроить наш клиент под любой другой сервер, даже если он написан когда-то давно и не нами. Или, наоборот, написан новый сервер под заданный клиент.

Если названия и формат команд выбрать достаточно абстрактными так, чтобы они подходили для всех жанров (вроде add, remove, move, change, и поля: from, to, type, target, etc), тогда мы получим единый протокол для абсолютно всех возможных игр. В этом случае разработчики смогут легко переключаться между проектами, без необходимости адаптироваться к новой системе команд. Каждая игра будет отличаться от других лишь тем, как эти команды выполняются.

Благодаря тому, что для всех платформ (web, mobile, desktop, console) и социальных сетей (fb, vk, ok, mm) используется один и тот же клиент-серверный протокол, все клиенты одного жанра могут подключаться к одному серверу. Это позволяет пользователям легко находить себе подходящего компаньона для игры, не ожидая пока проект раскрутится и соберет достаточную базу пользователей. То есть игра может брать первое время аудиторию из других проектов, пока не наберет свою собственную.

Подробное устройство серверного приложенияПодробное устройство серверного приложения

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

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

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

Также не нужно забывать о возможности играть в несколько разных игр одновременно — т.н. сеанс одновременной игры. Для многих пошаговых игр, где нужно долго ждать, пока походят все соперники, это важно (шахматы, покер). Удовольствие это недорогое — достаточно лишь сделать пару правильных архитектурных решений в самом начале разработки, и в будущем ничего не придется переделывать.

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

Что касается модулей геймплея, то все они наследуются от одного базового модуля, в котором реализуются разные режимы игры: игра на время, на очки, на выживание и другие. Это значит, что все эти режимы уже готовы и автоматически встроены во все текущие и будущие игры. Причем в любом сочетании — достаточно лишь задать нужные настройки: max_time_ms, min_score, max_round, etc, чтобы их активировать. Таким образом, логика и условия окончания игры, а также выявления победителей и проигравших реализуются отдельно от геймплея и только один раз для всех жанров.

Пример конфигов для серверной частиПример конфигов для серверной части

Все модули выносятся в библиотеки и используются повторно в других приложениях. Таким образом, можно новые игры собирать полностью из одних таких модулей, как в конструкторе Лего. А чтобы в сами модули можно было вносить изменения без необходимости переопределять библиотечные классы, мы все настройки можем задавать во внешних конфигурационных файлах (YAML, JSON, XML). В итоге, проект может состоять всего из одного класса, и то только потому, что для компилятора обязательно наличие точки входа. Весь остальной код находится в библиотеках.

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

Во внешние файлы можно не только вынести настройку приложения и визуальные ассеты, но даже и часть логики. Для разных игровых сущностей несложно реализовать концепцию условий, при соблюдении или несоблюдении которых мы можем показывать или скрывать определенные товары в магазине, показывать акции, давать новый уровень в игре и так далее. Например, мы можем показать специальное предложение, которое будет действовать только в первый день Нового года ({«date»:»2023–01–01»}) или в первый час нового дня ({«time.>=»:»0:00», «time.<": "1:00"}). Можно даже из общей массы пользователей выделить когорту платящих игроков старше 20 лет с средним чеком больше 20 и уровнем большим 10 ({"user.level.>»: 10, «user.age.>»: 20, «payments.hard.count.>»: 0, «payments.hard.avg.>»: 20}). Такое перетекание части логики из компилируемого кода в конфиги, значительно облегчая труд и гейм-дизайнеров, и разработчиков.

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

Когда мы одну и ту же логику помещаем полностью на сервер, то клиент получается тонким, если частично — то толстым. А если логика находится полностью на клиенте, то такое приложение является локальным, и разделение на клиент и сервер исчезает. Благодаря правильному выбору языка в наших исходниках изначально не существует такого разделения.

Код, общий для всех движков, определяющий структуру наших приложений, помещается в отдельную библиотеку-фреймворк. Также фреймворк определяет общие стандарты программирования для всей компании. Это означает, что программисту теперь не нужно думать, как построить то или иное приложение, как организовать его код, как сделать ту или иную вещь. Это высвобождает кучу энергии и времени, позволяя направить их на продуктивный труд, а не на эксперименты с «велосипедами». 

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

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

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

Результаты

Итак, что мы имеем в итоге и что нам это даёт?

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

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

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

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

Наличие универсального фреймворка определяет общие стандарты программирования для всей компании, позволяя новым членам команды быстро включаться в работу и достигать высокого уровня качества кода.

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

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

Приложения

К классификации игровых жанров

Эволюция игровых жанров (с картинками)

Эволюция игрового фреймворка (клиентского и серверного) — цикл из 6-ти статей, на который опирается данный материал.

Исходный код к статьям на Haxe (TypeScript-версия в процессе).

© Habrahabr.ru