Как именно работает клиент и сервер игры (для новичков)









Очень много непонимания у людей, как именно работает клиент и сервер игры. Попробую немного объяснить.
Все действия в игре происходят в 99% на сервере и только на нем, а клиент просто является графическим интерфейсом между сервером и игроком.
Пакеты от клиента почти всегда являются запросами на попытку каких либо действий, а пакеты от сервера почти всегда являются либо ответами на запросы клиента, разрешающие или запрещающие запрошенное клиентом действие, либо являются информирующими пакетами о каких либо изменениях в статусе игрока, в окружающем мире или прочей информации, немедленно отражающейся в клиенте как факт.
Вот, как пример, грубо приближенный ‘диалог’ сервера с клиентом:
(‘сервер клиенту:’ — это посылается пакет клиенту от сервера)
(‘клиент серверу:’ — это посылается пакет серверу от клиента)
сервер клиенту: в координатах x,z,y появился моб
сервер клиенту: это моб имеющий ID флема
(тут игрок увидел появившегося вдалеке флема и тычет на него мышой)
клиент серверу: я хочу его выделить (это последствие попытки игрока выделить моба)
сервер клиенту: да, моб достаточно близко и он выделябельный. вам разрешается его выделить.
(игрок видит, что цель выделилась и нажимает атаку)
клиент серверу: я хочу атаковать выделенного моба (реакция на попытку игрока атаковать)
сервер клиенту: атаковать запрещаю, так как ты далеко от моба, для этого типа атаки.
клиент серверу: ок, бегу к мобу
сервер клиенту: разрешаю.
(игрок видит что его чар побежал к мобу)
сервер клиенту: ты передвинулся. Теперь ты в координатах рядом с мобом.
(игрок видит, что он прибежал к мобу)
.
.
клиент серверу: я хочу атаковать выделенного моба
сервер клиенту: разрешаю, вы нанесли ему 100 урона.
(здесь и далее игрок видит присланные от сервера цифры урона)
клиент серверу: я опять атакую моба
сервер клиенту: разрешаю, вы нанесли ему еще 120 урона
сервер клиенту: а вот он вам ответил на 50 урона
сервер клиенту: у вас теперь здоровья меньше на 50
(здесь игрок видит как его здоровье уменьшилось)
клиент серверу: и опять я атакую моба
сервер клиенту: а вот фиг тебе, атаку запрещаю.
(здесь игрок увидел, что атака не прошла, урона не было)
сервер клиенту: так как моб умер.
(здесь игрок увидел, что моб упал и сдох)
сервер клиенту: ой, из него выпала шняга
(здесь повяляется дроп)
сервер клиенту: и еще одна шняга
(здесь повяляется еще дроп)
клиент серверу: я хочу поднять шнягу
сервер клиенту: разрешаю. Вы подняли шнягу.
(шняга упала к нам в инвентарь и изчезла с земли)
.
.
..
Дальше все в таком же духе.
Я привел пример очень грубо, без указания аргументов пакетов и какой либо конкретики вообще. (да и логику протокола немного покоробил)
Но, здесь видно главное что я хотел сказать — клиент не управляет сервером, он всего лишь ‘запрашивает’ разрешение на действия, а сервер уже решает что и сколько и как именно и что за это будет клиенту.
Надеюсь, что теперь флуда и глупостей на форуме будет меньше. 😉
В догонку, возникает вопрос, а как тогда возможно читерить, если за всем следит только сервер, а клиент ничего не решает ?
Мой ответ простой: Сервера пишут не боги, а люди. Следовательно, сервера также часто ошибаются, главное, надо найти как можно обмануть сервер на его ошибках так, чтобы это было выгодно.
Злой и Жестокий критик и баголов. Вижу очень далеко, насквозь.
Быстрей наслаждайся иллюзией, пока кто нибудь не растоптал твои грезы.
Архитектура сервера онлайн-игры на примере Skyforge
Привет, Хабр! Я Андрей Фролов, ведущий программист, работаю в Mail.Ru над Next-Gen MMORPG Skyforge. Вы могли читать мою статью про архитектуру баз данных в онлайн-играх. Сегодня я буду раскрывать секреты, касающиеся устройства сервера Skyforge. Постараюсь рассказать максимально подробно, с примерами, а также объясню, почему было принято то или иное архитектурное решение. По нашему серверу без преувеличения можно написать целую книгу, поэтому для того, чтобы уложиться в статью, мне придется пройтись только по основным моментам.

