Хабрахабр

[Перевод] О сетевой модели в играх для начинающих

image

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

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

В архитектуре peer-to-peer (p2p) данные передаются между любыми парами подключенных игроков, а в клиент-серверной архитектуре данные передаются только между игроками и сервером. В целом существует два основных типа сетевых архитектур: peer-to-peer и клиент-серверная.

Поэтому в этом руководстве мы сосредоточимся на клиент-серверной архитектуре.
В частности, нас больше всего интересуют авторитарные серверы: в таких системах сервер всегда прав. Хотя архитектура peer-to-peer по-прежнему используется в некоторых играх, стандартом является клиент-серверная: она проще в реализации, требует канал меньшей ширины и облегчает защиту от читерства. Использование авторитарных серверов упрощает распознавание читеров. Например, если игрок думает, что находится в координатах (10, 5), а сервер говорит ему, что он в (5, 3), то клиент должен заменить свою позицию той, которую передаёт сервер, а не наоборот.

В игровых сетевых системах есть три основных компонента:

  • Транспортный протокол: как передаются данные между клиентами и сервером.
  • Протокол приложения: что передаётся от клиентов серверу и от сервера клиентам и в каком формате.
  • Логика приложения: как передаваемые данные используются для обновления состояния клиентов и сервера.

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

Сравнение TCP и UDP

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

Следовательно, он имеет те же ограничения. UDP является всего лишь тонким слоем поверх IP. Он обеспечивает надёжное упорядоченное соединение между двумя узлами с проверкой на ошибки. В отличие от него, TCP обладает множеством особенностей. Но все эти функции имеют свою цену: задержку. Следовательно, TCP очень удобен и используется во множестве других протоколов, например, в HTTP, FTP и SMTP.

Когда узел-отправитель передаёт пакет узлу-получателю, он ожидает получить подтверждение (ACK). Чтобы понять, почему эти функции могут вызывать задержку, надо разобраться, как работает TCP. Более того, TCP гарантирует получение пакетов в правильном порядке, поэтому пока утерянный пакет не получен, все остальные пакеты не могут быть обработаны, даже если они уже получены узлом-получателем. Если спустя определённое время он не получает его (потому что пакет или подтверждение было утеряно, или по каким-то другим причинам), то отправляет пакет повторно.

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

Например, он может помечать некоторые пакеты как надёжные, а другие — как ненадёжные. Собственный протокол на основе UDP может быть эффективнее TCP по различным причинам. Или он может обрабатывать несколько потоков данных, чтобы потерянный в одном потоке пакет не замедлял остальные потоки. Поэтому его не волнует, добрался ли ненадёжный пакет до получателя. Если сообщение чата, которое не является срочными данными, потеряно, то оно не замедлит срабатывание ввода, который является неотложным. Например, может существовать поток для ввода игрока и ещё один поток для сообщений чата. Или же собственный протокол может реализовать надёжность иначе, чем в TCP, чтобы быть более эффективным в условиях видеоигр.

Итак, если TCP такой отстойный, то мы будем создавать свой транспортный протокол на основе UDP?

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

Однако в большинстве FPS применяются собственные протоколы на основе UDP, поэтому ниже мы поговорим о них подробнее. Во многих успешных играх, в том числе World of Warcraft, Minecraft и Terraria, используется TCP.

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

TCP. Чтобы подробнее узнать о различиях между UDP и TCP в контексте многопользовательских игр, можно прочитать статью Гленна Фидлера UDP vs.

Собственный протокол

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

Рекомендую вам начать с более старой. Первая статья, Networking for Game Programmers 2008 года, проще, чем вторая, Building A Game Network Protocol 2016 года.

И после прочтения его статей вы наверняка переймёте у него мнение о том, что TCP имеет в видеоиграх серьёзные недостатки, и захотите реализовать собственный протокол. Учтите, что Гленн Фидлер — большой сторонник использования собственного протокола на основе UDP.

Для успешной реализации собственного транспортного протокола нужно предварительно многому научиться. Но если вы новичок в работе с сетями, то сделайте себе одолжение и используйте TCP или библиотеку.

Сетевые библиотеки

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

  • yojimbo Гленна Фидлера
  • RakNet, которая больше не поддерживается, но её форк SLikeNet похоже ещё активен.
  • ENet — это библиотека, созданная для многопользовательского FPS Cube
  • GameNetworkingSockets компании Valve

Я не пробовал их все, но предпочтение отдаю ENet, потому что она проста в использовании и надёжна. Кроме того, у неё есть понятная документация и туториал для начинающих.

Транспортный протокол: заключение

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

Во-первых, от потребностей игры: нужны ли ей низкие задержки? Выбор между TCP, UDP и библиотекой зависит от нескольких факторов. Как мы увидим из следующей части, можно создать протокол приложения, для которого вполне подойдёт ненадёжный протокол. Во-вторых, от требований протокола приложения: нужен ли ему надёжный протокол? Наконец, нужно ещё учитывать опытность разработчика сетевого движка.

