В этой серии статей речь пойдет о том что такое PING и какими приемами можно сгладить задержку пересылки пакетов при его высоком значении в realtime онлайн играх с примерами кода на C# в игровом движке Unity для игр на ПК, мобильных устройствах и браузерных игр WebGL.

Статья включает в себя видео примеры, демонстрации кода и алгоритм подсчета PING без использований командной строки устройств

Что такое PING

Начнем с теории: отправляя команду в ММО игре, на сервер она доходит не моментально - ей требуется время дойти до физического сервера ("железа") по кабелю, который проложен, в том числе по дну океана и часто этот сервер находится очень далеко (поэтому часто делают несколько серверов в странах, близких к игрокам).

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

я изменил картинку из предыдущей статьеб чтобы показать что websocket это не классическая модуль запрос - ответ
я изменил картинку из предыдущей статье, чтобы показать что websocket это не классическая модуль запрос - ответ

Время, которое затрачено на отправку пакета и его возврат обратно - называется Ping, выражается (обычно) в миллисекундах (в одной секунде 1000 мс.) и чем он ниже - тем лучше. Важно отметить, что в него не включено время программных вычислений для обработки запроса, как например в расчете RPS сайтов и web приложений

Как высчитать PING

В Unity для версий игр ПК и мобильных устройств есть готовые инструменты анализа PING на устройстве, но в браузерных версиях игр (WebGL) они не работают.

инструменты расчета ping через код в Unity
инструменты расчета ping через код в Unity

Я расскажу о своем приеме расчета PING, который сможет заменить встроенный инструмент в Unity и использоваться в браузерных версиях игр:

  1. Отправляем с пакетом временную метку Unix Timestamp с точностью до миллисекунды по времени клиента. На скриншоте ниже пример демонстрационной игры с моего сайта (код ее открыт и доступен в GIT) я добавляю метку к каждому запросу (по времени на устройстве пользователя)

  1. На сервере, когда нужно отправить пакет по данной команде обратно, я отнимаю от этой метки-времени время, которое прошло с момента как сервер получил данную команду (разница в миллисекундах) - строки $event['time'] - $microtime .

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

код написан на PHP, но это не важно - идею можно использовать на любом языке
код написан на PHP, но это не важно - идею можно использовать на любом языке
  1. При получении команд в клиентской части я снова по времени клиента отнимаю полученные значения и это и есть чистый сетевой Ping. Для статистики я сохраняю результаты в массив (список), чтобы выводить среднее значение (сумма всех значений, делённое на количество этих значений в массиве).

  1. Периодически шлю попутно с пакетами из клиента на сервер дополнительный параметр ping, тем самым сервер знает средний пинг каждого игрока (этот параметр не влияет ни на какие вычисления на сервере и служит как информативный в связи с тем что его легко подменить в клиентской части через код)

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

Однако можно заметить, что PING в нашем случае зависит от размера пакета (вдобавок Websocket протокол на базе TCP в случае потери пакетов начнут их повторно отправлять, будет ждать подтверждение и т.п.), но как по мне это значение более удобно (мне важнее сетевые задержки на реальных игровых пакетах, чем анализ скорости отправки пары байт).

На вопрос каким является хороший показатель Ping в играх Google ответил цифру 50 мс.

Эта цифра означает, что если наш пакет из клиента (игры) будет идти 25 мс. в одну сторону и 25мс. с сервера в клиент (игру), т.е. в другую.

Я подготовил видео, где поясняю и показываю то, о чем написал в данном разделе (функционал уже немного изменился, но концепт остался тот же):

Что такое FPS