Обзор
- Сервер — это почти два миллиона строк кода на Java. Для соединения с сервером и отображения красивой картинки используется клиент, написанный на C++.
- Свой вклад в серверный код внесли полсотни программистов. Код писался в течение многих лет лучшими специалистами российского «православного» геймдева. В нем собраны все самые удачные идеи со всего мира.
- На текущий момент у нас написано около 5200 автоматических тестов, налажен continuous integration и нагрузочное тестирование с помощью ботов.
- Сервер умеет запускаться и работать на десятках и сотнях серверов, поддерживать игру сотен тысяч человек одновременно. Мы решили отказаться от традиционной для MMO техники шардирования и запустить всех игроков в один большой мир.
Первое и главное правило разработки сервера: клиент в руках врага. Клиент защищен, но чисто теоретически и его могут хакнуть, могут расшифровать клиент-серверный протокол. Взлом клиента может привести к обходу игровых правил, читам, ботоводству и т.п. Такие вещи разрушают игру для всех. Чтобы этого не произошло, мы должны эмулировать весь игровой мир со всеми игровыми правилами у себя на сервере, а клиент использовать только для отображения красивой картинки. Кроме того, клиент надо проверять на взлом, отслеживая подозрительное поведение и т.д.

Сервисная архитектура
Одна из основных особенностей разработки состоит в том, что мы не знаем, сколько у нас будет игроков. Может быть, всего один — сам разработчик, а может, 100000 одновременно. Поэтому сервер должен уметь запускаться в маленькой конфигурации, на ноутбуке, и растягиваться при необходимости на десятки и сотни мощных серверов.
Вторая особенность состоит в том, что при старте разработки мы понятия не имели, о чем будет наша игра, какие в ней будут особенности, сервисы и т.п. Структура сервера должна быть максимально гибкой в плане добавления новых сервисов и фич.
Третья большая проблема — это многопоточность. Как известно, лучший способ сладить с многопоточностью — это избежать ее. Deadlock, livelock, lock contention и другие милые сердцу программиста проблемы можно обойти, если архитектура сервера будет избавлять вас от необходимости синхронизировать потоки вручную. В идеале программист вообще должен писать простой однопоточный код и не задумываться о такого рода вещах.
- Существует пул физических серверов, на которых будет запускаться игра. Этот набор серверов и наше серверное приложение, которое на них запущено, называется Realm.
- На каждом сервере запускается серверное приложение (JVM), называемое ролью. Роли бывают разные: аккаунт-сервер, игровая механика, чат и т.д. Каждая роль берет на себя большой кусок функционала. Некоторые роли существуют в единственном числе, некоторые запускаются в нескольких экземплярах.
- Роль состоит из набора сервисов. Сервис — это обычный поток (thread), который занимается своей, специфичной для него задачей. Примером сервиса может служить сервис авторизации, сервис резервирования имен, балансировщик нагрузки и т.п. Каждый сервис ничего не знает о физическом расположении других сервисов. Они могут быть рядом, а могут быть на другой физической машине. Сервисы взаимодействуют через систему сообщений, которая скрывает от них такого рода подробности.
- Каждый сервис состоит из набора модулей. Модуль — это «кусок функциональности», у которого есть один метод tick(). Примером модуля может быть модуль статистики, модуль исполнения транзакций, модуль синхронизации времени. Вся работа сервиса заключается в том, чтобы в бесконечном цикле поочередно вызывать метод tick() у своих модулей. Один такой цикл называется «кадр сервера». Мы считаем показатель хорошим, если кадр сервера колеблется в пределах от 3 до 20 мс.
- Вся эта структура описывается в XML-файлах. Системе запуска надо просто «скормить» название роли. Она найдет соответствующий файл, запустит все нужные сервисы и отдаст им списки модулей. Сами модули создадутся с помощью java reflection.

