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

Что такое пинг

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

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

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

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

Как его посчитать

В 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

И то же самое на видео:

Что было дальше

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

Как это выглядит в игре, показываю на канале проекта: youtube.com/@mmogick_ru.

Если вы боролись с рывками в своей игре — расскажите, что дало больший эффект: сглаживание на клиенте или изменения в протоколе.

История:

  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)

  17. Внедряю ИИ: механики из одного описания

  18. Конвейер контента для MMO: импорт анимации, экипировка и обновление игры без патчей

  19. Каталог ассетов и git: как командой вести игру в облаке

  20. Тесты для кода, который пишет ИИ: контракты вместо ассертов