Разработка игр
Netcode: prediction и reconciliation
2020 год. Riot Games запустила Valorant с обещанием 'читеров не существует'. Часть этого обещания - архитектура netcode: client-side prediction с reconciliation на 128-тиковых серверах. Игрок в Стокгольме с ping 15 мс и игрок в Дели с ping 90 мс должны иметь честный поединок. Инженеры Riot потратили два года на netcode - и это видно в рейтинге: Valorant имеет меньше жалоб на 'неправильные хиты', чем CS:GO при схожей архитектуре.
- **Valorant** использует client-side prediction с 128-тиковым сервером и lag compensation до 140 мс. Tick rate в два раза выше CS:GO (64 тика) - это уменьшает позиционную ошибку в два раза.
- **Guilty Gear Strive** (Arc System Works, 2021) первый крупный файтинг с GGPO rollback netcode. Сообщество файтингов отмечает: онлайн-матчи с ping 100 мс стали неотличимы от локального кооператива.
- **Apex Legends** (Respawn) использует hybrid interpolation: 60 Hz сервер, snapshot interpolation для противников, prediction для своего персонажа. Dead Reckoning (экстраполяция) при потере пакетов - плавность сохраняется даже при 10% packet loss.
Client-Side Prediction: отзывчивость без ожидания сервера
При ping 100 мс без prediction каждое нажатие клавиши вызывает 100 мс задержку перед движением - управление ощущается как управление персонажем через интернет на луне. Client-side prediction: клиент применяет ввод немедленно, не ожидая ответа сервера. Персонаж движется мгновенно на клиенте, а сервер параллельно обрабатывает тот же ввод. Когда приходит серверный ответ, клиент сверяет своё предсказанное состояние с авторитарным серверным. Если совпало - ничего не происходит. Если расхождение - необходима reconciliation. Valve ввела этот паттерн в Half-Life (1998), и сейчас он стандарт в Unreal Engine (Character Movement Component), Unity Netcode и Mirror Networking.
Input buffer: клиент хранит историю всех неподтверждённых вводов с timestamp. При получении сервером подтверждённого состояния на тик N, клиент выбрасывает все inputs до N включительно из буфера.
Зачем клиент хранит историю неподтверждённых вводов в буфере?
Server Authority и Reconciliation
Prediction делает управление отзывчивым, но создаёт проблему: локальное состояние клиента может разойтись с авторитарным серверным. Сервер применяет те же вводы, но мог видеть столкновения, которых не увидел клиент (другой игрок заблокировал путь). Reconciliation - это процесс исправления предсказания. Схема Valve/Source Engine: 1) получить серверное состояние на тик N, 2) откатить локальное состояние к серверному, 3) replay все буферизованные вводы с тика N+1 до текущего тика. Визуально это выглядит как резкий телепорт - 'rubber-banding'. Чтобы избежать этого, применяют сглаживание: плавно двигать визуальный объект к предсказанной позиции, пока физическая позиция исправляется.
Error threshold: не каждое расхождение требует reconciliation. Ошибка менее 5-10 пикселей - результат float rounding, не реального расхождения. Только существенные расхождения требуют полного replay истории.
Rubber-banding (телепорт персонажа) - симптом чего?
Rollback Netcode: перемотка симуляции
Файтинги требуют реакции на 1/60 секунды - одного кадра. При ping 100 мс классический delay-based netcode добавляет 3-4 кадра задержки к каждому действию - это катастрофа для жанра. Rollback netcode (GGPO, 2008): клиент предсказывает ввод противника (обычно 'повторить предыдущий'), симулирует игру с предсказанным вводом и рендерит кадр немедленно. Когда приходит реальный ввод противника, если предсказание неверно - откатить симуляцию назад (rollback) до момента расхождения и проиграть с правильным вводом. Если предсказание верно - ничего не происходит. Guilty Gear Strive, Street Fighter 6, Mortal Kombat 11 - все используют GGPO или его вариации.
Rollback глубиной N кадров требует N сохранённых состояний симуляции. Для 8 кадров при ECS с 1000 сущностями - это 8 снапшотов состояния. Сжатие состояния критично: только изменённые компоненты. GGPO ограничивает rollback 8 кадрами (133 мс) - дальше игра добавляет искусственный input delay.
Rollback netcode предсказывает ввод противника как 'повторить предыдущий'. Почему это работает достаточно хорошо?
Snapshot Interpolation: плавность без prediction
Для персонажей, которыми игрок не управляет (другие игроки, NPC), prediction неприменим - нельзя предсказать произвольный ввод. Snapshot interpolation решает эту задачу: вместо предсказания - плавная интерполяция между двумя последними полученными snapshot. Клиент рендерит состояние, которое было в прошлом (на величину interpolation delay - обычно 2-3 тика). Выглядит идеально плавно, но с небольшим отставанием. Overwatch использует интерполяцию 64 мс для других игроков: визуально плавно, и задержка незаметна для наблюдателя. Для контролируемого персонажа - prediction без интерполяции.
Extrapolation (экстраполяция) как альтернатива: продолжить траекторию движения объекта за пределы последнего snapshot. Менее latency, но артефакты при смене направления ('пробежка сквозь стену'). Большинство игр используют интерполяцию, не экстраполяцию для других игроков.
Snapshot interpolation добавляет задержку отображения других игроков. Зачем её принимают как trade-off?
Ключевые идеи
- **Client-side prediction** применяет ввод немедленно без ожидания сервера - управление ощущается мгновенным при любом пинге.
- **Reconciliation** исправляет расхождение prediction с авторитарным серверным состоянием через replay истории вводов; визуальное сглаживание скрывает коррекцию.
- **Rollback netcode** (GGPO) позволяет файтингам работать без ощутимой задержки: предсказать - показать кадр - при ошибке откатить и пересимулировать.
- **Snapshot interpolation** обеспечивает плавность для объектов с неизвестным вводом (другие игроки) ценой 100-150 мс визуальной задержки.
Связанные темы
Prediction и reconciliation - продолжение сетевой архитектуры мультиплеера:
- Multiplayer Networking — Client-side prediction решает главный недостаток авторитарного сервера - задержку; это следующий уровень той же архитектуры
- Физический движок — Детерминированная физическая симуляция необходима для reconciliation: клиент и сервер должны получать одинаковые результаты из одних данных
Вопросы для размышления
- При reconciliation клиент replay-ит историю вводов. Если в истории 60 кадров (1 секунда лага), replay может занять больше кадра. Как современные движки решают эту проблему?
- Rollback netcode требует сохранять полное состояние симуляции каждый кадр. Как оптимизировать это для игры с 10 000 динамическими объектами?
- Snapshot interpolation задерживает отображение на 100 мс. В каком жанре игр это неприемлемо и почему?