Бесплатная консультация
← Все статьи

Диагностика рассинхрона в сетевой игре

Рассинхрон в сетевой игре редко выглядит одинаково два раза. У одного игрока персонаж уже открыл дверь, у второго она ещё закрыта. На клиенте противник погиб, сервер продолжает считать его живым. Матч идёт нормально десять минут, а затем состояния участников начинают постепенно расходиться.

Трёхмерная модель здания: работа над окружением

Рассинхрон в сетевой игре редко выглядит одинаково два раза. У одного игрока персонаж уже открыл дверь, у второго она ещё закрыта. На клиенте противник погиб, сервер продолжает считать его живым. Матч идёт нормально десять минут, а затем состояния участников начинают постепенно расходиться.

Из-за этого desync часто воспринимают как случайную сетевую проблему, которую почти невозможно воспроизвести.

На практике рассинхрон обычно можно диагностировать. Главная проблема в другом: к моменту возникновения ошибки у команды часто нет данных, которые позволят понять, в какой момент состояния перестали совпадать.

Поэтому при разработке multiplayer-систем на Unity диагностику сетевого состояния лучше проектировать вместе с самим networking layer, а не добавлять после появления первых серьёзных проблем.

Сначала убедитесь, что это действительно рассинхрон

Не каждый визуальный скачок персонажа означает desync.

Высокий ping, jitter, packet loss и задержка доставки snapshot могут приводить к rubber-banding, поздним попаданиям и резким коррекциям позиции. Клиент какое-то время показывает предсказанное состояние, затем получает более актуальные данные и корректирует картинку.

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

Например:

Клиент A считает:

Health = 40

Клиент B:

Health = 20

Сервер:

Health = 40

И если это расхождение не является временным эффектом prediction/reconciliation, система уже находится в разных состояниях.

Поэтому первый вопрос при диагностике:

какие данные в вашей архитектуре являются источником истины?

Для server-authoritative модели это обычно состояние сервера.

Для lockstep или deterministic simulation несколько участников должны независимо получить одинаковый результат из одинакового набора входных данных.

Не определив это заранее, команда начинает сравнивать визуальные симптомы вместо состояния системы.

Без записи состояния диагностики не будет

Главная причина, по которой рассинхроны ищут неделями, — отсутствие данных о моменте расхождения.

Ноутбук с открытым графическим редактором

Лог вида:

Player desynced

практически бесполезен.

Намного полезнее понимать:

  • какой это был simulation tick;
  • какое состояние видел каждый участник;
  • какие input были обработаны;
  • какие сетевые сообщения пришли перед ошибкой;
  • какая версия игры использовалась;
  • какой RNG state был активен.

Для детерминированной модели один из самых полезных инструментов — контрольная сумма состояния.

Каждый клиент через фиксированный интервал вычисляет hash значимой части simulation state.

Условно:

Tick 15420

Client A: F7A381

Client B: F7A381

Client C: F7A381

Следующий шаг:

Tick 15421

Client A: 21B947

Client B: 21B947

Client C: A849D2

Теперь команда уже знает главное: проблема появилась между 15420 и 15421 тиком.

Вместо поиска ошибки в двадцатиминутной игровой сессии нужно исследовать один небольшой фрагмент симуляции.

Такую диагностическую инфраструктуру полезно проверять ещё во время технического аудита Unity-проекта, особенно если multiplayer уже является одной из ключевых систем продукта.

Не хешируйте всё одной цифрой

Общий hash помогает понять, когда состояния разошлись.

Рабочее место художника с графикой на экране

Но он не отвечает на вопрос где.

Поэтому на крупных проектах полезнее строить диагностику слоями.

Например:

GameStateHash

внутри:

PlayersHash

EnemiesHash

WorldHash

InventoryHash

RngHash

Если общий hash не совпал, сравниваются категории.

Допустим:

Players — OK

World — OK

Inventory — ERROR

Дальше можно разбить inventory ещё глубже:

Player 01 — OK

Player 02 — ERROR

и затем уже сравнивать конкретные свойства.

Так вместо сообщения:

«На 15421 тике произошёл desync»

команда получает:

