[Перевод] Сетевая физика в виртуальной реальности

image


Введение
Около года назад ко мне обратилась компания Oculus с предложением проспонсировать мои исследования. По сути, они сказали следующее: «Привет, Гленн, существует большой интерес к передаваемой по сети физике для VR, а Вы сделали отличный доклад на GDC. Как считаете, сможете ли Вы подготовить образец сетевой физики в VR, который мы могли бы показывать разработчикам? Возможно, Вам удастся использовать сенсорные контроллеры?»

Я ответил «Да, чёрт побери!» «Кхм. Разумеется. Это будет весьма интересно!» Но чтобы быть честным, я настоял на двух условиях. Первое: разработанный мной исходный код должен быть опубликован под достаточно свободной лицензией open source (например, BSD), чтобы мой код принёс наибольшую пользу. Второе: когда я закончу, я буду иметь право написать статью, описывающую шаги, предпринятые мной для разработки этого образца.

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

Что мы будем строить?
Когда я впервые приступил к обсуждениям проекта с Oculus, мы представляли создание чего-то наподобие стола, за которым могут сидеть четыре игрока и взаимодействовать лежащими на столе физически симулируемыми кубиками. Например, бросать их, ловить и строить башни, может быть, разрушать башни друг друга взмахами рук.

Но после нескольких дней изучения Unity и C# я наконец оказался внутри Rift. В VR очень важен масштаб. Когда кубики были маленькими, всё было не особо интересно, но когда их размер вырос примерно до метра, то появилось замечательное ощущение масштаба. Игрок мог создавать огромные башни из кубиков, до 20–30 метров в высоту. Ощущения были потрясающими!

Невозможно визуально передать, как всё выглядит в VR, но примерно похоже на это:

e67ae2062b28705f67ae6de1deaaaba7.jpg


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

Хотя всё это и очень интересно, но не всё так безоблачно. Работая с Oculus как клиент, прежде чем приступать к работе, я должен был определить задачи и необходимые результаты.

Я предложил в качестве оценки успеха следующие критерии:

  1. Игроки должны иметь возможность подбирать, бросать и ловить кубики без задержки.
  2. Игроки должны иметь возможность складывать кубы в башни и эти башни должны становиться стабильными (приходить в состояние покоя) и без заметного дрожания.
  3. Когда брошенные любым из игроком взаимодействуют с симуляцией, такие взаимодействия должны происходить без задержек.


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

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

Мы могли выбрать одну из трёх основных сетевых моделей:

  1. Deterministic lockstep
  2. Клиент-сервер с прогнозированием на стороне клиента
  3. Распределённая симуляция со схемой полномочий


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

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

Причина этого в том, что для сокрытия задержек в модели deterministic lockstep мне нужно было бы хранить две копии симуляции и заранее прогнозировать полномочную симуляцию с локальным вводом до рендеринга (стиль GGPO). При частоте симуляции 90 ГЦ и при задержке вплоть до 250 мс это означало, что для каждого кадра визуального рендеринга потребуется 25 шагов симуляции физики. Затраты в 25X просто нереалистичны для физической симуляции с интенсивным использованием ЦП.

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

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

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

Схема полномочий
Интуитивно понятно, что получение полномочий (на работу в качестве сервера) на объекты, с которыми вы взаимодействуете, может сокрыть задержки — вы же сервер, поэтому у вас нет никаких задержек, верно? Однако не совсем очевидно, как в таком случае разрешать конфликты.

Что, если с одной башней взаимодействуют два игрока? Если два игрока из-за задержки хватают один и тот же куб? В случае конфликта кто победит, чьё состояние корректируется, и как принимать такие решения?

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

Я поразмышлял об этом какое-то время и пришёл к двум основным концепциям:

  1. Полномочия
  2. Владение


