Я хочу создать простую многопользовательскую игру клиент-сервер в реальном времени в качестве проекта для моего сетевого класса.
Я много читал о моделях многопользовательских сетей в реальном времени, и я понимаю отношения между клиентом и сервером и методы компенсации лагов.
Я хочу сделать что-то похожее на сетевую модель Quake 3: в основном, сервер хранит снимок всего игрового состояния; после получения входных данных от клиентов сервер создает новый снимок, отражающий изменения. Затем он вычисляет различия между новым снимком и последним и отправляет их клиентам, чтобы они могли синхронизироваться.
Мне кажется, что такой подход действительно надежен - если клиент и сервер имеют стабильное соединение, для синхронизации их будет отправлен только минимально необходимый объем данных. Если клиент не синхронизирован, можно также запросить полный снимок.
Однако я не могу найти хороший способ реализовать систему снимков. Мне действительно трудно отойти от архитектуры программирования для одного игрока и подумать о том, как можно сохранить состояние игры таким образом, чтобы:
- Все данные отделены от логики
- Различия могут быть рассчитаны между снимками состояний игры
- Игровыми сущностями все еще легко манипулировать с помощью кода
Как реализован класс снимка ? Как хранятся сущности и их данные? Каждый ли объект клиента имеет идентификатор, который совпадает с идентификатором на сервере?
Как рассчитываются различия между снимками?
В целом: как будет реализована система снимков состояния игры?
источник
Ответы:
Вы можете вычислить дельту моментального снимка (изменения его предыдущего синхронизированного состояния), сохранив два экземпляра моментальных снимков: текущий и последний синхронизированный.
Когда приходит клиентский ввод, вы изменяете текущий снимок. Затем, когда пришло время отправить дельту клиентам, вы вычисляете последний синхронизированный снимок с текущим полем (рекурсивно) и вычисляете и сериализуете дельту. Для сериализации вы можете назначить уникальный идентификатор каждому полю в области его класса (в отличие от глобальной области состояния). Клиент и сервер должны совместно использовать одну и ту же структуру данных для глобального состояния, чтобы клиент понимал, к чему применяется конкретный идентификатор.
Затем, когда вычисляется дельта, вы клонируете текущее состояние и делаете его последним синхронизированным, так что теперь у вас есть идентичное текущее и последнее синхронизированное состояние, но разные экземпляры, так что вы можете изменять текущее состояние и не влиять на другое.
Этот подход легче реализовать, особенно с помощью рефлексии (если у вас есть такая роскошь), но он может быть медленным, даже если вы сильно оптимизируете часть рефлексии (создавая схему данных для кэширования большинства вызовов рефлексии). Главным образом потому, что вам нужно сравнить две копии потенциально большого состояния. Конечно, это зависит от того, как вы реализуете сравнение и свой язык. Это может быть быстрым в C ++ с жестко закодированным компаратором, но не настолько гибким: любое изменение вашей глобальной структуры состояний требует модификации этого компаратора, и эти изменения настолько часты на начальных этапах проекта.
Другой подход заключается в использовании грязных флагов. Каждый раз, когда поступает клиентский ввод, вы применяете его к своей единственной копии глобального состояния и помечаете соответствующие поля как грязные. Затем, когда пришло время синхронизировать клиентов, вы сериализируете грязные поля (рекурсивно), используя те же уникальные идентификаторы. (Незначительный) недостаток заключается в том, что иногда вы отправляете больше данных, чем строго требуется: например,
int field1
изначально было 0, затем присвоено 1 (и помечено как грязное), а после этого снова присвоено 0 (но остается грязным). Преимущество заключается в том, что, обладая огромной иерархической структурой данных, вам не нужно анализировать ее полностью для вычисления дельты, только грязные пути.В общем, эта задача может быть довольно сложной, зависит от того, насколько гибким должно быть окончательное решение. Например, Unity3D 5 (готовится к выпуску) будет использовать атрибуты для указания данных, которые должны автоматически синхронизироваться с клиентами (очень гибкий подход, вам не нужно ничего делать, кроме добавления атрибута к вашим полям), а затем генерировать код как шаг после сборки. Подробнее здесь.
источник
Во-первых, вам необходимо знать, как представлять соответствующие данные в соответствии с протоколом. Это зависит от данных, относящихся к игре. Я буду использовать игру RTS в качестве примера.
В целях создания сетей все объекты в игре перечислены (например, пикапы, юниты, здания, природные ресурсы, разрушаемые предметы).
Игроки должны иметь данные, относящиеся к ним (например, все видимые единицы):
Сначала игрок должен получить полное состояние, прежде чем он сможет войти в игру (или, альтернативно, всю информацию, относящуюся к этому игроку).
Каждый блок имеет целочисленный идентификатор. Атрибуты перечисляются и, следовательно, также имеют интегральные идентификаторы. Идентификаторы устройств не должны быть длиной 32 бита (это может быть, если мы не бережливы). Это вполне может быть 20 бит (оставляя 10 бит для атрибутов). Идентификатор юнита должен быть уникальным, он вполне может быть назначен счетчиком при создании экземпляра юнита и / или добавлении его в игровой мир (здания и ресурсы считаются неподвижным юнитом, а ресурсам может быть присвоен идентификатор, когда карта загружен).
Сервер хранит текущее глобальное состояние. Последнее обновленное состояние каждого игрока представлено указателем
list
последних изменений (все изменения после указателя еще не были отправлены этому игроку). Изменения добавляются к тому,list
когда они происходят. Как только сервер завершит отправку последнего обновления, он может начать перебирать список: сервер перемещает указатель игрока по списку к его хвосту, собирая все изменения по пути и помещая их в буфер, который будет отправлен игрок (т. е. формат протокола может быть примерно таким: unit_id; attr_id; new_value) Новые юниты также считаются изменениями и отправляются со всеми значениями своих атрибутов принимающим игрокам.Если вы не используете язык со сборщиком мусора, вам нужно установить ленивый указатель, который будет отставать, а затем догонять самый устаревший указатель игрока в списке, освобождая объекты по пути. Вы можете вспомнить, какой игрок является самым устаревшим в куче приоритетов, или просто выполнить итерацию и освободить, пока ленивый указатель не станет равным (т.е. указывает на тот же элемент, что и один из указателей игрока).
Некоторые вопросы, которые вы не подняли, и я думаю, являются интересными:
источник