У меня есть два совета:

  • Максимально абстрагируйте транспортный протокол от остальной части приложения, чтобы его можно было легко заменить, не переписывая весь код.
  • Не занимайтесь преждевременной оптимизацией. Если вы не специалист по сетям и не уверены, нужен ли вам собственный транспортный протокол на основе UDP, то можете начать с TCP или библиотеки, обеспечивающих надёжность, а затем протестировать и измерить производительность. Если возникают проблемы и вы уверены, что причина заключается в транспортном протоколе, то возможно настало время создавать собственный транспортный протокол.

В завершение этой части рекомендую вам прочитать Introduction to Multiplayer Game Programming Брайана Хука, в котором рассмотрено множество обсуждаемых здесь тем.
Теперь, когда мы можем обмениваться данными между клиентами и сервером, нужно решить, какие именно данные передавать и в каком формате.

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

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

Сериализация

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

Но это будет совершенно неэффективно и впустую займёт большую часть канала. В голову сразу приходит мысль использовать человекочитаемый формат, например JSON или XML.

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

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

Только убедитесь, что библиотека создаёт портируемые архивы и заботится о порядке байтов.

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

Гленн Фидлер написал о сериализации две статьи: Reading and Writing Packets и Serialization Strategies.

Сжатие

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

Битовая упаковка

Первая техника — это битовая упаковка. Она заключается в использовании ровно того количества битов, которое необходимо для описания нужной величины. Например, если у вас есть перечисление, которое может иметь 16 различных значений, то вместо целого байта (8 бит) можно использовать всего 4 бита.

Гленн Фидлер объясняет, как реализовать это, во второй части статьи Reading and Writing Packets.

Битовая упаковка особенно хорошо работает с дискретизацией, которая будет темой следующего раздела.

Дискретизация

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

Гленн Фидлер (опять!) показывает, как применять дискретизацию на практике, в своей статье Snapshot Compression.

Алгоритмы сжатия

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

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

  • Кодирование Хаффмана с заранее вычисленным кодом, которое чрезвычайно быстро и может давать хорошие результаты. Оно использовалось для сжатия пакетов в сетевом движке Quake3.
  • zlib — алгоритм сжатия общего назначения, который никогда не увеличивает объём данных. Как можно увидеть здесь, он применялся во множестве областей применения. Для обновления состояний он может оказаться избыточным. Но он может и пригодиться, если вам нужно отправлять клиентам с сервера ассеты, длинные тексты или рельеф.
  • Копирование длин серий — это, наверно, простейший алгоритм сжатия, но он очень эффективен для определённых типов данных, и может использоваться как этап предварительной обработки перед zlib. Он особенно подходит для сжатия рельефа, состоящего из тайлов или вокселей, в которых множество соседних элементов повторяется.

Дельта-сжатие

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

Вот две статьи, объясняющих способ её использования: Впервые она была применена в сетевом движке Quake3.

Гленн Фидлер также использовал её во второй части своей статьи Snapshot Compression.

Шифрование

Кроме того вам может понадобиться шифровать передачу информации между клиентами и сервером. На это есть несколько причин:

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

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

Протокол приложения: заключение

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

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

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

Техники сглаживания задержек

Все описанные в этом разделе техники подробно рассмотрены в серии Fast-Paced Multiplayer Габриэля Гамбетты. Я настойчиво рекомендую прочитать эту великолепную серию статей. В ней также есть интерактивное демо, позволяющее увидеть, как эти техники работают на практике.

Это называется прогнозированием на стороне клиента. Первая техника заключается в том, чтобы применять результат ввода напрямую, не ожидая ответа от сервера. Если это не так, то ему нужно просто изменить своё состояние согласно полученному от сервера, потому что сервер авторитарен. Однако когда клиент получает обновление от сервера, он должен убедиться, что его прогноз был верным. Подробнее о ней можно прочитать в статье Quake Engine code review Фабьена Санглара [перевод на Хабре]. Эта техника впервые была использована в Quake.

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

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

В 2014 году он написал новую серию статей Networking Physics, в которой описал другие техники для синхронизации симуляции физики. Гленн Фидлер (как всегда!) написал в 2004 году статью Network Physics (2004), в которой заложил фундамент синхронизации симуляции физики между сервером и клиентом.

Также в wiki компании Valve есть две статьи, Source Multiplayer Networking и Latency Compensating Methods in Client/Server In-game Protocol Design and Optimization в которых рассматривается компенсация задержек.

Предотвращение читерства

Существует две основные техники предотвращения читерства.

Как сказано выше, хорошим способом её реализации является шифрование. Первая: усложнение отправки читерами вредоносных пакетов.

Клиент не должен иметь возможности изменять состояние на сервере, кроме как отправкой ввода. Вторая: авторитарный сервер должен получать только команды/ввод/действия. Тогда сервер каждый раз при получении ввода должен перед его применением проверять его на допустимость.

Логика приложения: заключение

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

  • Блог Гленна Фидлера — стоит прочитать весь его блог, в нём есть множество отличных статей. Здесь собраны все статьи по сетевым технологиям.
  • Awesome Game Networking автора M. Fatih MAR — это подробный список статей и видео по сетевым движкам видеоигр.
  • В wiki сабреддита r/gamedev тоже есть множество полезных ссылок.
Показать больше

Похожие публикации

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

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

Кнопка «Наверх»