← Модуль 3/Клиент-предсказание
EN
Модуль 3 · 3D-революция (1993–1999)

Клиент-предсказание: как спрятать пинг

QuakeWorld (1996) сделал онлайн-шутер играбельным на дайалапе одной идеей: клиент рисует результат ввода сразу, не дожидаясь сервера, а сервер позже подтверждает и при расхождении поправляет. Авторитет — у сервера, отзывчивость — у клиента.
глубокий~16 мин
Суть за 30 секунд
Если сервер авторитетен и клиент ждёт ответа на каждый ввод, движение запаздывает на целый пинг (RTT) — на дайалапе 1996-го это 200–400 мс, неиграбельно. Client-side prediction (Кармак, QuakeWorld, август 1996): клиент сразу применяет ввод локально той же физикой, что и сервер, и шлёт ввод с порядковым номером. Сервер — авторитет: симулирует и присылает состояние + номер последнего обработанного ввода. Клиент сверяет свой прогноз на этот номер с истиной; при совпадении (обычный случай) — ничего не делает, при расхождении — откатывается к серверному состоянию и переигрывает все ещё не подтверждённые вводы (server reconciliation). Чужих игроков предсказать нельзя (нет их ввода) → их интерполируют по прошлым снапшотам, рисуя слегка в прошлом. А чтобы выстрел по движущейся цели засчитывался честно, сервер отматывает время назад (lag compensation). Авторитет сервера ловит читы; предсказание — лишь презентация.

Проблема: авторитет стоит пинга

Наивная схема «сервер решает всё» безопасна, но медленна. Нажал W → пакет летит на сервер → сервер двигает → состояние летит обратно → только теперь персонаж шагнул. Задержка движения = полный round-trip:

tmove= RTT= tup+tdown

При RTT 120 мс каждый шаг, прыжок и поворот отстаёт на 120 мс — управление «по почте». Нельзя просто отдать власть клиенту («я на X, у меня 999 HP») — это открывает дверь читам. Нужно и авторитет сервера, и мгновенный отклик. Разрешение конфликта — предсказание.

Предсказание + сверка (reconciliation)

Клиент делает две вещи разом на каждый ввод: (1) применяет его локально немедленно — персонаж двигается в этом же кадре; (2) отправляет ввод на сервер с монотонным порядковым номером. Сервер обрабатывает ввод, двигает авторитетно и в ответном снапшоте сообщает «последний обработанный ввод = N» (ack) и итоговое состояние.

Клиент хранит историю своих вводов. Получив ack на N, он: выкидывает из истории всё ≤ N; берёт серверное состояние как истину; и заново переигрывает поверх него ещё не подтверждённые вводы N+1, N+2, … теми же формулами. Если прогноз совпал с сервером (так в 99% кадров при честной сети) — позиция не дёрнется. Если разошёлся (клиент думал, что бежит, а сервер знал про стену) — позиция мягко съезжает к правде.

Клиент Сервер t0: ввод #50двигаюсь сразу ввод+#50 → симуляция, ack=50 ← состояние+ack сверкасовпало → 0 правок RTT прошёл, но игрок двигался с t0 — пинг спрятан

Числовой пример. RTT 120 мс, клиент на вводе #50. Без предсказания персонаж замер бы до t=120 мс. С предсказанием он шагнул на t=0; в t=120 мс приходит ack на #47 с серверной позицией. Клиент ставит позицию = серверная (на момент #47) и мгновенно переигрывает #48, #49, #50 → возвращается ровно туда, где уже рисует. Игрок ничего не заметил. Если же между #47 и #50 сервер увидел стену, переигровка упрётся в неё → лёгкий «доводчик» к правде вместо телепорта.

Жёсткое требование — детерминизм: клиент и сервер должны гонять идентичный код движения. Разойдись формулы на копейку — прогноз будет постоянно мазать, и персонажа будет «колбасить» поправками каждый кадр. Поэтому физику игрока пишут один раз и компилируют в обе стороны.

Чужие игроки: интерполяция в прошлом

Свой ввод предсказать можно — он у тебя в руках. Чужой нельзя: ты не знаешь, куда враг нажмёт. Поэтому остальных не предсказывают, а интерполируют между двумя последними полученными снапшотами — и намеренно рисуют чуть в прошлом (буфер интерполяции Δ, обычно 50–100 мс), чтобы всегда иметь две точки для плавного lerp:

prender= (1−α)·S(t0) + α·S(t1) , α= t−t0t1−t0

где t = t_now − Δ, а S(t0), S(t1) — снапшоты до и после. Альтернатива — экстраполяция (dead reckoning): продолжить движение врага по последней скорости. Дёшево, но если он свернул — будет рывок-«резинка» при коррекции. Большинство быстрых шутеров выбирают интерполяцию (платим фиксированной задержкой за плавность), а экстраполяцию держат для редких пропусков пакетов.