«На 15421 тике впервые разошлось количество предметов у Player 02».

Чем ближе diagnostics подводит разработчика к конкретной системе, тем быстрее становится исправление.

Детерминизм ломается в предсказуемых местах

Если multiplayer-архитектура предполагает одинаковую симуляцию на нескольких машинах, существует несколько классических источников расхождения.

Плавающая точка

Floating-point вычисления могут давать различия между платформами, архитектурами и некоторыми реализациями вычислений.

Художник за работой над игровым визуалом

Небольшая погрешность сама по себе может быть незаметна:

10.000001

против

10.000002

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

Особенно внимательно стоит относиться к физике, movement simulation и системам, где результат текущего тика влияет на следующий.

Порядок обхода коллекций

Логика не должна зависеть от порядка элементов в структуре, если этот порядок не гарантирован.

Например, два клиента получили одинаковый набор сущностей, но обработали их в разной последовательности.

Если одна сущность изменяет общий ресурс или влияет на другую, итоговые состояния могут отличаться.

Поэтому для simulation-critical логики порядок обработки должен быть явным.

Random

Один из самых частых источников ошибок — несколько независимых обращений к случайным значениям.

Мобильная игра на экране смартфона в руках

Представим:

Клиент A вызвал RNG 152 раза.

Клиент B — 153.

Все следующие случайные значения уже будут разными.

Поэтому для детерминированной системы необходимо контролировать:

  • seed;
  • состояние генератора;
  • количество вызовов;
  • порядок вызовов;
  • разделение gameplay RNG и визуального RNG.

Частицы или случайная декоративная анимация не должны незаметно сдвигать генератор, от которого зависит gameplay.

Время

Логику симуляции опасно строить на локальном wall-clock time или нестабильном времени кадра.

Если одна машина работает с 144 FPS, а другая с 50 FPS, количество выполненных операций может отличаться.

Simulation-critical код должен опираться на согласованный tick или фиксированный шаг, если архитектура проекта требует детерминизма.

Именно поэтому выбор сетевой модели желательно проверить ещё на стадии прототипа механики. Поздняя смена подхода к синхронизации затрагивает намного больше систем, чем кажется в начале production.

Логируйте входные данные, а не только результат

Hash показывает, что система разошлась.

Игрок запускает мобильную игру на телефоне

Но для воспроизведения необходимо знать, что произошло перед этим.

Поэтому полезно хранить последние N тиков input history.

Например:

15417 — Move X=1 Y=0

15418 — Fire Weapon 2

15419 — Move X=0.7 Y=0.2

15420 — Ability 3

15421 — DESYNC

Это позволяет восстановить последовательность действий.

Для серверной архитектуры дополнительно полезно записывать ключевые сообщения:

  • spawn;
  • despawn;
  • ownership change;
  • damage;
  • inventory transaction;
  • ability activation;
  • scene transition;
  • reconnect;
  • изменение authoritative state.

При этом логировать абсолютно всё обычно не нужно.

Большой поток debug-информации сложно хранить и ещё сложнее анализировать. Лучше заранее определить набор событий, которые действительно способны изменить simulation state.

Воспроизведение важнее самого исправления

Пока баг невозможно воспроизвести по команде, любое исправление остаётся гипотезой.

Разработчик меняет код, запускает матч, ошибка не возникает и задача закрывается.

Через неделю desync появляется снова.

Намного надёжнее иметь возможность сохранить проблемную сессию и повторить её.

Минимальный replay для диагностики может содержать:

  • начальный state;
  • seed;
  • последовательность input;
  • tick rate;
  • ключевые сетевые события;
  • версию протокола;
  • версию билда.

Затем команда запускает тот же сценарий локально и проверяет, возникает ли divergence на том же simulation tick.

Если да — проблема из сетевой превращается в обычную воспроизводимую ошибку кода.

И это радикально сокращает время диагностики.

Сделайте replay частью автоматических проверок

Особенно неприятны рассинхроны, которые уже однажды были исправлены, но вернулись после изменения другой системы.

Мобильная игра на смартфоне крупным планом

Поэтому записи реальных проблемных сессий полезно превращать в regression tests.

