Хабрахабр

Как мы замахнулись на мобильный fast paced шутер: технологии и подходы

Игра продолжает развиваться, у нее десятки миллионов установок и игроков по всему миру, постоянно выходят апдейты. Год назад у нас в компании был один проект — мобильный шутер War Robots с относительно медленными, но красочными и напряженными боями. Но реализовать задуманное для мобильных платформ (в первую очередь iOS и Android) на основе War Robots при текущих архитектуре и подходах было практически нереально. В какой-то момент мы захотели сделать динамичный шутер на Unity со скоростями, сравнимыми с Overwatch, CS:GO или Quake.

Здесь нет Rocket Science, все эти подходы придумали еще 30 лет назад и за это время они особо не поменялись. Мы понимали, как это сделать в теории — есть много статей, презентаций на YouTube, детально рассказывающих о том, как написать шутер, как работать с сетью, какие возникают проблемы и как их решать. НО: у нас не было практики.

Мы создали для мобильных платформ динамичный быстрый шутер, который сейчас находится в бета-тестировании и активно дорабатывается. Забегая вперед, скажу — нам удалось реализовать задуманное. Это первая, обзорная статья с перечислением и кратким описанием практически всего того, что мы используем (прошу не путать с другим нашим проектом в разработке, технологии и подходы в котором похожие, но отличаются в деталях). И мне очень хотелось бы всем этим поделиться.

Симуляция

Начнем с симуляции геймплея. Мы решили писать её на ECS — это data-oriented подход, в котором данные отделены от логики. Данные представлены как сущности (Entity) и компоненты (Components), принадлежащие сущностям. Логика описана в системах (Systems), которые обычно проходятся по компонентам сущностей и меняют их, создают новые компоненты и сущности.

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

Это дало нам следующие преимущества: Один и тот же код симуляции работает одновременно на клиенте и сервере.

  1. Игровые фичи полностью может писать клиентский программист — та же самая логика будет работать на сервере.
  2. Нет задержки между действием игрока и результатом действия в игре, т.к. клиент моментально обрабатывает команду локально (prediction). И только если результат локальной симуляции разошелся с серверной, клиент берет за основу серверное состояние мира и ресимулирует (rollback).
  3. Обучение игрока (tutorial) мы можем симулировать локально на устройстве, без подключения к игровому серверу.
  4. При разработке фичи на проекте не нужен постоянно работающий сервер. Чтобы тестировать логику, графику, гейм-дизайн и т.д. — можно запустить матч локально.
  5. Используя общую библиотеку, мы смогли быстро написать ботов для нагрузочного тестирования, что вовремя выявило различные проблемы производительности и памяти на игровом сервере.

Клиент

Помимо prediction и rollback на клиенте, мы используем интерполяцию. Но не в привычном смысле, т.к. мы симулируем и рисуем в одном кадре, 30 раз в секунду, и по сути, классическая интерполяция нам не нужна.

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

Архитектура клиента

В качестве DI-фреймворка мы используем Zenject.
В коде клиента активно используется инъекция зависимостей. В большинстве случаев мы стараемся писать так, чтобы отключив конкретный Installer мы отключили фичу/представление/сетевое взаимодействие. Конкретные настройки связывания описываются в маленьких Composition Root, которые в Zenject называются Installer.

В игре есть несколько контекстов и объекты там принадлежат конкретному контексту, живут вместе с ним:

  • ProjectContext — объекты, которые живут все время жизни приложения;
  • MetaContext — выбор персонажа, экипировка, покупки и т.д.;
  • MatchContext — контекст PvP-боя.

Контекст боя

Моделью выступает данные из ECS, presenter’ы работают с ними и Unity-view частью для отображения. В контексте MatchContext на уровне представления игровой механики мы используем MVP. На уровне представления UI используется MVVM, описанный ниже. У нас есть презентеры для сущностей и компонент.

Сериализуем данные сами: мы написали кастомную дельта-компрессию, т.к. Для транспорта данных между клиентом и сервером выступает низкоуровневая библиотека Photon, из которой мы используем протокол, основанный на udp. мир игры большой, а объем трафика критичен для мобильных устройств и нам хорошо бы уложиться в MTU, чтобы состояние мира вместилось в один физический пакет.

Контекст меты

В результате у нас нет для каждого окна кастомного MonoBehaviour-класса с кучей настроек и логикой UI, как это обычно делается на Unity-проектах. В MetaContext для уровня отображения мы используем MVVM и DataBinding на основе библиотеки Unity-Weld. В нашем случае программист пишет только логику и выдает «наружу» необходимые данные, а UI-дизайнер верстает и настраивает привязку данных к отображению. Вместо этого логика UI описана в ViewModel конкретного окна, а уровень View представлен всего одним классом — View (наследует MonoBehaviour) и несколькими стандартными классами для связывания данных (data binding).