Lag compensation: сервер отматывает время

Теперь конфликт: ты стреляешь по врагу, которого видишь в прошлом (интерполяция + пинг). На сервере «сейчас» враг уже сдвинулся. Если сервер проверит попадание по своему «сейчас» — ты будешь мазать по тому, во что целился. Решение Яна Бернье (Valve, Counter-Strike; статья «Latency Compensating Methods», 2001): сервер откатывает мир назад к тому моменту, который видел стрелок, и там проверяет луч:

trewind= tnow− (tlat+Δ)

Сервер хранит кольцевой буфер недавних позиций всех игроков; для выстрела клиента он восстанавливает хитбоксы на t_rewind (его латентность + его буфер интерполяции) и трассирует там. Цена компромисса — знаменитое «я уже зашёл за угол, но всё равно умер»: с точки зрения стрелка ты ещё был на виду, и сервер встал на сторону стрелка (favor-the-shooter). Честно сделать обоим хорошо при ненулевом пинге математически нельзя — выбирают, кому отдать правду.

Хардкор · инженерия: тикрейт, снапшоты, UDP и потеря пакетовможно пропустить
  • UDP, не TCP. TCP гарантирует порядок и доставку → при потере пакета всё встаёт в очереди (head-of-line blocking), а в шутере устаревший пакет уже не нужен — нужен свежий. Поэтому шлют UDP и сами решают, что требует надёжности (события: «дверь открыта»), а что можно терять (позиции — придёт следующий снапшот).
  • Тикрейт. Сервер симулирует фиксированными тиками (Quake — десятки/с; CS:GO — 64/128; Overwatch поднимал до ~63). Выше тик = точнее симуляция и lag-comp, но дороже CPU и трафик. Клиент рендерит на своём fps, между тиками — интерполяция/предсказание.
  • Снапшоты и дельта-сжатие. Сервер шлёт состояние мира N раз/с; чтобы влезть в канал — дельты от последнего подтверждённого клиентом снапшота (только то, что изменилось) + приоритет ближних/видимых сущностей (PVS из урока Quake режет и сетевой объём, не только отрисовку).
  • Избыточность ввода. Над UDP клиент в каждом пакете досылает последние несколько вводов, а не только новый — потеря одного пакета не создаёт дыру в истории, сервер возьмёт дубль из следующего.
  • Джиттер-буфер. Пакеты приходят неравномерно; небольшой буфер сглаживает разброс времён прибытия ценой ещё чуть-чуть задержки. Тот же буфер интерполяции Δ.
Хардкор · хостинг: авторитет, детерминизм и где это считатьможно пропустить
  • Модель авторитета. Dedicated-сервер как единственный источник истины — стандарт для соревновательных игр (античит, нет «host advantage»). P2P/listen-server дешевле (нет инфраструктуры), но хост-игрок имеет нулевой пинг и его сложнее защитить от читов.
  • Детерминизм через платформы. Если прогноз/линкстеп опирается на полное совпадение симуляции, всплывает беда float: один и тот же код на разных CPU/компиляторах может дать чуть разный результат (порядок операций, FMA, x87 vs SSE). RTS с лок-степом фиксируют математику (fixed-point или строго оговорённый float) — иначе клиенты разъезжаются (desync).
  • Консистентность vs задержка. Это та же дилемма, что в распределённых системах: сильная консистентность (ждать авторитет) безопасна и медленна; оптимистичное локальное действие быстро, но требует сверки и отката. Геймдев выбрал оптимизм + reconciliation задолго до того, как это стало мейнстримом в вебе.
  • Региональные серверы. Физику пинга не обмануть: RTT ≥ 2·расстояние/c. Поэтому матчмейкинг привязывает к ближайшему дата-центру — снизить базовый RTT, на котором работает всё предсказание.
Аналогия
Клиент-предсказание — это оптимистичный UI: жмёшь «отправить» — сообщение появляется сразу (предсказание), хотя сервер ещё не подтвердил; придёт ошибка — отрисовку откатят (reconciliation). Или автодополнение: набор продолжается по догадке, правка приходит, если догадка не сошлась. А lag compensation — как судья в записи матча: чтобы засчитать гол честно, смотрят тот кадр, когда был удар, а не «сейчас».
Почему это важно
Это решение жёсткого конфликта авторитет против отзывчивости, которое всплывает везде, где есть сеть и пользователь, ждущий мгновенной реакции. Связка «действуй оптимистично локально → сверься с авторитетом → откатись при расхождении» — каноничный паттерн, и игры доказали его боем за десятилетие до того, как оптимистичные апдейты стали нормой в вебе и спекулятивное исполнение — в процессорах и LLM.
🔁 За пределами игр — куда это переносится
Предсказание+сверка — это спекулятивное исполнение (действуй по догадке, откатись при промахе) и оптимистичная конкурентность (не блокируйся в ожидании, проверь в конце).