Например, есть replay:

desync_inventory_15421

После исправления он проходит нормально.

Теперь его можно оставить в наборе автоматических проверок.

При изменении inventory, networking или simulation CI снова воспроизводит этот сценарий.

Если hash снова начинает расходиться, команда узнаёт об этом ещё до появления нового bug report от QA.

Такие проверки удобно включать в общий build pipeline проекта вместе с тестами и созданием внутренних билдов.

Необязательно проигрывать тысячи матчей после каждого commit. Более тяжёлые replay suites можно запускать ночью или после изменений в критичных сетевых системах.

Проверяйте номер версии протокола и билда

Часть «случайных рассинхронов» на самом деле оказывается несовместимостью версий.

Один клиент использует старую структуру данных, второй — новую.

Или сервер обновился, а часть пользователей ещё играет на предыдущем клиенте.

Поэтому в multiplayer-сессии необходимо явно фиксировать:

  • game version;
  • build number;
  • network protocol version;
  • content/data version;
  • commit или другой идентификатор сборки.

Несовместимые версии лучше не допускать в одну сессию, чем пытаться диагностировать последствия их взаимодействия.

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

Иногда проблема не в сети

Рассинхрон проявляется через multiplayer, но причина может находиться далеко от networking code.

Игра на телефоне в руках пользователя

Например:

  • один клиент загрузил другой набор данных;
  • локальное сохранение содержит устаревшее значение;
  • asset или config отличается между сборками;
  • script execution order влияет на симуляцию;
  • async-операция завершилась в другом порядке;
  • клиент получил другую версию remote config;
  • объект был создан с разным ID;
  • часть состояния не сбросилась после предыдущего матча.

Поэтому диагностика должна охватывать не только packets.

Если проблема появляется после второго или третьего матча, но никогда после первого, стоит проверять lifecycle состояния.

Если только на определённой платформе — различия платформ.

Если только после reconnect — восстановление authoritative state.

Если только при загрузке сохранения — сериализацию и migration.

Именно поэтому сложные сетевые баги часто требуют не точечного «починить networking», а полноценного аудита и улучшения существующей игры.

Разделяйте desync и плохое соединение

Тестирование multiplayer должно учитывать реальные условия сети.

Идеальные localhost-тесты не показывают большую часть проблем.

Полезно отдельно проверять:

  • latency;
  • jitter;
  • packet loss;
  • duplication;
  • reorder;
  • временный disconnect;
  • reconnect;
  • резкий скачок latency.

Но важно не смешивать две задачи.

Network simulation отвечает на вопрос:

как игра ведёт себя при плохом соединении?

Desync diagnostics отвечает на другой:

остаются ли состояния участников согласованными?

Например, при packet loss персонаж может визуально двигаться рывками, но после восстановления соединения прийти к правильному authoritative state.

Это плохой UX, но не обязательно логический desync.

А игра с идеальным ping может рассинхронизироваться из-за одного лишнего вызова RNG.

QA нужен сценарий, а не просьба «поиграть в мультиплеер»

Формулировка задачи:

Игровой контроллер на ярком фоне

«Поиграйте пару часов и посмотрите, будет ли desync»

даёт мало полезной информации.

Гораздо эффективнее построить матрицу.

Например:

Сценарий 1

Два игрока, стабильное соединение, 30 минут.

Сценарий 2

Четыре игрока, 100 ms latency.

Сценарий 3

Игрок отключается и возвращается.

Сценарий 4

Смена уровня во время высокой сетевой активности.

Сценарий 5

Долгая сессия с несколькими матчами подряд.

Сценарий 6

Один клиент находится на минимально поддерживаемом устройстве.

Каждый тест должен сохранять необходимые diagnostics.

Так QA и тестирование игры превращаются из случайного поиска бага в воспроизводимый процесс.

Сетевые системы особенно полезно тестировать ещё до content lock. Если архитектурная проблема обнаружится только на release candidate, стоимость её исправления будет значительно выше.

Что должно попадать в баг-репорт

Фраза:

«Игроков рассинхронило»

почти ничего не даёт разработчику.