Сейчас он основан на сигналах Zenject, но написан так, что в любой момент его можно переписать на любой другой вариант. В контексте меты мы также используем подход Signal-Command-Service, частично позаимствованный из библиотек StrangeIoC и IoC+. Таким образом связь событие-действие/сервис у нас сейчас настраивается на уровне Installer и описывается в одну строчку.

Протокол общения с мета-серверами основан на Protobuf, в качестве транспорта используется WebSockets.

Сторонние решения

Помимо описанных выше мы используем на клиенте множество других сторонних решений и плагинов для ускорения разработки:

  • FMOD — для работы со звуком. У нас есть отдельный sound designer, он умеет «готовить» клевые звуки и музыку в FMOD-редакторе.
  • Volatile Physics — физика на клиенте и сервере, написанная на C#.
  • Lunar Console — для просмотра логов на клиенте, а также тестового функционала.
  • Helpshift — для общения с нашими игроками и сбора отзывов.
  • AppMetr — наша собственная система аналитики.
  • Json.net.
  • И др.

Общие решения

У нас есть отдельная команда, которая разрабатывает для проектов общие решения. Например:

  • у нас свой PackageManager (он лучше, чем в Unity 2018 и появился задолго до него), который поддерживает версионность и удаление. Через него мы доставляем в проект все другие наши решения, а также сторонние плагины и библиотеки. У нас нет проблемы удаления и обновления плагинов;
  • свой BuildPipeline — возможность настраивать сборку для разных конфигураций и платформ, т.к. обычно шаги сборки и настройки отличаются + интеграция с TeamCity;
  • модуль авторизации клиента в системе;
  • система автоматического тестирования;
  • адаптированные под нас сторонние плагины (логирование, аналитика; см. выше);
  • общие шейдеры;
  • и др.

Оптимизация клиента

У нас большой опыт оптимизации под мобильные платформы. В частности:

  • Оптимизируем шейдеры.
  • Сокращаем в разы размеры билда за счет компрессии текстур, сборки их в атласы, уменьшения количества variants в шейдерах и не только.
  • Используем свой MeshMerger для объединения статических объектов с одним материалом в один объект, также мержим текстуры.
  • С помощью встроенного в Unity профилировщика, а также dotTrace и dotMemory оптимизируем код.
  • Используем memory pools и preallocated memory, чтобы минимизировать сборку мусора.
  • Используем дельта-компрессию для уменьшения размера отправляемых пакетов.
  • Многое другое.

Что-то из этого можно прочитать в наших предыдущих статьях «Пост-эффекты в мобильных играх» и «Оптимизация 2d-приложений для мобильных устройств в Unity3d». Вторая из этих двух статей уже устарела, но многие советы оттуда работают и сейчас.

Игровой сервер

Игровой сервер, на котором проходят бои, написан на C#. В качестве сетевого протокола используется упомянутая выше upd-based Photon Network Library. На текущий момент правки в сервер вносятся очень редко, т.к. вся игровая логика пишется клиентскими программистами и она же крутится на сервере.

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

Мета-сервер

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

  • Java — основной язык разработки.
  • Cassandra, postgres — для хранения данных игроков.
  • Consul — как service discovery и в том числе хранилище key-value данных для настроек игры и серверов.
  • RabbitMQ — очередь сообщений.
  • Protobuf — протокол между сервисами и клиентом.
  • gRPC — для общения между сервисами и игровым сервером.
  • А также netty, akka, logback и многое другое.

Команда

Игра быстро разрастается фичами и мы активно привлекаем новых сотрудников, но в целом команда проекта состоит из ядра и смежных отделов.

К смежным отделам относятся: В ядро входят клиентские программисты, геймдизайнер, Project Manager, Product Owner и QA (тестировщики).

  • программисты графики;
  • программисты мета-серверов;
  • команда, разрабатывающая общие решения для проектов;
  • художники;
  • аниматоры;
  • community-менеджеры;
  • саппорты;
  • маркетинг и др.

Как мы работаем

Мы работаем по Scrum. Но стоит понимать, что у всех свой Скрам. У нас есть двухнедельные итерации, покер-планирование, ретроспективы, демо и т.д. Но при этом релизы у нас не привязаны к окончанию спринта и есть дополнительный этап релизного тестирования.

Код пишем в Rider и Visual Studio + Resharper. Храним код на GitHub, делаем pull request’ы и используем Upsource для code review.

Установка клиента на мобильное устройства происходит в один-два клика: Для сборки клиента, а также развертки и деплоя серверов мы используем TeamCity и Gradle.

  • собрать клиент в TeamCity, нажав Run (этот шаг можно пропустить, так как в основном сборки происходят в автоматическом режиме);
  • установка по QR-коду, сгенерированному для билда.

Вместо заключения

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть