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

  1. Для кого и для чего

  2. Синглплеер и Мультиплеер

  3. -> Клиент и Сервер

  4. ...

Следить за выходом новых статей и другого контента можно в моём блоге на VK / Telegram Dtf.


Сервер

В сильно общем смысле:

Сервер – это вычислительная единица, которая выполняет определенные сервисные функции.

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

В зависимости от своих функций серверы могут быть разных типов:
- файловые серверы;
- веб-серверы;
- DNS-серверы;
- почтовые серверы;
- и пр.

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

"Агрегатные состояния" сервера:
отдельный компьютер, настроенный на выполнение своих функций;
виртуальная машина, эмулирующая отдельный компьютер;
контейнер, выполняющий серверные функции;
серверное приложение, запущенное на настроенном для этого компьютере.

В моей практике в геймдеве чаще всего в качестве "серверов" используют серверные приложения в виде исполняемых файлов или docker-контейнеров, в зависимости от конфигурации окружения, в котором планируется их запускать. Это, в частности, позволяет запускать такие сервера на локальном компьютере для проведения локальных тестов.

В геймдеве есть и набор наиболее часто используемых типов серверов.

Мета-сервер

Самый распространённый вид сервера. Многие игры вовсе ограничиваются только им. А какие-то и не используют совсем.

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

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

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

Профиль-сервер

Профиль-сервер предназначен для хранения и управления данными игроков.

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

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

Варианты реализации:
- отдельный сервер или сервис, с которым общаются через мета-сервер;
- отдельный сервер или сервис, с которым общаются клиенты напрямую (если нет мета-сервера);
- ответственность мета-сервера непосредственно.

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

Игровой сервер предназначен для обеспечения мультиплеерного взаимодействия.

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

Задачи игрового сервера:
- возможность нескольким игрокам одновременно участвовать в одной игровой сессии;
- обмен информацией между игроками о своих действиях и своём состоянии;
- управление состоянием игрового мира, не контролируемого игроками;
- предоставление выделенного доверенного безопасного узла, который будет поддерживать честный геймплей и обеспечивать защиту от читеров.

Функции игрового сервера:
- обработка данных, полученных от клиентов;
- передача полученных данных другим игрокам;
- обеспечение синхронизации игрового процесса;
- управление логикой игрового процесса;
- обеспечение защиты от мошенничества;
- управления правами доступа игроков;
- хранение состояния игры;
- ведение статистики;
- и пр.

Игровой сервер обычно обеспечивает работу одной конкретной игровой сессии в рамках CoreGame. Когда сервер будет выключен, игровая сессия для всех игроков на этом сервере завершится. Поэтому в играх с длительной игровой сессией может требоваться механизм бэкапов, когда игровой сервер записывает перед выключением или по мере своей работы игровые данные в какое-нибудь подключенное хранилище, чтобы иметь возможность восстановить игровую сессию после запланированной (или нет) перезагрузки игрового сервера или его замены на другой.

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

Каждый отдельны матч в какой-нибудь Counter-Strike — это отдельная игровая сессия на отдельном игровом сервере, который можно или выбрать самостоятельно, или запросить наиболее подходящий у мастер-сервера или мета-сервера. Матч закончен -> сессия завершается -> игровой сервер прекращает свою работу.

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

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

Мастер-сервер

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

Функции мастер-сервера:
- получение от активных игровых серверов специальных сигналов с информацией — heartbeat;
- формирование и хранение списков активных серверов по различным параметрам: режимы, карты, модификации и пр;
- мониторинг игровых серверов, отслеживание их заполненности и доступности;
- выдача списка доступных серверов по запросу клиентам или мета-серверу;
- создание и запуск новых игровых серверов.

Варианты реализации:
- отдельный сервер или сервис, с которым общаются через мета-сервер;
- отдельный сервер или сервис, с которым общаются клиенты напрямую (если нет мета-сервера);
- ответственность мета-сервера непосредственно.


Клиент

Клиент – это сама игра на устройстве пользователя.

Основные функции клиента (в контексте мультиплеера):
- получение ввода от пользователя;
- обработка полученных данных;
- передача данных серверу и/или другим клиентам;
- получение и обработка ответов от сервера и/или других клиентов;
- рендеринг игрового процесса на основе актуальных игровых данных.

В некотором общем смысле, если использовать термины MVP:

  • Сервер: это Model (владеет данными игры и реализует бизнес-логику).

  • Клиент: это View (получает инпут и рендерит игру) и Presenter (обрабатывает инпут, взаимодействует с моделью и обновляет представление).

В синглплеерных режимах/играх вся тройка MVP находится на одном узле – на клиенте.

Почему именно модель уходит на сервер:

  • На сервере нет необходимости что-то рендерить.

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

  • Игра — это, в первую очередь, данные и манипуляции с этими данными. Чтобы игровой процесс распространить между несколькими клиентами, нужно чтобы данные, поверх которых строится игровой процесс, были общими.

  • Для многопользовательской работы с данными нужно обеспечить очерёдность и порядок доступа к этим данным.

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

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


Синхронизация данных на клиенте

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

При недостаточном качестве соединения синхронизация локальных данных будет происходить слишком долго. Из-за этого игра будет терять в отзывчивости (input lag), и игрок всегда будет наблюдать действия, которые уже не актуальны.

