Найти в Дзене
Сделай игру

Мультиплеер в играх - создаём свою платформу

Оглавление

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

Геймеры-медведи от Кандинского; немного пугают излишние конечности, но на обложку - норм
Геймеры-медведи от Кандинского; немного пугают излишние конечности, но на обложку - норм

Общие сведения о мультиплеере

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

  1. Возможность сохранения, передачи и восстановления состояния управляемого объекта (персонажа).
  2. Краткость передаваемых данных: передаётся лишь необходимое и достаточное.

Подытожим: нужно краткое состояние игрока, которое можно передать и быстро восстановить.

Виды мультиплеера

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

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

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

Второй случай предполагает более сложную схему взаимодействия: есть сервер, который обрабатывает подключения всех игроков и этот сервер, вероятней всего, независим от запущенных экземпляров игры участников; все игроки становятся "удалёнными", даже хозяин сервера (сервер вполне может запускаться на одной машине с клиентом), а все взаимодействия игрового мира перемещаются на сервер; игра же используется лишь как терминал, отображающий данные. Однако могут быть и иные варианты взаимодействия.

Что происходит на сервере

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

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

Например, это может выглядеть так:

  1. Запрос открытых для подключения игр;
  2. Отправка запроса на присоединение к игре;
  3. Получение метаданных об игре, участниках, состоянии игровых и не игровых объектов, прочая информация;
  4. Получение уведомление об ожидании подключения всех участников;
  5. Отправка уведомления о начале игры с текущими метаданным (положение, параметры персонажа итп);
  6. Обмен данными об изменении состояния всех персонажей, включая часть их метаданных;
  7. Отправка информации о завершении игры.

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

Каналы взаимодействия клиент-сервер

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

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

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

Иными словами, лучше, если каждый клиент будет иметь два соединения с сервером: один, TCP, по которому он будете передавать свои актуальные данные; второй - UDP - по которому будет получать все игровые данные и на основании которых обновлять свою игровую модель.

Игровая модель данных

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

Представьте себе игру Super Mario, где может одновременно играть сразу несколько человек и цель не просто добежать до замка, но и сделать это раньше других, набрав при этом как можно больше очков (монет, побеждённых врагов).

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

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

Очевидно, если слать "дельты" по UDP, есть шанс потерять один или несколько ключевых пакетов, а слать постоянно состояние всей карты - дорого и неэффективно. Поэтому часть данных от сервера к клиенту можно передавать и по TCP, благо, канал для этого у нас есть. Главное, делать это не часто и в небольших количествах.

Переходя от теории к практике

Честно говоря, не думал, что теория окажется столько затянутой, это я её ещё сокращал как мог.

Давайте же напишем фреймворк, на базе которого можно будет развернуть клиент-серверное взаимодействие мультиплеера.

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

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

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

В-четвёртых, предусмотреть механизм актуализации данных как у клиента, так и у сервера с тем, чтобы безопасно восстановить модель данных и не допустить того, чтобы устаревшие данные были установлены поверх более новых.

Пишем сервер

Для сервера я выбрал nodejs (можно было написать и на Go, но т.к. клиент будет написан под веб, пусть лучше везде будет один и тот же язык; что-то может будет повторно использовать). Правда есть небольшая трудность - со стороны клиента можно использовать только вебсокеты, что накладывает некоторые ограничения. Поэтому я ограничусь одним каналом связи, благо у меня не будет больших объёмов данных для передачи.

Начнём с каркаса сервера: он просто принимает сообщения и отправляет уведомление обратно. Я использовал готовый пакет ws:

Сервер довольно прост
Сервер довольно прост

И под сервер написать простейший веб-клиент, который просто отправляет несколько сообщений и закрывает соединение.

Отправляет 5 уведомлений и завершает сеанс
Отправляет 5 уведомлений и завершает сеанс

Песочница

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

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

Все вычисления производятся на сервере, на клиенте - только отрисовка и управление.

Знакомство

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

Код сервера, от подключения до запуска игры
Код сервера, от подключения до запуска игры

В это же время клиент получает данные от сервера и реагирует на них.

После открытия соединения - отсылаем пустой пакет; это - заявка на подключение
После открытия соединения - отсылаем пустой пакет; это - заявка на подключение

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

Сбор игроков

На этом этапе мы ждём, пока подключатся все игроки. У автора сервера (первого подключившегося) появляется кнопка "начать игру" и счётчик игроков; прочие же видят лишь счётчик игроков, но начать игру не могут (это блокируется на уровне сервера).

Две вкладки рядом
Две вкладки рядом

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

Пакет служебных данных при инициализации
Пакет служебных данных при инициализации

Фактически, сервер передаёт клиенту игровые настройки, которые будут использованы (в том числе данные карты и дальность обзора; они нужны для отрисовки).

Начало игры

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

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

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

Синхронная игра
Синхронная игра

Как по мне, получилось неплохо; задержки минимальны, а объекты двигаются на экране одновременно. Дальше необходимо добавить подарочки, которые увеличивают счёт игроков и разлёт от столкновения.

Это основной код для клиента
Это основной код для клиента
А это основной код сервера
А это основной код сервера

Обработка столкновений

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

Алгоритм вычисления столкновений
Алгоритм вычисления столкновений

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

При "наезде" на подарок - голубой ромб - счётчик внутри увеличивается на 1; при достижении определённого счёта - видим сообщения о победе.

Сладкий вкус победы
Сладкий вкус победы
Горький вкус поражения
Горький вкус поражения

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

Выглядит всё примерно так
Выглядит всё примерно так

Заключение

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

Главное, были соблюдены все указанные этапы: ожидание игры и её запуск, игровой процесс и завершение игры.

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