Таким образом, мы можем запустить роль «локальный сервер», где будет все необходимое, а можем разбить сервер на несколько десятков ролей — аккаунт-сервер, итем-сервер, игровая механика и т.д. — и запускать его на десятках разных физических серверов. Структура оказалась чрезвычайно гибкой и удобной, советую серьезно к ней присмотреться.
Основные сервисы
- Аккаунт-сервис. Отвечает за авторизацию и подключение новых клиентов.
- Сервер игровой механики. Тут происходит, собственно, сама игра. После прохождения авторизации клиент подключается сюда и тут играет. С другими сервисами клиент напрямую не взаимодействует. Таких сервисов можно и нужно запускать несколько десятков, а то и сотен. Именно эти сервисы несут основную нагрузку.
- Сервисы баз данных. Эти сервисы выполняют операции над данными игровых персонажей, их предметами, деньгами, прогрессом развития. Их обычно запускается несколько штук. Подробнее об архитектуре баз данных можно прочитать в моем прошлом докладе. ( habrahabr.ru/company/mailru/blog/182088 )
- Чат. Занимается роутингом сообщений чата между пользователями.
- Все остальные сервисы. Их несколько десятков, и они обычно не создают сильной нагрузки, поэтому не требуют обособленных серверов.

Под словом «сеть» я подразумеваю систему доставки сообщений от одного сервиса к другому или от одного объекта к другому. Исторически так сложилось, что у нас существует сразу две такие системы. Одна основана на сообщениях. Вторая система основана на удаленном вызове процедур (RPC). В Skyforge система сообщений применяется внутри сервиса игровой механики, чтобы послать какое-то сообщение от аватара к мобу, а также для общения клиента и сервера. RPC используется для общения между сервисами.

Сообщения
Все объекты, которые хотят посылать или принимать сообщения, называются абонентами. Каждый абонент регистрируется в общей директории и получает уникальный идентификатор — адрес. Любой, кто хочет послать сообщение какому-либо абоненту, должен указать адреса «откуда» и «куда». Сетевой движок знает, где находится абонент, и доставляет ему сообщение. Сообщение — это Java-объект, у которого есть метод run(). При отправке этот объект сериализуется, прилетает к целевому абоненту, там десериализуется, а затем вызывается метод run() над целевым абонентом.

Такой подход очень удобен тем, что позволяет реализовывать простые команды типа «нанести удар», «выдать анлок», «запустить фаербол». Вся эта логика оказывается внешней по отношению к объекту, над которым выполняется действие. Большой минус этого подхода в том, что если логика команды требует выполнения какого-либо кода на нескольких абонентах, то нам потребуется сделать несколько сообщений, которые будут посылать друг друга по цепочке. Логика оказывается фрагментирована на несколько классов, и цепочки сообщений часто довольно долго и сложно распутывать.
Удаленный вызов процедур или RPC появился, чтобы решить проблему цепочек сообщений.
Основная идея заключается в использовании кооперативной многозадачности (Coroutine, Fibers). Тому, кто не знаком с это концепцией, для понимания темы советую заглянуть в «Википедию». en.wikipedia.org/wiki/Coroutine.
Сервис, который хочет, чтобы его могли вызывать через удаленный вызов процедур, должен реализовывать специальный интерфейс и зарегистрировать в специальной директории. Тогда любой желающий может попросить директорию дать ему интерфейс этого сервиса, и директория вернет специальный враппер над сервисом. Вызывать сервисы по RPC можно только внутри файбера (coroutin), т.е. специального контекста исполнения, который можно прерывать и возобновлять в точках разрыва. При вызове методов враппера он будет посылать RPC вызовы на удаленный сервис, прерывать текущий файбер в ожидании ответа и возвращать результат, когда удаленный сервер ответит.

