Как играть в многопользовательские игры
Пьеса «Разработка многопользовательской сетевой игры.» Часть 1: Архитектура
В общем, как и обещал, публикую серию статей по разработке многопользовательской сетевой игры. Изначально я хотел просто накатать статейку по разработке серверной части на интересном языке Scala. Но понял, что одной статейкой для развертывания темы не получится обойтись. А писать очередной топик обо всем и ни очем, не хотелось изначально. Поэтому встречайте пьесу в трех действиях. В течении которой мы разработаем архитектуру проекта, реализуем серверную и клиентскую части…
Все помнят прикольные танчики на денди?
Ну вот на примере этих танчиков и будем разрабатывать сервер и клиент.
Для любителей халявы и жестких копипастеров сразу скажу, что полных исходных кодов не будет. Буду выкладывать только отдельные интересные(сложные?) моменты как сервера так и клиента.
Итак приступим дорогие зрители.
В процессе нашего повествования вы окунетесь в мир технологий сетевых игр… познаете сладкий плод запретного scala кода и получите возможность вкусить кодинг под flash… и в качестве финального действия попробуем устроить хабраэффект готовому проекту, который я запущу на отдельном сервере для проверки получившейся архитектуры на реальном железе.
Часть первая. Действие первое: Общее описание проекта.
Распишем по порядку что же мы будем делать.
Так как перед нами не стоит задача сделать свой BigWorld (это движок такой для тех кто не в курсе) и мы делаем прототип, то упрощаем себе задачу везде где только возможно. Ибо наша основная задача показать как вообще разрабатываются сетевые игры. На конкретном, работающем примере. А не сделать готовый коммерческий продукт.
Задача: Сетевая игра типа «танчики на денди». Клиентская часть на Flash, сервер на Scala. Соединение через сокеты. Дабы убрать многие заморочки будем делать асинхронный сервер по принципу «Кто раньше встал — того и тапки».
Что в общем случае подразумевается под клиент серверным взаимодействием в игре?
1. Логин пользователя
2. Обмен с сервером (Отправка команды от клиента к серверу, Получение ответа на команду от сервера, Получение команды от сервера)
3. Адиминская часть управления сервером
Логин пользователя — это первичная аутентификация в игре. Мы делаем все по простому и проверка будет идти только на этапе логина. А вообще авторизацию надо периодически проверять в игре на всякий пожарный от читеров. При первом входе с новым логином игра заносит его в базу вот и вся регистрация.
Админская часть — тоже примитивная. Будем отображать список пользователей онлайн. Пригодится для нагрузочного тестирования.
Обмен с сервером — это и есть протокол игры. Его распишем когда будем делать реализацию сервера.
В реализации сервера не будем заморачиваться ни с какими синхронными вариантами синхронизации клиентов ( воспроизведение истории, предсказание и т. д. и т. п. ). (Вот это я загнул — синхронная синхронизация… — прим. автора)
Т.е. Принцип очень простой. Меряем пинг до клиента и таким образом знаем задержку каждого клиента. Это позволяет нам убрать влияние клиентов с плохим соединеним на всю игру да и реализация проще чем у синхронных вариантов. Да, при таком варианте возможен вариант, когда пинг очень долгий, получить слайд шоу. Но при этом все корректно будет обрабатываться. И слайд шоу будет только у клиентов с большим пингом. В нагрузку получим необходимость хранить актуальное значение пинга для каждого клиента.
С общим описанием проекта все вроде ясно.
Часть первая. Действие второе: Архитетура сервера.
Сразу хочу предупредить, что статья в какой-то мере исследовательская. Т.е. в процессе реализации и нагрузочного тестирования сервера, его архитектура может (и будет) меняться под воздействием различных обстоятельств…
Общая схема получается такая:
В общем делаем по классической схеме с пулом тредов. Игра у нас простая, физики в ней нет никакой, да и вообще расчетов на сервере мало.
Общая логика работы сервера получается такая.
1. Получаем от клиента запрос авторизации.
2. Рандомно выдаем координаты появления танка на карте.
3. Клиент шлет свои координаты на сервер с какой-то периодичностью (Во время движения чаще).
4. При выстреле на сервер отсылаются координаты точки откуда он произошел.
5. Сервер вычисляет полет снаряда и решает есть попадание или нет.
6. Так же во время движения сервер определяет препятствия. И не дает двигаться танку в этом направлении.
7. По запросу клиента сервер передает количество клиентов онлайн.
Вот в общем-то и все что задумано.
В дальнейшем, если хабровчанам будет интересно эту схему можно расширить до нормальной архитектуры с разнесением частей сервера по разным машинам.
Через пару дней будет следующая часть, в которой мы разработает протокол обмена, реализуем TCP сервер и произведем первый коннект сервера и клиента.
P.S. На самом деле тема очень обширная. Поэтому если есть какие-то пожелания на чем заострить внимание или есть что-то, что я упустил из виду при описании — велкам в каменты.
Ваши пожелания и критика позволят в следующую часть повествования сделать более полезной и конструктивной.
upd. По просьбам хабравчан открыл Github для проекта
Основы многопользовательской игры на Unity3D
Я, как и многие из вас, большой поклонник многопользовательских игр. В них меня прельщает в основном дух соревнования и возможность приобретать улучшения, накапливая достижения. Да и сама идея выхода в свет все большего количества игр данного типа побуждает к действию.
С недавнего времени я и сам взялся за разработку собственного проекта. И поскольку на Хабрахабре статей на эту тематику не нашел – решил поделиться своим опытом написания многопользовательской игры на движке Unity3D. Также хочу рассказать о компонентах Network и NetworkView, атрибуте RPC и встроенных методах-ивентах. В конце статьи подан пример игры и, разумеется, сам проект для Unity. Итак…
Класс Network
Данный класс нужен для организации соединения «клиент-сервер». Основные функции: создание сервера, подключение к серверу, создание сетевого экземпляра префаба.
Основные методы:
Network.Connect (string host, int remotePort, string password = «») – выполняет подключение к серверу host с портом remotePort и паролем password. Метод возвращает перечисление NetworkConnectionError.
Network.InitializeSecurity() – вызывается перед Network.InitializeServer() для защиты от читерства. Подробности в официальной документации. Не вызывать на клиенте!
Network.Instantiate(Object prefab, Vector3 position, Quaternion rotation, int group) – создает экземпляр префаба prefab в сети в позиции position с поворотом rotation и группой group. Возвращает весь созданный объект, с которым после создания можно выполнить дополнительные действия. Подробности – далее в статье.
Основные свойства:
bool Network.isClient и bool Network.isServer – определяют, является ваша игра сервером либо клиентом. Оба свойства являются false, если не был создан сервер или не было подключения к серверу.
string Network.incomingPassword – свойство задает пароль для входящих подключений.
NetworkPlayer Network.player – возвращает экземпляр локального игрока NetworkPlayer.
NetworkPeerType Network.peerType – возвращает текущее состояние подключения: Disconnected (отключен), Server (запущен как сервер), Client (подключен к серверу), Connecting (попытка, в процессе подключения).
NetworkPlayer[] Network.connections – возвращает всех подключенных игроков. На клиенте возвращает только игрока сервера.
Основные ивенты (для унаследованного от MonoBehaviour):
OnConnectedToServer() – вызывается на клиенте при успешном подключении к серверу.
OnDisconnectedFromServer(NetworkDisconnection info) – вызывается на клиенте при отключении от сервера и на сервере при завершении подключений Network.Disconnect(). В info содержится причина отключения: LostConnection (потеря связи) и Disconnected (при успешном отключении).
OnFailedToConnect(NetworkConnectionError error) — вызывается на клиенте при ошибке подключения. error содержит ошибку типа NetworkConnectionError.
OnNetworkInstantiate(NetworkMessageInfo info) — вызывается на клиенте и сервере, если был создан новый экземпляр методом Network.Instantiate(). Содержит info типа NetworkMessageInfo.
OnPlayerConnected(NetworkPlayer player) — вызывается на сервере при успешном подключении клиента и содержит player типа NetworkPlayer.
OnPlayerDisconnected(NetworkPlayer player) — вызывается на сервере при отключении клиента и содержит player типа NetworkPlayer.
OnServerInitialized() — вызывается на сервере, после того как сервер был успешно создан.
OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info) — важный ивент для синхронизации компонента с сетью. Подробности – далее в статье.
Класс NetwokView
Основные методы:
networkView.RPC(string name, RPCMode mode, params object[] args) — вызывает удаленную процедуру name, mode определяет получателей, args – аргументы для передачи процедуре.
networkView.RPC(string name, NetworkPlayer target, params object[] args) – то же, что и предыдущий метод, однако выполняет отправку конкретному игроку NetworkPlayer.
Основные свойства:
bool networkView.isMine – свойство, определяющее, является ли объект локальным. Весьма часто используется для проверки владельца объекта.
Component networkView.observed – компонент, который будет синхронизироваться. Если это скрипт, то он должен содержать метод OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info), упомянутый выше.
NetworkPlayer networkView.owner – свойство, возвращающее владельца объекта.
NetworkStateSynchronization networkView.stateSynchronization — тип синхронизации: Off, ReliableDeltaCompressed, Unreliable.
NetworkViewID networkView.viewID — уникальный идентификатор в сети для NetworkView.
Атрибут RPC
Метод OnSerializeNetworkView(BitStream stream, NetworkMessageInfo info)
Данный метод используется для синхронизации компонента в сети. Он вызывается всякий раз при получении либо отправке данных по сети.
Вот типы данных, которые могут быть получены/отправлены методом Serialize: bool, char, short, int, float, Quaternion, Vector3, NetworkPlayer, NetworkViewID.
Для проверки, идет ли прием либо передача, используются свойства isReading или isWriting.
Привожу пример использования:
Данный пример не идеален, поскольку при его работе наши объекты будут «дергаться». Чтобы избежать этого, нужно воспользоваться интерполяцией. Подробнее – далее в статье.
Интерполяция
Подробнее о методах оптимизации синхронизации по сети смотрите на сайте разработчиков: Valve Developer Community — Source Multiplayer Networking
Пример многопользовательской игры
Итак, имея представления об основах, можно приниматься за написание небольшой многопользовательской игры. В качестве примеров я использую разные способы применения NetworkView. Вам остается лишь выбрать для себя наиболее удобный способ.
Создаем скрипт ServerSide.cs и пишем туда следующее:
Теперь создаем скрипт клиента ClientSide.cs:
Таким образом, клиентская и серверная логика есть, теперь для нее нужно сделать управление MainMenu.cs:
Управление сетью создано. Далее пишем управление игроком PlayerControls.cs. В данном примере я использую другой способ применения компонента NetworkView:
Знаю, что синхронизация и управление должны находиться раздельно, но для примера я решил объединить их. Как вы заметили, здесь NetworkView создается во время инициализации скрипта. На мой взгляд, это более удобный способ для защиты от возможного «забыл добавить» (разумеется, если не написано RequireComponent( typeof( Rigidbody ))), а также уменьшает в инспекторе количество компонентов на объекте.
К примеру, у меня был случай: когда, на первый взгляд, все было сделано правильно, однако мой скрипт не делал интерполяцию, и все мои действия в синхронизации игнорировал. Так вот ошибкой оказалось то, что Observed был не моим скриптом, а трансформ объекта.
Итак, теперь у нас есть все необходимые скрипты для написания мини-игры.
Создаем пустой объект и назначаем ему скрипты MultiplayerMenu, ServerSide, ClientSide.
Создаем плоскость и немного опускаем.
Создаем префаб игрока (в моем примере это будут шары). Создаем объект «сфера», назначаем ему скрипт PlayerControls и добавляем в префаб. Префаб перетягиваем на ClientSide в поле Player Prefab.
На этом все, компилируем проект (не забывая в настройках игрока включить Run in background) и запускаем несколько раз. В одном из окон жмем сервер, на остальных – клиент, и смотрим на результат.
Ссылка на проект.
*В проекте могут быть логические ошибки, но на суть данной статьи они не влияют.
Всех благодарю за внимание!
Желаю успехов в создании многопользовательских игр!
Многопользовательский геймплей: Три подхода к организации соревнований между игроками
Советы гемдизайнера Wooga
Геймдизайнер Адам Телфер опубликовал в блоге Mobile Free to Play заметку о том, как разработчики мобильных игр организовывают многопользовательский геймплей в своих проектах — с помощью ботов, асинхронных боёв или одновременного мультиплееера.
В рубрике «Рынок игр» — перевод заметки.
Сильный многопользовательский геймплей является определяющим фактором успеха любой free-to-play-игры. Этот успех обусловлен двумя очевидными причинами:
Однако создать по-настоящему социальный многопользовательский геймплей на мобильном устройстве отнюдь не просто. Эту и без того трудную дизайнерскую задачу ещё более усложняют известные ограничения мобильных платформ: маленький экран, короткие сессии и нестабильное соединение.
Из-за этих проблем до сих пор не появлялось по-настоящему успешных и полностью синхронизированных многопользовательских игр на мобильных платформах, за исключением нескольких ключевых примеров: Hearthstone, 8-ball Pool и World of Tanks. Даже играя в Hearthstone — игру, наиболее удачно приспособившуюся к мобильным устройствам, — я должен быть предельно сосредоточен, а кроме того, я не могу выйти из игры в любой момент. Несколько раз я нарывался на неприятности, застревая в Hearthstone как раз тогда, когда моего внимания требовала реальная жизнь.
Я убеждён, что развитие видеоигр будет идти (пусть и не слишком быстро) к полностью синхронному мультиплееру, и талантливые разработчики научатся создавать его преимущества (такие как социальный геймплей и повторяемый контент) для систем, более адаптированных к мобильным ограничениям.
В этой статье рассмотрены три ключевых метода, которые используют сегодняшние разработчики при создании многопользовательских игр для мобильных устройств.
Фальшивая синхронность
Многие free-to-play-игры создают ощущение синхронной игры, фактически её не предоставляя. Вместо реального соперника игрок взаимодействует с ботом. Разработчики таких игр считают, что игроки не способны почувствовать разницу между живым игроком и ботом. Но это можно сделать, хотя и с трудом.
В качестве примера приведу игру Contest of Champions от Kabam. Хотя игра создаёт ощущение многопользовательской, на самом деле каждый игрок сражается с ботом. Таким образом разработчики избавились от проблем, связанных с синхронизацией файтинга при прерывистом интернет-соединении.
В игре CSR Racing происходит нечто подобное: вы соревнуетесь на трассе либо с фантомом реального игрока, либо с ботом. Геймплей устроен так, что игрок никак не может влиять на скорость своего партнёра. Поэтому разница между живым человеком и ботом не слишком заметна.
Конечно, это не идёт ни в какое сравнение с возможностями синхронного мультиплеера. Разработчики Real Racing 3 попытались это исправить: они используют сохранённую запись гонки другого игрока, и вы фактически соревнуетесь с его фантомом. Во время гонки вы можете врезаться в машину соперника или вытолкнуть его с трассы, однако он чудесным образом снова возвращается в то место, где в это время гонки находился его прототип. И всё-таки вы уже подошли немного ближе к синхронной игре.
В целом такой подход позволяет вам имитировать простейшее взаимодействие между игроками, решает проблему нестабильного соединения и даже вовлекает игрока (если сессия длится недолго). Однако в этой игре нет ощущения, что ты играешь с живым игроком.
Одновременный мультиплеер
Второй подход используют лишь некоторые из мобильных игр. Впервые его применили в браузерных играх и в спортивных фэнтези-играх в конце 90-х и в начале 2000-х годов (примерами таких игр могут служить Travian и Hat-Trick).
В одновременном мультиплеере игроков просят заранее подготовить свои стратегии, а затем пара таких стратегий проигрывается одновременно. Таким образом, игроки делают свой выбор до того, как наступит их очередь играть, и чтобы взаимодействовать и соревноваться друг с другом, им не нужно быть в сети в одно и то же время.
Игра Top Eleven от Nordeus — одна из немногих, использующих эту систему. Её матчи проходят в строго установленное время. Каждый игрок заранее определяет тактику, стратегию и расстановку своей команды, а затем дожидается начала своего матча. Игра запускается независимо от того, активны игроки или нет.
Этот подход хорош тем, что он обеспечивает естественное вовлечение в геймплей:
Однако применение этого метода может привести к множеству проблем: в такой системе очень трудно обеспечить монетизацию и не превратить free-to-play-игру в pay-to-win. Каждый день все игроки производят одинаковое количество действий. Если те, кто платят, будут совершать больше действий или получат какое-то другое преимущество, это сразу почувствуют их соперники. При этом очень важным становится правильный подбор пар игроков.
Дизайн сессий в игре довольно ограничен. После того, как вы определились со стратегией, вам фактически нечего делать до следующего матча. Дизайнерам нелегко определиться с расписанием матчей: если игры будут слишком частыми, игроки начнут нервничать, а если слишком редкими — заскучают. Проблема этой системы состоит в её излишней регламентированности и недостаточной гибкости (особенно для тех игроков, которые пропустили свою очередь или хотят играть чаще).
Асимметричная и асинхронная многопользовательская игра
Самый популярный способ создать многопользовательскую игру на мобильном устройстве основан на асинхронном мультиплеере.
Такие игры как Words with Friends или Draw Something имеют полностью асинхронный мультиплеер. Все игроки играют по очереди, а затем ждут, пока игру закончат их друзья. Хотя это по самой своей сути вовлекающий процесс, большинство игр, которые придерживаются такого асинхронного стиля, потерпели фиаско при выходе на мобильный рынок. Кроме того, дизайн сессии ограничивает монетизацию в основном из-за трудностей в составлении пар (читайте наши статьи об уничтожении энергии и построении социальной механики).
Впрочем, асинхронность не обязательно должна строиться именно таким образом. Подобные игры могут быть асимметричными и асинхронными.
Асимметричные асинхронные игры делят внимание игрока между геймплеями двух типов —активным и пассивным, причём каждый из них должен быть как можно более вовлекающим. Активные игроки вовлекаются в один вид геймплея (атакующий геймплей), а неактивные игроки — в другую разновидность геймплея (защитный геймплей). Именно такая механика реализована в Clash of Clans, Rage of Bahamut и в играх Theives.
Ключом к успеху подобных игр являются хорошо сбалансированные вовлекающие геймплеи обоих типов. Ведь игрокам действительно трудно разорваться: с одной стороны, они вынуждены вести защитную игру, чтобы обезопасить свои ресурсы от посягательств соперников, с другой — должны участвовать в атакующей игре, чтобы успеть накопить эти ресурсы и обеспечить свой прогресс.
До последнего времени асимметричные игры для мобильных устройств поддерживали стиль Clash of Clans (атака против защиты). Иногда предпринимались попытки уйти от этой формулы, например, в Zombination, где игрок может выбрать, на чём ему сосредоточиться: на атаке (зомби) или на защите (люди). Но в системе есть огромный потенциал для появления других игр, например, применение формулы «пассивный против активного» к новым жанрам и новому геймплею. Пока лучшим примером из всего, что я видел, является King of Thieves.
Заключение
Существует несколько типов многопользовательских игр для мобильных устройств.
Вся трудность состоит в том, чтобы увязать преимущества синхронного мультиплеера с ограничениями мобильных устройств.
Существует три способа построить мультиплеер для мобильного устройства:
Каждый из этих способов позволяет создать сильный дизайн сессий, работать с ограничениями мобильных устройств и дать игроку ощущение присутствия в живом сообществе. У каждого способа есть свои недостатки и свои уникальные возможности.
Мультиплеер в быстрых играх (части I, II)
Разработка игры — само по себе непростое занятие. Но мультиплеерные игры создают совершенно новые проблемы, требующие разрешения. Забавно, что у наших проблем всего две причины: человеческая натура и законы физики. Законы физики привнесут проблемы из области теории относительности, а человеческая натура не даст нам доверять сообщениям с клиента.
Если вы уже знакомы с базовыми идеями, стоящими за многопользовательскими играми — смело пропускайте первую часть и переходите ко второй.
Часть I
Проблема читерства
Вся наша головная боль начинается с читерства.
Если вы разрабатываете одиночную игру, вам наплевать если пользователь решит смухлевать — его действия влияют только на него. Да, возможно игрок не получит тот опыт, который вы хотели ему дать, но в конце концов он купил игру и может пользоваться ей как захочет.
А вот в мультиплеерных играх все совсем иначе. В любой соревновательной игре читер не просто упрощает себе игру, но и ухудшает чужой игровой опыт. Вам, как разработчику, стоит препятствовать этому, так как читеры отгоняют игроков от вашей игры.
Есть много вещей которые можно сделать чтобы предотвратить читерство. Но самый главный принцип(и наверное самый глубокий) очень прост: не доверяй игроку. Всегда ожидайте худшего — что игрок будет пытаться вас обмануть.
Авторитарный сервер и наивный клиент
Этот принцип ведет нас к простому, на первый взгляд, решению — вся игровая логика крутится на главном сервере, под вашим контролем, а клиент лишь демонстрирует текущее состояние сервера и отправляет ему команды (нажатия клавиш и т.д.). Обычно это называют авторитарным сервером, потому что он единственный, кто умеет моделировать мир.
Конечно, сервер может быть взломан, но эта тема выходит за рамки данной серии статей. Тем не менее, использование авторитарного сервера предотвращает широкий спектр читов. Например, вы не можете доверять клиенту уровень жизней игрока. Взломанный клиент может изменить локальную информацию и сообщить что у игрока 100000% жизней, но сервер знает что жизней всего 10% и если игрока атакуют, он умрет вне зависимости от того, что об этом думает клиент.
Так же нельзя верить игроку, когда он сообщает о его позиции в мире. Если вы доверитесь, взломанный клиент может сообщить серверу:
— Я на (10, 10)
А секундой позже:
При этом возможно он «прошел» через стену или двигается быстрее чем ему положено.
А вот правильная парадигма. Сервер знает что игрок находится в позиции (10, 10); клиент говорит: «Я хочу подвинуться на единицу вправо». Сервер обновляет позицию игрока на (11, 10), производя все необходимые проверки, а затем отвечает игроку: «Вы на (11, 10)»:
Подытожим: игровое состояние управляется только сервером. Клиенты отправляют свои действия на сервер, а сервер периодически обновляет свое состояние и отправляет его на клиенты, которые, в свою очередь отображают его пользователям.
Разбираемся с сетями
Наивный клиент отлично подходит для медленных пошаговых игр — стратегий или покера. Так же он неплохо сработает при локальном подключении, где информация передается практически мгновенно. Но он абсолютно непригоден для быстрых игр по интернету.
Давайте поговорим о физике. Предположим что вы находитесь в Сан-Франциско и подключаетесь к серверу в Нью-Йорке. Это примерно 4000 километров. Так как ничто не может передвигаться быстрее скорости света, в лучшем случае сигнал дойдет за 13 миллисекунд. Но весьма маловероятно, что у вас будет такая хорошая связь. В реальном мире информация не идет прямым путем, причем не со скоростью света.
Так что давайте предположим, что это занимает 50 мс. И это практически лучший сценарий. А что если вы подключаетесь к серверу в Токио? А что если линия связи перегружена? В таких случаях задержки доходят до половины секунды.
Вернемся к нашему примеру. Пусть клиент отправляет сообщение:
— Я нажал на стрелку вправо.
Сервер получает запрос через 50 мс и сразу отправляет обратно обновленное состояние.
Это сообщение дойдет до пользователя еще через 50 мс.
С точки зрения игрока, он нажал на стрелку, потом 0.1 секунды ничего не происходило, а затем персонаж наконец подвинулся на единицу вправо. Этот лаг между командой и её результатом может показаться незначительным, но он заметен. И уж конечно лаг в полсекунды был бы не просто заметным, а сделал бы игру абсолютно неиграбельной.
Резюмируя
Игры по сети невероятно веселы, но привносят совершенно новый класс проблем и препятствий. Авторитарная архитектура хороша против читеров, но наивная реализация сделает игру неотзывчивой для пользователей.
В дальнейшем мы исследуем возможность создания системы, базирующейся на авторитарном сервере, но с минимальными задержками для игроков, делая их неотличимыми от одиночных игр.
Часть II
Введение
В первой части мы рассмотрели клиент-серверную модель с авторитарным сервером и наивным клиентом, который отправляет команды на сервер и отображает обновленное состояние пришедшее в ответе.
Наивная реализация этой концепции приводит к ощутимой задержке между командой и реакцией. Например, если игрок нажимает стрелку влево, персонаж начнет двигаться через полсекунды. Это происходит потому что команда должна дойти до сервера, а результат команды после этого должен дойти до клиента.
В интернете, где задержки могут составлять десятые доли секунды, геймплей в лучшем случае будет неотзывчивым, а в худшем — неиграбельным. В этой части мы найдем способы уменьшить эту проблему или избавиться от неё вовсе.
Предсказание на стороне клиента
Несмотря на то что некоторые игроки пытаются читерить, большую часть времени сервер получает корректные запросы. Это означает, что полученный ввод будет корректным и игра обновится так, как ожидается. То есть если персонаж находится на (10, 10) и отправляет команду на движение вправо, он окажется на (11, 10).
Мы можем использовать это если игра достаточно детермениртована (то есть результат определен командами и предыдущим состоянием).
Предположим что у нас лаг 100 мс и время перемещения персонажа составляет 100 мс. При использовании наивной реализации, время действия составит 200 мс.
Предполагая что команды будут исполнены, клиент может предсказывать состояние игрового мира и часто предсказание будет правильным, так как игровой мир детерминированный.
Так что вместо того чтобы отправлять команду и ждать пока придет новое игровое состояние чтобы отрендерить его, мы можем отправить команду и начать рендерить результат как если бы команда уже была выполнена. И, разумеется, надо ждать от сервера результата — «настоящего» состояния игры, которое по большей части будет совпадать с локальным состоянием.
Теперь у нас нет абсолютно никакой задержки между действием игрока и результатом на экране, а сервер все еще авторитарный (если взломаный клиент начнет отправлять некорректные команды, он может рендерить что угодно на экране, но это никак не повлияет на состояние игры на сервере, которое видят другие игроки).
Проблемы синхронизации
В предыдущем примере я аккуратно подобрал числа чтобы все отлично работало. Давайте немного изменим сценарий. Лаг будет составлять 250 мс, а анимация передвижения на одну единицу будет длиться 100 мс. А еще давайте игрок дважды быстро нажмет на стрелку вправо.
При использовании текущего подхода вот что произойдет:
Мы столкнулись с интересной проблемой на t = 250 мс, когда нам пришло новое состояние. Клиент предсказал x = 12, но сервер говорит что x = 11. Так как сервер авторитарный, клиент должен передвинуть персонажа обратно на x = 11. Но позже, на t = 350, сервер говорит что x = 12, так что персонаж опять прыгает, но на этот раз вперед.
С точки зрения игрока, он нажал на стрелку вправо дважды, так что персонаж переместился на две единицы вправо, постоял там 50 мс, прыгнул на единицу влево, постоял там 100 мс и прыгнул на единицу вправо. Конечно, это совершенно неприемлимо.
Согласование с сервером
Ключ к решению этой проблемы лежит в понимании того, что клиент видит игровой мир в настоящем времени, но из-за лага обновления с сервера приходят о состоянии мира в прошлом. К тому моменту, когда сервер отправил нам обновления, он еще не получил некоторые наши команды.
Но обойти эту проблему не так уж сложно. Давайте будем добавлять к каждому запросу от клиента его номер. А сервер при ответе будет добавлять номер последнего обработанного запроса. И давайте хранить на клиенте копию всех команд, отправляемых на сервер.
Итак, на t = 250 клиенту приходит «x = 11, последняя команда #1». Клиент удаляет все команды до #1 включительно, но оставляет копию #2, о которой еще не знает сервер. Он применяет полученное от сервера состояние (x = 11), а затем применяет ввод, который еще не виден серверу. В данном случае #2 «вправо на 1 единицу». Конечный результат x = 12, что соответствует истине.
Далее, на t=350, от сервера приходит новое состояние: «x = 12, последняя команда #2». Клиент удаляет все копии команд до #2 включительно, а затем применяет состояние x=12(ничего не изменилось). Так как более нет необработанных команд, на этом все заканчивается, с корректным результатом.
Итоги
Мы разобрали пример с движением, но этот же принцип применим почти ко всему. Например, при атаке вражеского персонажа, вы можете сразу показать кровь и число, отражающее нанесенный урон.
Но не стоит на самом деле обновлять жизни персонажа, пока сервер не пришлет обновленное состояние.
Из-за сложностей игрового состояния, которое не всегда легко откатить, возможно не стоит убивать персонажа даже если уровень его жизни упал ниже нуля. Что если что другой игрок вылечился прямо перед вашей атакой, но сервер еще об этом не сообщил?
Прим. перев. Я бы убивал персонажа сразу, но обеспечил персистентность(возможность откатывания состояний). Так будет проще писать переносимый код, выполняющийся и на сервере и на клиенте. В любом случае, как вы убедитесь в этом позже, это придется делать.
Это все нас приводит к интересному заключению — даже если мир абсолютно детерминированный и нет читерящих игроков, все равно есть вероятность того, что состояние предсказанное клиентом и состояние отправленное с сервера не совпадают. Хоть для одного игрока это невозможно, очень легко воспроизвести такую проблему при нескольких одновременно играющих игроках. Это будет темой следующей статьи.