У каждого куба будут иметься полномочия, или со значением по умолчанию (белый цвет), или цвета игрока, с которым он взаимодействовал последним. Если с объектом взаимодействовал другой игрок, то полномочия сменяются и переходят к этому игроку. Я планировал использовать полномочия для взаимодействий бросаемых в сцене объектов. Я представлял, что куб, брошенный игроком 2, может брать полномочия над всеми объектами, с которыми он взаимодействовал, а те в свою очередь рекурсивно со всеми объектами, с которыми взаимодействовали они.

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

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

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

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

4bdafe200846a9f00422627e6540f62a.png


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

bed1498af59cedf894f29cfdfd50c707.png


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

8e91d7680db1ade306783fa7dfba2cfb.png


Состояние, получаемое от каждого куба, выглядит вот так:

struct CubeState
{
    Vector3 position;
    Quaternion rotation;
    Vector3 linear_velocity;
    Vector3 angular_velocity;
};


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

Этого простого изменения достаточно, чтобы синхронизировать левую и правую симуляции. За 1/10 секунды PhysX не успевает достаточно отклониться между обновлениями, чтобы продемонстрировать какие-либо заметные колебания.

862703460db41f0154f7df0c12abe6c3.png


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

Оптимизация пропускной способности
Чтобы обеспечить играбельность образца сетевой физики через Интернет, мне нужно было контролировать пропускную способность.

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

[position] (vector3)
[rotation] (quaternion)
[at rest] (bool)

{
    [linear_velocity] (vector3)
    [angular_velocity] (vector3)
}


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

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

Но хотя это и снижает нагрузку на канал, в то же время возрастает риск. Я опасался, что при передаче по сети башни из кубов (например, 10–20 поставленных друг на друга кубов) дискретизация может создавать ошибки, приводящие к дрожанию этой башни. Возможно, оно даже может привести к неустойчивости башни, но особенно раздражающим и сложным в отладке способом, а именно когда башня выглядит для вас нормально, и неустойчива только при удалённом просмотре (т.е. при симуляции без полномочий), когда другой игрок наблюдает за тем, что вы делаете.

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

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

Переход в состояние покоя
Но дискретизация физического состояния создала некие очень интересные побочные эффекты!

  1. Движку PhysX на самом деле не очень нравится, когда его заставляют менять состояние каждого твёрдого тела в начале каждого кадра, и он даёт нам об этом знать, потребляя большую часть ресурсов ЦП.
  2. Дискретизация добавляет к позиции ошибку, которую PhysX упорно пытается устранить, немедленно и с огромными скачками выводя кубы из состояния проникновения друг в друга!
  3. Повороты тоже невозможно представить точно, что тоже приводит к взаимопроникновению кубов. Интересно, что в этом случае кубы могут застрять в петле обратной связи и начать скользить по полу!
  4. Хотя кубы в больших башнях кажутся находящимися в состоянии покоя, при внимательном изучении в редакторе выясняется, что на самом деле они колеблются на небольшие величины, поскольку кубы дискретизируются немного над поверхностью и падают на неё.


Я почти ничего не мог предпринять для решения проблемы с потреблением движком PhysX ресурсов ЦП, но нашёл решение для выхода из взаимопроникновения объектов. Я задал для каждого твёрдого тела скорость maxDepenetrationVelocity, ограничив скорость, с которой могут отталкиваться кубы. Оказалось, что достаточно хорошо подходит скорость один метр в секунду.

Привести кубы в состояние покоя оказалось гораздо сложнее. Найденное мной решение заключается в полном отключении расчётов состояния покоя самого движка PhysX и замене их кольцевым буфером позиций и поворотов для каждого куба. Если куб не двигался и не поворачивался на значительные величины в течение последних 16 кадров, то я принудительно заставляю его перейти в состояние покоя. Бум! В результате мы получили идеально устойчивые башни с дискретизацией.

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

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