Для компенсации задержек используется масса всяких приёмов (подробнее – в следующих статьях). Из-за этого в реальности описанные выше MVP-границы разделения ответственностей часто размываются:

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

  • Где-то сервер пытается действовать наперёд, заранее предсказывая и имитируя инпут игрока.

  • Где-то только наиболее важная определённая для геймплея часть данных считается достоверной и управляется сервером, а оставшиеся данные обрабатывают в своих локальных моделях сами клиенты.
    (это про авторитет над данными, об этом – позже)

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

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

Если какой-то элемент игры не влияет на геймплей, то не страшно, если у каких-то игроков его состояние не синхронизируется вовремя и корректно.

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

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


Реализация Клиента и Сервера

Образно, что из себя представляет сервер, а что клиент, обсудили. Теперь обсудим, что они из себя представляют в плане реализации. Я знаком с двумя вариантами.

Клиент и Сервер – отдельные проекты

Достоинства:
✅ Можно выбрать любую любимую технологию с набором любимых библиотек, не зависимо от того, в каком движке разрабатывается игра. Подойдут любые языки программирования, предоставляющие возможность писать серверные приложения: PythonJava.NETNode.jsPHP и др.
✅ Это легковесное решение, т.к. серверное приложение будет содержать только необходимые зависимости, заложенные разработчиком и самой технологией.
✅ Можно использовать готовый шаблон, готовый фреймворк или реализовать сервер полностью своими силами, что позволит контролировать максимум аспектов работы приложения.
✅ Есть возможность реализовать специфичные сценарии работы. Например, реализацию поддержки нескольких игровых сессий параллельно.

Недостатки:
❌ Реализация сервера потребует дополнительного времени на разработку и дальнейшую поддержку. А также дополнительной экспертизы в выбранной технологии.
❌ Может возникнуть дублирование кодовой базы, т.к. обычно в мультиплеерных играх есть слой общей логики и общих данных, которые должны быть использованы на сервере и на клиенте. Если сервер и клиент написаны с использованием разных языков и несогласующихся технологий, то может появиться потребность дублировать часть кодовой базы для клиента и сервера.
❌ От написанного сервера будет сложно отказаться, если потребуется в будущем изменить "топологию подключения" и перейти на вариант без сервера. Про то, какие варианты есть, будет написано в будущих статьях.

Проблему общих зависимостей и их дублирования в контексте Unity решают следующим образом:
- Клиентская кодовая база в Unity написана на C#.
- Для реализации сервера используют .NET Core на C#.
- Общие зависимости выделяют в отдельную сборку или отдельный C#-проект.
- Клиентский и серверный проект подключают к себе модуль с общими зависимостями или используют собранные dll с ними.

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

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

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

Клиент и Сервер – один проект

Подобный способ реализации предоставляют практически все Netcode-библиотеки для Unity. Работая в Unity-проекте идёт работа как над клиентской логикой, так и над серверной. В зависимости от стиля работы это может быть как сгруппировано в отдельные модули внутри проекта, так и не иметь строго разделения. Тогда используют конструкции ветвления внутри логики для определения текущей стороны исполнения и запуска соответствующих сценариев работы.

Достоинства:
✅ Вместо двух-трёх проектов — один, что в каком-то смысле упрощает разработку.
✅ Проблема дублирования логики и общих зависимостей решается сама собой, т.к. всё необходимое всем сторонам исполнения уже находится в одном проекте.
✅ От сервера достаточно просто отказаться. Серверная логика уже написана на нужном языке и находится в целевом проекте – достаточно адаптировать её и включить в клиентские сценарии.

Недостатки:
❌ Сервер – полноценная игра на клиентском движке. Т.е. помимо серверного кода от CoreGame туда уйдёт, как минимум, сам движок с его зависимостями и, как максимум, вся остальная игра с MetaGame и прочим.
❌ Чтобы лишние зависимости не уходили на сервер, нужно постоянно заниматься их контролем и организовывать проект так, чтобы на этапе сборки всё лишнее имело возможность не попадать в билд.
❌ Клиент и сервер обычно имеют разные конфигурации для итоговой сборки, особенно в контексте CI/CD, поэтому нужно реализовывать и поддерживать разные сценарии сборки.
❌ Если используется готовая Netcode-библиотека, то один сервер будет обслуживать только одну игровую сессию. Чем больше параллельных сессий, тем больше серверов нужно включать.

Чем чревато утекание лишних зависимостей на сервер:

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

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

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

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

Аналогично серверные зависимости также могут "протекать" на клиент и вызывать схожие проблемы.

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

В целом, его можно использовать и для реализации мета-сервера, причём не только в мультиплеерных проектах.
На мой взгляд, это может быть оправдано в случаях, когда нет опыта в разработке серверных приложений, а проект сделать нужно и быстро.
Но так можно потерять в легковесности, гибкости, простоте (у мета-сервера много специфичной логики, которая с клиентом может иметь мало общего) и тестируемости.
Ведь тестировать придётся не обычное серверное приложение, для которого есть богатый парк инструментов, а, грубо говоря, игру на Unity, что может ограничить в возможностях и удобстве.


Заключение

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

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


Контент

Полезные ресурсы по теме:
- Bare Metal vs Virtual Machines vs Containers: dev.to
- MasterServer: How it works: 333Networks
- What is a Game Server and How Does it Work: ServerMania
- Архитектура мета-сервера у Tacticool: Habr.Pixonic
- Инфраструктура серверов War Robots: Habr.Pixonic
- Fast paced шутер: технологии и подходы: Habr.Pixonic
- Вторые полгода разработки нового мобильного PvP: Habr.Pixonic