Между отправкой любой команды вам нужно время на повторное нажатие клавиши (сенсора), но есть команды, когда можно держать клавишу нажатой (например движение) и можно предположить, что команды шлются на сервер в онлайн играх непрерывно, но это не так - игра поделена на кадры (условно циклы while) в секунду (FPS) и каждый такой кадр (можно реже, но не чаще обычно) она может считывать нажатие клавиши и меняет картинку. FPS может как взлетать до 300, так и опускаться до 30 (ниже тоже может, но уже видны торможения игры) и даже это слишком быстро, чтобы слать с такой частотой команды (т.к. иначе вы либо будете двигаться при одном нажатии клавиши на доли миллиметра или движение будет настолько быстрым, что буде�� походить на телепортации)

Возможно Вы знаете из Wikipedia, что глаз человека воспринимает смену изображений как плавную при частоте в диапазоне 48 (фильм "Хоббит" был первым снят с этой частой) - 60 FPS (кадров в секунду), поэтому у каждой команды есть своя пауза и она составляет обычно столько, сколько человеческий глаз может уловить и больше, а значит минимальную комфортную паузу между кадрами можно высчитать по формуле: 1 секунда / 60 FPS = 0,017 (округленно) секунды = 17 мс.

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

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

Отсюда и считается, что хорошим показателем FPS в играх является 60 FPS

FixedUpdate Unity

В Unity есть циклы, выполняющийся раз в фиксированное времяFixedUpdateчастота которого и выражается в виде миллисекунд. В текущем примере стоят частота 50 FPS

Интерполяция

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

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

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

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

  2. мы знаем среднее значение него PING на текущий момент, а значит мы знаем когда примерно когда пакет достигнет сервера от момента отправки - 1/2 от скорости пинг

  3. при получении ответа от сервера мы знаем что она выполнилась 1/2 пинга назад (т.к. этот пакет шел до нас это время).

Оперируя этими данными мы знаем примерное (т.к. PING величина не постоянная) время когда сервер будет готов принять новую команду и позволить ее заранее отправить с клиента (не дожидаясь предыдущего ответа от сервера).

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

Видео ниже демонстрирует как работает интерполяция:

Экстраполяция

Выше я уже сказал что PING величина не постоянная и может резко меняться (особенно при работе с беспроводным интернетом) и в моменте он может резко увеличится (ухудшиться) и вернутся на нормальное значение. За это время весь игровой мир может замедлиться - такое явление называют "фризы" (от англ. freez - замерзнуть). Для подобных ситуаций используют подход, который продолжает поведение объекта как если бы он продолжил бы делать то же самое, но в нашем случае (в игре) - с меньшей скоростью, называется экстраполяция

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

А так же подготовил видео с демонстрацией как это работает в самой игре (это улучшило переход в бесшовном мире)

В настоящий момент проект на моем сайте http://mmogick.ru является не коммерческим (есть бесплатный демо доступ в админ панель) и я веду его в частном порядке, но верю что продолжая и делясь архитектурными идеями в будущем это станет рабочим продуктом сделанным в России, а статьи помогут в написании игрового сервера для MMO игр.

Буду благодарен за лайк статье.

Если вам интересно чем закончится данный проект - подписывайтесь на мой профиль (я публикую только статьи данного проекта) и следите за новыми видео на Youtube (они выходят чаще чем статьи).

История:

  1. Введение

  2. Масштабируемость и асинхронность

  3. WebSocket

  4. Redis

  5. LUA и JavaScript

  6. Выбор технологий, протокола и архитектурный шаблон Entity Component System

  7. Игровые локации (тайловые карты)

  8. Клиентская часть на Unity

  9. Игровые серверные механики

  10. Открытый бесшовный мир в 2D игре

  11. FPS, Ping, паузы между командами, интерполяция и экстраполяция

  12. Очереди и параллельное программирование на CPU

  13. Event-driven паттерн, JSON-RPC и почему не сервисная (SOA) архитектура

  14. Сетевая карта и задержка кадра (Latency frame) по RFC 2544 (1242)

  15. Создание сервера для онлайн ММО игр на PHP

  16. Готовое MVP сервиса 2D MMO RPG игр (realtime)