Таким образом, мы концентрируем логику в одном методе, а не размазываем ее по сотням сообщений. Код сильно упрощается, его можно писать в терминах вызова функций каких-то объектов, а не в терминах посылки сообщений. Но возникают проблемы с неким подобием многопоточности, т.к. после того, как мы вернулись из удаленного вызова, окружение уже могло измениться. В целом такой подход очень удобен, когда у сервиса есть ограниченный интерфейс из десятка методов. Когда методов становится много, интерфейс лучше разбивать на несколько.
Подробнее о нашей имплементации файберов можно узнать из лекции Сергея Загурского ( www.youtube.com/watch?v=YWLHELcvNbE ).
Сериализация
Чтобы у нас работала система посылки сообщений и удаленный вызов процедур, нам нужен клиент-серверный протокол и способ сериализации/десериализации объектов. Напомню, что у нас есть необходимость пересылать команды и данные с клиента на сервер, т.е. из C++ в Java и обратно. Для этого мы по Java-классам генерируем их копии в C++, а также генерируем методы для сериализации и десериализации объектов в байтовый поток. Код для сериализации встраивается прямо внутрь классов и обращается к полям класса напрямую. Таким образом, сервер не тратит процессорное время на обход классов с помощью reflection. Все это мы генерируем с помощью самописного плагина для IntelliJ IDEA. Внутрисерверный протокол для общения между сервисами полностью аналогичен клиент-серверному протоколу.
При сериализации какого-либо класса в байтовый поток, сначала пишется id класса, потом данные полей этого класса. На другой стороне считывается id, выбирается соответствующий класс и у него вызывается специальный конструктор, который восстанавливает класс из байтового потока.

Игровая механика
Основной сервис, который был бы вам интересен, это сервис игровой механики. Именно там выполняется весь код, непосредственно связанный с игрой, именно там моделируется весь игровой мир, летают фаерболы и «грабятся корованы».
Карты и балансировка нагрузки
На серверах игровой механики создаются карты, на которых, собственно, находятся игроки, мобы и происходит все веселье. У каждой карты есть лимит на количество игроков, которые могут на ней находиться. Например, лимит может быть равен единице для персональных приключений, 10–30 для групповых активностей и 250 для больших карт. Что происходит, если на карту захочет попасть еще один игрок, когда лимит исчерпан? Тогда будет создана еще одна копия той же самой карты. Игроки с этих карт не будут видеть друг друга, не будут друг другу мешать. Т.е. в каком-нибудь игровом городе могут быть тысячи человек, но там не будет тесно. Такой способ организации игроков называется «каналы».
За создание карт отвечает центральный сервис «балансировщик карт», который распределяет карты по сервисам игровой механики в зависимости от популяции, нагрузки и других магических причин, стараясь поддерживать равномерное распределение нагрузки и нормальную плотность играющих, чтобы им не было скучно.
На каждом сервере игровой механики загружена информация о карте проходимостей, коллизиях и других подобных вещах. Когда игрок или моб пытается двинуться в какую-либо точку, то сервер просчитывает, может ли игрок туда попасть, не пытается ли он считерить и пройти сквозь стену. Когда игрок пытается кинуть во врага фаербол, то по этой же информации сервер рассчитывает, видит ли игрок врага и нет ли на его пути препятствий.

