Привет! Я Андрей Махорин, разработчик серверов Pixonic (MY.GAMES). В этой статье я расскажу, как мы с командой создали универсальное решение для серверной разработки. Вы узнаете об этой концепции, ее результатах и о том, как наша система под названием Singularity показала себя в реальных проектах. Я также углублюсь в проблемы, с которыми мы столкнулись.
Когда игровая студия только начинает свою деятельность, крайне важно быстро сформулировать и реализовать интересную идею: проверяются десятки гипотез, игра постоянно меняется; добавляются новые функции, а неудачные решения пересматриваются или отбрасываются. Однако этот процесс быстрых итераций в сочетании со сжатыми сроками и коротким горизонтом планирования может привести к накоплению технического долга.
При наличии технического долга повторное использование старых решений может быть затруднено, поскольку с их помощью необходимо решать различные проблемы. Это явно не оптимально. Но есть и другой путь: «универсальные рамки». Разрабатывая универсальные компоненты многократного использования (такие как элементы макета, окна и библиотеки, реализующие сетевое взаимодействие), студии могут значительно сократить время и усилия, необходимые для разработки новых функций. Такой подход не только уменьшает объем кода, который необходимо написать разработчикам, но также гарантирует, что код уже протестирован, что приводит к сокращению времени, затрачиваемого на обслуживание.
Мы обсуждали разработку функций в контексте одной игры, но теперь давайте посмотрим на ситуацию под другим углом: для любой игровой студии повторное использование небольших фрагментов кода внутри проекта может быть эффективной стратегией оптимизации производства, но в конечном итоге они нужно будет создать новую хитовую игру. Теоретически повторное использование решений из существующего проекта могло бы ускорить этот процесс, но возникают два существенных препятствия. Во-первых, здесь применимы те же проблемы технического долга, а во-вторых, любые старые решения, вероятно, были адаптированы к конкретным требованиям предыдущей игры, что делало их плохо подходящими для нового проекта.
Эти проблемы усугубляются другими проблемами: дизайн базы данных может не соответствовать требованиям нового проекта, технологии могут быть устаревшими, а новой команде может не хватать необходимого опыта.
Более того, базовая система часто разрабатывается с учетом определенного жанра или игры, что затрудняет адаптацию к новому проекту.
Опять же, именно здесь в игру вступает универсальная структура, и хотя создание игр, сильно отличающихся друг от друга, может показаться непреодолимой задачей, есть примеры платформ, которые успешно справились с этой проблемой: PlayFab, Photon Engine и подобные платформы. продемонстрировали свою способность значительно сокращать время разработки, позволяя разработчикам сосредоточиться на создании игр, а не инфраструктуры.
Теперь давайте перейдем к нашей истории.
Для многопользовательских игр необходим надежный бэкэнд. Показательный пример: наша флагманская игра War Robots. Это мобильный PvP-шутер, он существует уже более 10 лет и накопил множество функций, требующих серверной поддержки. И хотя наш серверный код был адаптирован под специфику проекта, в нем использовались устаревшие технологии.
Когда пришло время разрабатывать новую игру, мы поняли, что повторное использование серверных компонентов War Robots будет проблематичным. Код был слишком специфичен для проекта и требовал знаний в технологиях, которых не хватало новой команде.
Мы также осознавали, что успех нового проекта не гарантирован, и даже если бы он был успешным, нам в конечном итоге пришлось бы создать еще одну новую игру, и мы столкнулись бы с той же проблемой «чистого листа». Чтобы избежать этого и подготовиться к будущему, мы решили определить основные компоненты, необходимые для разработки игр, а затем создать универсальную структуру, которую можно будет использовать во всех будущих проектах.
Нашей целью было предоставить разработчикам инструмент, который избавил бы их от необходимости многократно проектировать серверные архитектуры , схемы баз данных, протоколы взаимодействия и конкретные технологии. Мы хотели освободить людей от бремени реализации авторизации, обработки платежей и хранения пользовательской информации, позволяя им сосредоточиться на основных аспектах игры: игровом процессе, дизайне, бизнес-логике и многом другом.
Кроме того, мы хотели не только ускорить разработку с помощью нашей новой среды, но и дать возможность клиентским программистам писать серверную логику без глубоких знаний сетей, СУБД или инфраструктуры.
Кроме того, стандартизировав набор сервисов, наша команда DevOps сможет одинаково обрабатывать все игровые проекты, меняя только IP-адреса. Это позволит нам создавать повторно используемые шаблоны сценариев развертывания и панели мониторинга.
На протяжении всего процесса мы принимали архитектурные решения, учитывающие возможность повторного использования бэкенда в будущих играх. Такой подход гарантировал, что наша структура будет гибкой, масштабируемой и адаптируемой к различным требованиям проекта.
(Также стоит отметить, что разработка фреймворка не была островом – он создавался параллельно с другим проектом.)
Мы решили предоставить Singularity набор функций, не зависящих от жанра, сеттинга или основного игрового процесса игры, в том числе:
Эти функции имеют основополагающее значение для любого многопользовательского мобильного проекта (по крайней мере, они актуальны для проектов, разработанных в Pixonic).
В дополнение к этим основным функциям Singularity была разработана для включения большего количества функций, специфичных для проекта, ближе к бизнес-логике. Эти возможности созданы с использованием абстракций, что делает их пригодными для многократного использования и расширения в различных проектах.
Вот некоторые примеры:
Технически платформа Singularity состоит из четырёх компонентов:
Далее давайте рассмотрим каждый из этих компонентов.
Некоторые службы, такие как служба профилей и подбор игроков, требуют бизнес-логики, специфичной для игры. Чтобы учесть это, мы разработали эти сервисы для распространения в виде библиотек. Опираясь на эти библиотеки, разработчики смогут создавать приложения, включающие в себя обработчики команд, логику поиска совпадений и другие компоненты, специфичные для проекта.
Этот подход аналогичен созданию приложения ASP.NET, где платформа обеспечивает низкоуровневую функциональность протокола HTTP, в то время как разработчик может сосредоточиться на создании контроллеров и моделей, содержащих бизнес-логику.
Например, предположим, что мы хотим добавить возможность менять имена пользователей в игре. Для этого программистам потребуется написать класс команды, включающий новое имя пользователя и обработчик этой команды.
Вот пример команды ChangeNameCommand:
public class ChangeNameCommand : ICommand { public string Name { get; set; } }
Пример этого обработчика команды:
class ChangeNameCommandHandler : ICommandHandler<ChangeNameCommand> { private IProfile Profile { get; } public ChangeNameCommandHandler(IProfile profile) { Profile = profile; } public void Handle(ICommandContext context, ChangeNameCommand command) { Profile.Name = command.Name; } }
В этом примере обработчик должен быть инициализирован с помощью реализации IProfile, которая обрабатывается автоматически посредством внедрения зависимостей. Некоторые модели, такие как IProfile, IWallet и IInventory, доступны для реализации без дополнительных действий. Однако с этими моделями может быть не очень удобно работать из-за их абстрактного характера, поскольку они предоставляют данные и принимают аргументы, которые не соответствуют конкретным потребностям проекта.
Чтобы упростить задачу, проекты могут определять свои собственные модели предметной области, регистрировать их аналогично обработчикам и при необходимости внедрять в конструкторы. Такой подход обеспечивает более индивидуальный и удобный опыт работы с данными.
Вот пример доменной модели:
public class WRProfile { public readonly IProfile Raw; public WRProfile(IProfile profile) { Raw = profile; } public int Level { get => Raw.Attributes["level"].AsInt(); set => Raw.Attributes["level"] = value; } }
По умолчанию профиль игрока не содержит свойства Level. Однако, создав модель для конкретного проекта, можно добавить такое свойство и легко читать или изменять информацию уровня игрока в обработчиках команд.
Пример обработчика команд с использованием модели предметной области:
class LevelUpCommandHandler : ICommandHandler<LevelUpCommand> { private WRProfile Profile { get; } public LevelUpCommandHandler(WRProfile profile) { Profile = profile; } public void Handle(ICommandContext context, LevelUpCommand command) { Profile.Level += 1; } }
Этот код ясно демонстрирует, что бизнес-логика конкретной игры изолирована от базовых уровней транспорта или хранения данных. Эта абстракция позволяет программистам сосредоточиться на основной игровой механике, не беспокоясь о транзакциях, условиях гонки или других распространенных проблемах с серверной частью.
Более того, Singularity предлагает широкие возможности для улучшения игровой логики. Профиль игрока представляет собой набор пар «значений с типом ключа», позволяющий разработчикам игр легко добавлять любые свойства по своему усмотрению.
Помимо профиля, сущность игрока в Singularity состоит из нескольких важных компонентов, предназначенных для обеспечения гибкости. Примечательно, что сюда входит кошелек, который отслеживает количество каждой валюты внутри него, а также инвентарь, в котором перечислены предметы игрока.
Интересно, что элементы в Singularity — это абстрактные сущности, похожие на профили; каждый элемент имеет уникальный идентификатор и набор пар значений с типом ключа. Таким образом, предмет не обязательно должен быть осязаемым объектом, таким как оружие, одежда или ресурс в игровом мире. Вместо этого оно может представлять собой любое общее описание, предназначенное исключительно для игроков, например квест или предложение. В следующем разделе я подробно расскажу, как эти концепции реализованы в конкретном игровом проекте.
Одним из ключевых отличий Singularity является то, что элементы хранят ссылку на общее описание в балансе. Хотя это описание остается неизменным, свойства отдельного выданного предмета могут меняться. Например, игрокам может быть предоставлена возможность менять скины оружия.
Кроме того, у нас есть надежные варианты переноса данных игроков. При традиционной серверной разработке схема базы данных часто тесно связана с бизнес-логикой, а изменения свойств объекта обычно требуют прямых изменений схемы.
Однако традиционный подход непригоден для Singularity, поскольку фреймворку не хватает информации о бизнес-свойствах, связанных с сущностью игрока, а команда разработчиков игры не имеет прямого доступа к базе данных. Вместо этого миграции разрабатываются и регистрируются как обработчики команд, которые работают без прямого взаимодействия с репозиторием. Когда игрок подключается к серверу, его данные извлекаются из базы данных. Если какие-либо миграции, зарегистрированные на сервере, еще не были применены к этому проигрывателю, они выполняются, а обновленное состояние сохраняется обратно в базу данных.
Список примененных миграций также сохраняется как свойство игрока, и у этого подхода есть еще одно существенное преимущество: он позволяет распределять миграции по времени. Это позволяет нам избежать простоев и проблем с производительностью, которые в противном случае могли бы вызвать массовые изменения данных, например, при добавлении нового свойства ко всем записям игроков и установке для него значения по умолчанию.
Singularity предлагает простой интерфейс для взаимодействия с серверной частью, позволяя проектным группам сосредоточиться на разработке игр, не беспокоясь о проблемах протоколов или сетевых технологий связи. (Тем не менее, SDK обеспечивает гибкость для переопределения методов сериализации по умолчанию для команд, специфичных для проекта, если это необходимо.)
SDK обеспечивает прямое взаимодействие с API, но также включает в себя оболочку, автоматизирующую рутинные задачи. Например, выполнение команды в службе профилей генерирует набор событий, указывающих на изменения в профиле игрока. Оболочка применяет эти события к локальному состоянию, гарантируя, что клиент поддерживает текущую версию профиля.
Вот пример вызова команды:
var result = _sandbox.ExecSync(new LevelUpCommand())
Большинство сервисов в Singularity разработаны так, чтобы быть универсальными и не требуют настройки для конкретных проектов. Эти сервисы распространяются в виде готовых приложений и могут использоваться в различных играх.
В комплекс готовых услуг входит:
Некоторые службы являются фундаментальными для платформы и должны быть развернуты, например служба аутентификации и шлюз. Другие являются необязательными, например, служба друзей и таблица лидеров, и их можно исключить из среды игр, которые в них не нуждаются.
Вопросы управления большим количеством сервисов я коснусь позже, а пока важно подчеркнуть, что дополнительные сервисы должны оставаться необязательными. По мере роста количества сервисов сложность и порог включения в новые проекты также увеличиваются.
Хотя базовая структура Singularity вполне функциональна, важные функции могут быть реализованы проектными группами независимо друг от друга без изменения ядра. Когда функциональность определена как потенциально полезная для нескольких проектов, она может быть разработана командой платформы и выпущена как отдельные библиотеки расширений. Эти библиотеки затем можно интегрировать и использовать в обработчиках команд в игре.
Некоторые примеры функций, которые могут здесь применяться, — это квесты и предложения. С точки зрения базовой структуры, эти сущности — это просто элементы, назначенные игрокам. Однако библиотеки расширений могут наделять эти предметы определенными свойствами и поведением, превращая их в квесты или предложения. Эта возможность позволяет динамически изменять свойства предмета, позволяя отслеживать ход квеста или записывать последнюю дату, когда предложение было представлено игроку.
Сингулярность была успешно реализована в одной из наших последних игр, доступных во всем мире, Little Big Robots, и это дало разработчикам клиента возможность самостоятельно управлять логикой сервера. Кроме того, мы смогли создавать прототипы, использующие существующую функциональность, без необходимости прямой поддержки со стороны команды платформы.
Однако это универсальное решение не лишено проблем. По мере расширения количества функций росла и сложность взаимодействия с платформой. Singularity превратилась из простого инструмента в сложную и сложную систему, в некотором смысле похожую на переход от простого кнопочного телефона к полнофункциональному смартфону.
Хотя Singularity избавила разработчиков от необходимости погружаться в сложности баз данных и сетевых коммуникаций, она также ввела собственную кривую обучения. Теперь разработчикам необходимо понять нюансы самой Singularity, что может стать значительным сдвигом.
С проблемами сталкиваются самые разные люди: от разработчиков до администраторов инфраструктуры. Эти профессионалы часто обладают глубоким опытом развертывания и поддержки таких известных решений, как Postgres и Kafka. Однако Singularity — это внутренний продукт, требующий приобретения новых навыков: им необходимо изучить тонкости кластеров Singularity, различать обязательные и дополнительные сервисы и понимать, какие метрики имеют решающее значение для мониторинга.
Хотя это правда, что внутри компании разработчики всегда могут обратиться к создателям платформы за советом, но этот процесс неизбежно требует времени. Наша цель — максимально минимизировать барьер входа. Для достижения этого необходима подробная документация для каждой новой функции, что может замедлить разработку, но, тем не менее, считается инвестицией в долгосрочный успех платформы. Более того, для обеспечения надежности системы необходимо надежное покрытие модульных и интеграционных тестов.
Singularity во многом полагается на автоматическое тестирование, поскольку ручное тестирование потребует разработки отдельных экземпляров игры, что непрактично. Автоматизированные тесты позволяют обнаружить подавляющее большинство — то есть 99 % — ошибок. Однако всегда существует небольшой процент проблем, которые становятся очевидными только во время конкретных игровых тестов. Это может повлиять на сроки выпуска, поскольку команда Singularity и проектные группы часто работают асинхронно. В коде, написанном давно, может быть обнаружена блокирующая ошибка, и команда разработчиков платформы может быть занята другой критической задачей. (Эта проблема не уникальна для Singularity и может возникнуть и при разработке пользовательского бэкэнда.)
Еще одна серьезная проблема — управление обновлениями во всех проектах, использующих Singularity. Обычно существует один флагманский проект, который стимулирует развитие платформы с помощью постоянного потока запросов на новые функции и улучшений. Взаимодействие с командой этого проекта тесное; мы понимаем их потребности и то, как они могут использовать нашу платформу для решения своих проблем.
В то время как некоторые флагманские проекты тесно связаны с командой фреймворка, другие игры на ранних стадиях разработки часто работают независимо, полагаясь исключительно на существующие функциональные возможности и документацию. Иногда это может привести к избыточным или неоптимальным решениям, поскольку разработчики могут неправильно понять документацию или неправильно использовать доступные функции. Чтобы смягчить это, крайне важно облегчить обмен знаниями посредством презентаций, встреч и обмена командами, хотя такие инициативы требуют значительных затрат времени.
Singularity уже продемонстрировала свою ценность в наших играх и готова к дальнейшему развитию. Несмотря на то, что мы планируем ввести новые функции, наша основная задача сейчас — убедиться, что эти улучшения не усложняют использование платформы для проектных групп.
Помимо этого необходимо снизить барьер входа, упростить развертывание, добавить гибкость в плане аналитики, позволяя проектам подключать свои решения. Это вызов для команды, но мы верим и видим на практике, что усилия, вложенные в наше решение, обязательно окупятся сполна!