Вот, как это работает на практике:

  1. У каждого куба есть показатель приоритета, который вычисляется в каждом кадре. Чем больше значения, тем выше вероятность их передачи. Отрицательные значения означают «этот куб передавать не нужно».
  2. Если показатель приоритета положителен, то он добавляется в значение накопителя приоритетов каждого куба. Это значение сохраняется между обновлениями симуляции таким образом, что накопитель приоритетов увеличивается в каждом кадре, то есть значения кубов с более высоким приоритетом растут быстрее, чем у кубов с низким приоритетом.
  3. Отрицательные показатели приоритета сбрасывают накопитель приоритетов до значения -1.0.
  4. При передаче пакета кубы сортируются по порядку от самого высокого значения накопителя приоритетов до самого низкого. Первые n кубов становятся набором кубов, которые потенциально могут быть включены в пакет. Объекты с отрицательными значениями накопителя приоритетов исключаются из списка.
  5. Пакет записывается и кубы сериализируются в пакет по порядку важности. В пакет не обязательно поместятся все обновления состояний, поскольку обновления кубов имеют кодировку переменных, зависящую от их текущего состояния (в покое, не в покое, и так далее). Следовательно, сериализация пакетов возвращает для каждого куба флаг, определяющий, был ли он включён в пакет.
  6. Значения накопителя приоритетов для кубов, переданные в пакете, сбрасываются до 0.0, что даёт другим кубам честный шанс на включение в следующий пакет.


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

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

Дельта-компрессия
Даже при использовании всех перечисленных выше способов передача данных по-прежнему недостаточно оптимизирована. Для игры на четырёх человек я хотел сделать затраты на одного игрока ниже 256 кбит/с, чтобы для хоста вся симуляция могла помещаться в канал 1 Мбит/с.

У меня в рукаве оставался последний трюк: дельта-компрессия.

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

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

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

Системы поддержки и структуры данных тоже должны стать гораздо сложнее:

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


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

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

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

Более сложная стратегия заключается в кодировании различий между текущим и опорным значениями, нацеленном на кодировании небольших изменений как можно меньшим количеством битов. Например, дельта позиции может быть (-1,+2,+5) относительно опорной точки. Я выяснил, что это хорошо работает для линейных значений, но плохо реализуется для дельт трёх наименьших компонентов кватернионов, поскольку самый большой компонент кватерниона часто отличается между опорной точкой и текущим поворотом.

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

Прогнозирование было осложнено тем, что код прогнозирования пришлось писать с фиксированной запятой, поскольку вычисления с плавающей запятой не гарантируют детерминированности. Но спустя несколько дней экспериментов я смог написать баллистический предсказатель для позиции, линейной и угловой скорости, который с дискретным разрешением примерно в 90% случаев соответствовал результатам интегратора PhysX.

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

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

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

Синхронизация аватаров
Через несколько месяцев работы я добился следующего прогресса:

  • Доказательство того, что синхронизация состояний в Unity и PhysX работает
  • Устойчивые башни кубов при удалённом просмотре при дискретизации состояния с обеих сторон
  • Занимаемый канал снижен до уровня, при котором четыре игрока могут поместиться в 1 Мбит/с


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

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

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

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

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

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

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

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

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

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

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

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

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

Для применения этих исправлений мне нужен был некий способ, позволяющий хосту перехватывать управление у гостей и говорить им: «нет, у тебя нет полномочий/владения этим кубом, и ты должен принять это обновление». Также мне нужен был какой-то способ, чтобы хост мог определять порядок взаимодействий гостей с миром, чтобы ни один из клиентов не ощущал увеличения задержек или серий поздней доставки пакетов; эти пакеты не должны получать предпочтения по отношению к более поздним действиям других гостей.

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

  1. Порядок полномочий
  2. Порядок владения


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

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

Порядковое число владения увеличивается каждый раз, когда игрок берёт куб. Владение «сильнее», чем полномочия, поэтому увеличение порядкового числа владения побеждает увеличение порядкового числа полномочий. Например, если игрок взаимодействует с кубом непосредственно перед тем, как его возьмём другой игрок, то взявший куб игрок побеждает.

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

Заключение
Высококачественную сетевую физику с устойчивыми башнями из кубов можно

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

Я благодарю компанию Oculus за спонсирование моей работы и возможность проведения этого исследования!

Исходный код образца сетевой физики можно скачать здесь.

© Habrahabr.ru