Аватары и мобы
Аватар — это персонаж, которым управляет игрок, моб — это монстр, которого игрок убивает. Это весьма разные, но часто очень похожие сущности. И моб, и аватар умеют ходить по карте, у них есть здоровье, они могут использовать заклинания и т.п. Только аватаром управляет игрок, а у моба есть свой мозг. Кроме того, на картах есть множество всяких сундуков, растений и других интерактивных сущностей. Очень часто нужно делать некую функциональность и цеплять ее к разным сущностям. Для этих целей мы используем компонентный подход, собирая игровую сущность из набора функциональностей. Поясню на примере. Допустим, у игрока и моба есть показатель здоровья. В таком случае мы оформляем элемент «здоровье» как отдельный Java-класс, в котором описываем, как здоровье себя ведет: как оно может уменьшаться, как восстанавливаться, какие есть таймеры и т.п. Потом мы просто складываем все функциональности в специальную HashMap внутри сущности и берем ее оттуда по необходимости. Таких компонент у нас существуют сотни, на них собрана половина игровой механики.
Так как серверное приложение очень сложное, неизбежно возникновение ошибок. Нужно сделать так, чтобы возникновение ошибки, даже необработанного NullPointerException, не приводило к падению сервера. Можно ошибку просто залогировать и пойти дальше, но если ошибка возникнет посреди какого-то длинного действия над аватаром, то аватар может оказаться в сломанном и неконсистентном состоянии. Тут нам на помощь приходит концепция под названием «локаль». Локаль — это контекст, внутри которого объекты могут ссылаться друг на друга. Объекты из одной локали не могут ссылаться на объекты из другой. Если из локали вылетает необработанное исключение, то локаль удаляется целиком. Аватары, мобы и другие сущности являются локалями, удаляются целиком и не могут держать ссылок на других аватаров и мобов. Поэтому все взаимодействие между аватарами и мобами идет через систему сообщений, хотя они находятся вместе на одной машине и в теории могли бы держать друг на друга прямую ссылку.
Репликация
Моделировать игровой мир нужно не только на сервере, но и частично на клиенте. Например, клиенту нужно видеть других игроков и мобов, которые находятся рядом с ним. Для этого используется механизм клиент-серверной репликации, когда с сервера клиентам рассылаются обновления окружающего игрового мира. Делается это с помощью генератора кода, который встраивает отсылку обновлений в сеттеры серверных Java-объектов. Вокруг игрока создается круг определенного радиуса, и если кто-то, например другой аватар, попадает в этот круг, он начинает реплицироваться на клиент. С репликацией есть фундаментальная проблема. Если в одном месте столпится N аватаров, то на каждого из них нужно будет посылать N реплик. Таким образом возникает квадратичная зависимость, что ограничивает количество аватаров, которые могут собраться в одном месте. Именно из-за этой фундаментальной квадратичности клиенты всех ММО тормозят в столицах. Мы избегаем этой проблемы, ограничивая количество игроков на карте и распределяя их по каналам.
Ресурсная система
В игре существуют сотни и тысячи заклинаний, предметов, квестов и других подобных сущностей. Как вы, наверное, догадываетесь, программисты не пишут все сотни квестов, это делают геймдизайнеры. Программист разрабатывает один Java-класс квеста, а описания всех квестов с их логикой, задачами и текстами содержатся в XML-файлах, называемых ресурсами. При старте сервера мы загружаем эти ресурсы и на их основе собираем Java-классы с описанием мира. Этими классами уже может пользоваться сервер. Примерно такая же система существует и на стороне клиента, только там ресурсы не грузятся из XML-файлов, а просто загружается заранее созданный «кусок памяти», содержащий все нужные объекты и ссылки между ними. Ресурсных файлов у нас существует несколько сотен тысяч, но их загрузка на сервере занимает около двух минут. На клиенте же все грузится за секунды. Система очень навороченная, поддерживает такие фичи, как прототипы и наследование, вложенные описатели и т.п. Поверх ресурсной системы у нас созданы специализированные программы для редактирования карт и остальных игровых сущностей.