Системы / фронтенд: оптимистичные апдейты UI (React/Redux: показать результат до ответа сервера, откатить при ошибке) — буквально prediction+reconciliation. Optimistic concurrency control в БД (версии/CAS вместо локов). Eventual consistency и CRDT: локальная правка сразу, схождение позже.

ML / AI: speculative decoding — точная копия приёма: дешёвая draft-модель «предсказывает» несколько токенов вперёд, большая модель их верифицирует одним проходом и откатывает с первого расхождения, переигрывая дальше — это reconciliation по номеру ввода, слово в слово. Off-policy RL: распределённые акторы (IMPALA) действуют устаревшей политикой, а корректирующий importance-sampling (V-trace) правит рассинхрон обучения — тот же «оптимистично действуй, потом поправь по авторитету».

Железо: branch prediction + speculative execution в CPU: процессор угадывает ветку и считает наперёд, при промахе сбрасывает конвейер (rollback) — предсказание с откатом на кремнии.

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

🔧 Запусти и поковыряй — на домашнем компе
Во что поиграть — ниже (🕹). Здесь — увидеть предсказание счётчиками и консолью:
🔧 Поковырять (debug) ~30 мин, Source/Quake
В Source-игре (CS:GO/2, TF2) открой консоль: net_graph 3 — пинг, потери, тикрейт, интерполяция. Покрути cl_interp / cl_interp_ratio (буфер интерполяции чужих) и cl_predict 0/1 — с выключенным предсказанием почувствуешь движение «по пингу». Включи sv_showhitboxes 1 и (на своём сервере) визуализацию lag-comp: увидишь, как сервер ставит хитбоксы туда, где цель была в прошлом. В QuakeWorld-сорс-порте — cl_predict и net-графики аналогично.
🧪 Потестить (глазами QA) ~15 мин
Спровоцируй артефакты: искусственно подними пинг/потери (net_fakelag, net_fakeloss в Source) и лови «резину» (rubber-banding) при упоре в стену, варп чужих игроков при потерях, «смерть из-за угла» (lag-comp favor-the-shooter), рассинхрон при дёрганой сети. Чеклист сетевых артефактов.
Чеклист: выключил cl_predict и почувствовал пинг; покрутил cl_interp и увидел плавность-vs-задержку; воспроизвёл «смерть за углом» и понял, почему сервер на стороне стрелка. Глубокий [LAB]-симулятор лага/предсказания — в Модуле 4.

🕹 В какие игры поиграть — и что заметить

От первой реализации в QuakeWorld к индустриальному стандарту и контрастам (лок-степ, роллбэк). По каждому: что внутри и во что сыграть / что ввести в консоль.

QuakeWorld 1996 · где всё началось

Первая широкая реализация client-side prediction (Кармак, .plan от 16 авг 1996: «позволяю клиенту угадывать результат движения, пока не придёт авторитетный ответ сервера»). Сделала Quake играбельным на дайалапе и породила соревновательный онлайн-шутер.

🎮 Сыграй: в сорс-порте (ezQuake/FTE QuakeWorld) поставь высокий пинг и потыкай cl_predict 0 vs 1 — с нулём движение запаздывает на пинг, с единицей мгновенно. Ты щёлкаешь ровно тот тумблер, что Кармак добавил в 96-м.

Counter-Strike lag compensation

Бернье в Valve добавил поверх предсказания lag compensation: сервер отматывает хитбоксы в прошлое стрелка. Отсюда фирменное «зашёл за угол — и всё равно убит».

🎮 Сыграй: в CS введи net_graph 3, посмотри пинг/тик/интерп. Поймай момент, когда умер уже за укрытием — это не баг, это favor-the-shooter: на экране врага ты ещё был на виду, и сервер отмотал к его кадру.

Overwatch высокий тик + favor-the-shooter

Подробно разобран на GDC 2017 (Тим Форд / Tim Ford): высокий тикрейт, предсказание, интерполяция и осознанный выбор «верим стрелку». Хороший современный референс той же архитектуры.

🎮 Посмотри: в настройках сети включи оверлей (ping, tickrate). Сравни ощущение хитскана у героя с мгновенным выстрелом (lag-comp решает всё) и снаряда (летит — частично предсказывается на лету). Та же netcode-модель, разные виды оружия.

Файтинги (GGPO) контраст · rollback

Другая ветвь предсказания: rollback netcode (GGPO; Killer Instinct, Guilty Gear Strive). Предсказывают ввод оппонента (обычно «жмёт то же, что в прошлом кадре»), симулируют дальше, а при приходе реального ввода — откат и пересимуляция кадров. Никакого авторитетного сервера — детерминированный P2P-лок-степ с откатом.