Полезный report должен содержать как минимум:

  • номер и версию build;
  • network protocol version;
  • участников сессии;
  • роль каждого клиента или сервера;
  • первый tick расхождения;
  • state hashes;
  • input history перед ошибкой;
  • relevant network events;
  • seed и RNG state, если они влияют на симуляцию;
  • карту или сцену;
  • условия сети;
  • шаги воспроизведения;
  • replay или diagnostic dump;
  • видео, если визуальное проявление помогает понять контекст.

Хороший bug report позволяет разработчику начать исследование без нескольких дополнительных сообщений тестировщику.

Ещё лучше — если кнопка в debug build автоматически собирает большую часть этих данных в один архив.

Не собирайте diagnostics только после появления бага

Самая дорогая ошибка — начать добавлять логирование после первого серьёзного desync.

Клавиатура, геймпад и портативная консоль

Команда уже получила проблему, но повторить её не может.

Добавляется новый diagnostics build.

QA снова пытается поймать ошибку.

Она появляется через несколько дней, но оказывается, что не хватает ещё одного параметра.

Создаётся следующий build.

Так диагностика превращается в длинный цикл.

Намного дешевле заложить observability заранее:

  • state hashes;
  • tick counter;
  • build version;
  • input recording;
  • network statistics;
  • structured events;
  • replay;
  • debug dump.

Это небольшая дополнительная стоимость во время разработки и огромная экономия при первом сложном multiplayer-баге.

Когда полный детерминизм может быть слишком дорогим

Строгая детерминированность — не универсальная цель для любого multiplayer-проекта.

Она требует дисциплины во многих частях системы:

  • физике;
  • random;
  • порядке обработки;
  • времени;
  • математике;
  • сериализации;
  • платформенных различиях.

Для определённых жанров это оправдано.

В других проектах server-authoritative архитектура с prediction, interpolation и reconciliation может оказаться проще в разработке и поддержке.

Ключевой вопрос не в том, какая модель «правильнее».

Нужно выбрать архитектуру, которая соответствует:

  • жанру;
  • количеству игроков;
  • требованиям к latency;
  • объёму симуляции;
  • платформам;
  • возможностям команды;
  • стоимости backend.

И принимать это решение желательно до того, как на конкретный подход завязано большинство gameplay systems.

Чек-лист диагностики рассинхрона

Если сетевой проект уже сталкивается с desync, начните с короткого списка.

Геймпад в руках игрока

Архитектура

определён ли authoritative source;

  • понятно ли, какие системы должны быть детерминированы;
  • используется ли единый simulation tick.

Состояние

считаются ли state hashes;

  • можно ли определить первый разошедшийся tick;
  • можно ли сравнить состояние по подсистемам.

Random

известен ли seed;

  • синхронизировано ли RNG state;
  • отделён ли gameplay random от визуального.

Воспроизведение

записываются ли input;

  • можно ли проиграть сессию повторно;
  • сохраняются ли проблемные replays.

Версии

записывается ли build number;

  • существует ли network protocol version;
  • блокируются ли несовместимые клиенты.

QA

тестируются ли latency и packet loss;

  • есть ли reconnect-сценарии;
  • проверяются ли длинные сессии;
  • сохраняются ли diagnostics автоматически.

CI

есть ли regression replay;

  • прогоняются ли известные проблемные сценарии после изменений networking code.

Если на большинство вопросов ответ «нет», проблема не в том, что рассинхрон слишком сложный.

Проблема в том, что система пока не даёт команде достаточно информации для его расследования.

Диагностика должна быть частью сетевой архитектуры

Хороший multiplayer-код — это не только код, который синхронизирует игроков при нормальном соединении.

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

Поэтому state hashes, replay, structured logs и versioning — не временные debug-инструменты, которые нужно выбросить после релиза. Это часть инфраструктуры сетевого проекта.

NoPanic Studio подключается к multiplayer-проектам на разных этапах: от проектирования сетевой модели и разработки networking-систем на Unity до диагностики проблем в уже существующей игре, QA, оптимизации и подготовки проекта к релизу.

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

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

Следующая статьяПять признаков того, что проект столкнётся с проблемами через месяц
→