Сервер в действии
Давайте теперь на примерах рассмотрим несколько сценариев того, как работает вся эта система в действии.
Как работают онлайн-игры и что с ними может пойти не так
Миллионы игроков по всему миру каждый день проводят долгие часы, играя в онлайн-игры, где мы можем соревноваться с людьми со всего мира в режиме реального времени. Этот вид развлечения просто работает, поэтому многие из нас не задаются вопросом, что происходит «под капотом». Какие процессы должны произойти, чтобы сбить с ног цифровую аватарку игрока, сидящего перед компьютером за сотни километров от нас. Мы решили поговорить с создателем онлайн-игры https://thebattle.club/ru, популярной в сети, о том, как работают онлайн-игры.
Как работают онлайн-игры и что с ними может пойти не так
Интересно, как выглядит информационный поток в онлайн-игре. Предположим, игрок бьет другого игрока по голове. Какие данные необходимо отправить и получить, чтобы эффект выглядел так, как ожидалось?
Я опишу пример потока данных на основе нашего примера, т.е. взаимодействия клиент-сервер. Чтобы подчеркнуть, клиент — это игрок, а сервер — это центральный блок, который собирает информацию от всех игроков и распространяет информацию между ними.
Движение игрока
- Каждый игрок отправляет на сервер данные о своей текущей позиции и ротации.
- Сервер получает данные и затем обновляет их в памяти.
а) В нашем случае мы дополнительно анализируем здесь, не пытается ли игрок сжульничать, телепортируясь в другое место. При этом проверяем, чтобы позиция игрока между обновлениями данных не изменилась слишком сильно и чтобы у него хватило выносливости, чтобы преодолеть дистанцию на такой скорости.
- Сервер отправляет обратно каждому игроку положение и поворот всех персонажей в поле зрения этого игрока.
- За счет математических функций интерполяции и экстраполяции устранен эффект скачков символов между позициями, полученными в последующих обновлениях. Другими словами, игрок испытывает более плавное движение аватара.
Стрельба на примере лука:
- Игрок сообщает серверу, что он произвел выстрел, отправив сгенерированный идентификатор, позицию, вращение пули и силу натяжения (от того, как долго мы натягивали нить, зависит, с какой силой полетит стрела).
- Сервер сохраняет данные пули и время ее выстрела. В каждом случае выстрел анализируется:
а) Мог ли быть произведен выстрел? Стрела должна быть достаточно близко к игроку, а не только рядом с головой противника.
б) У игрока должны быть боеприпасы.
- Сервер сохраняет уменьшенное количество стрел и затем сообщает игроку об изменении количества пуль в инвентаре.
Частота обновлений зависит от типа игры и количества игроков. В нашем случае последующие обновления ротации и положения отправляются с сервера игрокам каждые 100 мс. В игре, ориентированной на стрельбу из пушек, мы, вероятно, уменьшили бы это время до 50 мс, тогда как в такой игре, как World of Warcraft, где положение самого игрока не критично, 150-200 мс, вероятно, не изменили бы игровой процесс.
Обратите внимание, что эти обновления основаны на постоянно меняющихся данных, таких как позиция или ротация игрока. Все разовые действия, такие как стрельба, сбор чего-либо или использование умений, синхронизируются без задержек.
Состояние мира у всех игроков должно быть одинаковым, поэтому все вышеперечисленные элементы синхронизированы. В случае больших карт и часто меняющегося состояния объектов можно попробовать их кластеризовать и обновлять игрока только элементами из кластеров, находящихся в пределах его досягаемости. Однако в этом случае, когда новый кластер (который не был синхронизирован) входит в его область действия, он должен быть инициализирован текущим состоянием с сервера.
Как работают онлайн игры? [дубликат]
Всем привет! Как работают онлайн шутеры, а именно как работает их геймплей? Например PUBG.
В моём представление, они работают так: Есть сервер и клиент. Каждый кадр отсылается GET и POST запрос на сервер, в get мы получаем информацию об игроках, а в post передаём нашу информацию (Координаты и тд). Но, как по мне, такой способ является очень плохим, ведь даже с +- хорошим интернетом такие манипуляции могут занимать большое время, особенно в шутере, где каждая миллисекунда важна
UPD
Всем спасибо за ответы и комментарии, как я понял, то в гемйплее онлайн игр используется UDP протокол, а так-же очень интересная механика: Игра сама прощитывает траекторию движения соперника, так, например, если игрок двигается вперёд, то в случае обрыва его соединения игра будет двигать игрока вперёд бесконечно. На первый взгляд такая механика кажется немного странной, но если немного порассуждать, то выходит, что она довольна логична. К слову, в PUBG, по всей видимости, используется такая же механика, т.к. несколько раз я встречал подобные явления: игрока выкинуло из игры, но при этом машина, которую он вёл, продолжала движение