🎮 Сыграй: в GGST/любом rollback-файтинге онлайн поймай микро-«тряску» персонажа при скачке пинга — это видимый откат+пересимуляция. Глубокий разбор rollback и лок-степа — в Модуле 4.

RTS-лок-степ контраст · StarCraft/AoE

Совсем другой выбор: при сотнях юнитов слать их позиции нереально. Шлют только команды, а симуляцию гоняют детерминированно и синхронно на всех машинах (lockstep). Латентность прячут не предсказанием, а небольшой задержкой команды (turn delay).

🎮 Сыграй: в старом StarCraft/AoE на плохой сети заметь, что лагует весь мир разом (ждём всех игроков), а не отдельный юнит резиной — это лок-степ, противоположность предсказанию. Почему так — детерминизм и объём (см. хардкор).

Связи
основа
Quake: настоящий 3D — QuakeWorld вырос из того же движка; PVS режет не только отрисовку, но и сетевой объём (кому какие сущности слать).
дальше
Камеры — следующий урок модуля. А таксономия нетокода (client-server / rollback / lockstep) и [LAB]-симулятор лага и предсказания ждут в Модуле 4: здесь основа, там — вглубь.
контраст
Doom (deathmatch) — ранний сетевой Doom был peer-to-peer лок-степом по модему: ввод всех ко всем, без авторитета и предсказания. QuakeWorld отказался от этого ради клиент-сервера.
Вопросы пытливого ума
Если сервер всё равно авторитет, зачем вообще предсказывать на клиенте?
Авторитет и презентация — разные слои. Сервер решает, что произошло «по правде» (античит, разрешение конфликтов). Предсказание решает, что игрок видит сейчас, чтобы управление не запаздывало на пинг. Без предсказания игра честная, но ощущается как «по почте». С предсказанием — отзывчивая, и сервер всё равно последнее слово оставляет за собой через reconciliation. Это не «или-или», а два независимых вопроса: «кто прав» и «что показать немедленно».
Почему UDP, а не надёжный TCP — ведь пакеты теряются?
Именно поэтому. TCP при потере блокирует всё последующее, пока не дошлёт потерянное (head-of-line blocking) — а в шутере устаревшая позиция уже бесполезна, нужна свежайшая. UDP даёт слать без гарантий и самим решать: позиции можно терять (придёт следующий снапшот), важные события (смерть, открытие двери) — продублировать или подтвердить вручную. Надёжность точечно, где нужна, вместо тотальной и тормозящей.
Почему чужих игроков рисуют в прошлом, а не экстраполируют вперёд?
Чтобы иметь две реальные точки для интерполяции и не угадывать. Экстраполяция (продолжить по скорости) выглядит хорошо, пока враг не сменил направление — тогда рывок-«резинка» при коррекции, и хуже всего по быстро маневрирующим целям. Интерполяция с буфером Δ платит фиксированной задержкой (50–100 мс), но движение всегда гладкое и соответствует тому, что реально прислал сервер. Для хитскана эту задержку компенсирует lag-comp на сервере.
Что ломается, если клиентская и серверная физика не идентичны?
Прогноз будет систематически расходиться с сервером → reconciliation будет дёргать позицию каждый кадр (постоянный «джиттер»/резина), даже на идеальной сети. Поэтому движение игрока пишут как один кусок кода для обеих сторон, а в лок-степ-играх воюют ещё и с недетерминизмом float между платформами (порядок операций, FMA) — там расхождение = полный desync, мир разъезжается. Детерминизм — не роскошь, а условие работы предсказания.
«Я умер, уже зайдя за угол» — это лаг, баг или дизайн?
Дизайн-компромисс lag compensation. Сервер отмотал хитбоксы к моменту выстрела стрелка; на его экране (с учётом его пинга и интерполяции) ты ещё высовывался. Сервер выбрал сторону стрелка (favor-the-shooter), потому что иначе мазали бы все, кто видит цель в прошлом. Сделать одновременно честно стрелку и убегающему при ненулевом пинге нельзя — разработчик осознанно решает, кому отдать спорный кадр.
Не открывает ли предсказание дверь читерам — клиент же сам себя двигает?
Нет, потому что предсказание не наделяет клиента властью. Клиент шлёт ввод, а не «свою позицию-истину»; сервер симулирует сам и может отвергнуть невозможное (телепорт, скорость выше лимита, стрельба сквозь стену). Клиент лишь заранее рисует то, что ожидает от сервера. Читы ловятся серверными проверками и тем, что итог всегда пересчитывает сервер; предсказание влияет только на то, что видно до подтверждения.
Что почитать