Мультиплеер в мобильной игре: как тестировать сеть, лаги и реконнект без ста человек
Самый частый способ «протестировать мультиплеер», который я видел: два телефона, один Wi-Fi, сыграли матч, всё двигается, ок. Потом игра выходит, и первые же отзывы: «телепортируется», «выстрелил, а урона нет», «вылетело из матча и не пустило обратно, засчитали поражение». Ни один из этих багов не про игровую логику. Все они про сеть, а сеть на одном Wi-Fi не тестируется вообще: задержка два миллисекунды, потерь ноль, обрывов нет.
Настоящий игрок сидит в метро на 4G с задержкой 150 мс и потерей 3 % пакетов, сворачивает игру на звонок, переходит из Wi-Fi в LTE, заезжает в лифт. Вот это и надо воспроизводить. Ниже разбираю, как устроена сеть в игре, чтобы понимать, что именно ломать, какие классы багов она рождает, и как всё это тестировать одному человеку без сотни игроков.
Как устроена сеть в игре, коротко и без математики
Чтобы осмысленно ломать, нужно понимать, что под капотом. Схем в реальности три.
Авторитарный сервер. Клиент отправляет только ввод (нажал вперёд, выстрелил), сервер считает мир и рассылает всем состояние. Клиент никому не верит, и ему никто не верит. Так устроено большинство шутеров и всё, где есть деньги на кону. Минус: между нажатием и ответом сервера проходит round-trip, и без хитростей персонаж реагирует с задержкой.
Клиентское предсказание и интерполяция. Хитрости из предыдущего абзаца. Клиент не ждёт сервер, а сразу двигает своего персонажа, предполагая, что сервер согласится. Если сервер не согласился, клиента «откатывают» к серверному состоянию, и это тот самый рывок назад, который игроки зовут резинкой. Чужих игроков клиент показывает не «где они сейчас», а «где они были 100 мс назад», плавно интерполируя между снапшотами. Классический разбор у Гленна Фидлера: What Every Programmer Needs To Know About Game Networking и Snapshot Interpolation. Второй обязательный текст: Source Multiplayer Networking от Valve, там же про lag compensation: сервер при проверке выстрела «отматывает» мир к моменту, который видел стрелок.
Детерминированный lockstep и rollback. Стратегии и файтинги. Все клиенты считают одну и ту же симуляцию из одинакового ввода, по сети летит только ввод. Работает, пока симуляция строго детерминирована: один и тот же ввод даёт одно и то же состояние на всех устройствах. Одна разница в float-математике на разных процессорах, и клиенты «разъезжаются», это десинк. Rollback-вариант (GGPO) угадывает ввод соперника, а при ошибке откатывает и пересчитывает несколько кадров.
Плюс есть P2P и модель «один из игроков хост», популярная в кооперативе и на Photon-подобных платформах. Там появляется отдельный класс проблем: хост вышел, и кто теперь хост.
Зачем это тестировщику: каждая схема падает по-своему. Авторитарный сервер даёт резинку и «выстрелил в стену». Lockstep даёт десинк. P2P даёт сломанную миграцию хоста. Не зная схему, ты ловишь симптомы наугад.
Что ломается: классы багов мультиплеера
Латентность. При 150–250 мс предсказание начинает расходиться с сервером. Симптомы: рывки назад, телепорты чужих игроков, попадание по врагу без урона, подбор предмета, который «отменяется» через полсекунды. Отдельно смотри, что происходит с анимациями и звуком при откате: часто выстрел уже проиграл звук и вспышку, а сервер его не засчитал, и игрок уверен, что игра врёт.
Потеря пакетов и jitter. Потеря сама по себе не страшна, если протокол готов. Страшно, что при потере события приходят не в том порядке или дублируются. Типовые баги: подобранный предмет появился дважды, дверь открылась и закрылась, чат-сообщение пришло раньше, чем событие, о котором оно сообщает. Jitter (неравномерная задержка) ломает интерполяцию: чужие игроки двигаются рывками даже при хорошем среднем пинге.
Десинк. Для lockstep-игр баг номер один. Два клиента показывают разную картину мира, а узнают об этом только при проверке контрольных сумм или когда у одного игрока юнит жив, а у другого мёртв. Причины почти всегда в недетерминизме: float на разных архитектурах, порядок обхода словаря, случайное число не из общего сида, зависимость от времени кадра. Тестировать это одними глазами невозможно, нужен хэш состояния, который клиенты сравнивают каждые N тиков.
Реконнект. Самая дорогая для игрока категория. Он потерял связь на десять секунд и получил поражение, минус рейтинг, потерянную ставку. Что проверять: обрыв на 3 секунды (должен пережить незаметно), на 30 секунд (вернуться в тот же матч в актуальное состояние), на 5 минут (матч закончился, что видит игрок и что записано в его статистику). Обрыв в момент ключевого действия: покупка, конец раунда, момент смерти. Переключение Wi-Fi на LTE: IP меняется, сессия должна выжить. Сворачивание приложения: iOS через какое-то время приостанавливает процесс, сокеты рвутся, после возврата игра должна переподключиться сама, а не висеть на «Соединение…».
Миграция хоста. Если игра P2P или хост-клиент: хост вышел, свернул игру, у него сел телефон. Кто становится хостом, что происходит с состоянием матча, за сколько секунд, что видят остальные. Отдельный кейс: хост вышел ровно в момент, когда другой игрок совершал действие.
Матчмейкинг. Отмена поиска и мгновенный повторный поиск. Два быстрых тапа по кнопке «Играть». Поиск при пустой очереди: таймаут и что дальше, боты или сообщение. Потеря сети во время поиска. Регион: игрока из Европы свело с Азией, и вот 300 мс пинга законно. Приглашение друга, который уже в другом матче.
Доверие клиенту. Всё, что клиент присылает как факт, а не как ввод, будет подделано. Если пакет «нанёс 9999 урона» сервер принимает без проверки, читеры найдут это за день. Про серверную авторитетность и подмену запросов подробно писал в разборе экономики мобильной игры, здесь то же самое, но в реальном времени.
Время. Таймер раунда считается на сервере или на клиенте? Если на клиенте, переведи часы телефона на час вперёд и посмотри, что станет с матчем. Конец раунда должен наступать по серверному времени, иначе у одного игрока раунд уже закончился, а у другого ещё идёт.
Нагрузка комнаты. Максимум игроков в комнате плюс один. Спам действиями: сто нажатий в секунду, флуд в чате, быстрый вход-выход одного игрока. Размер снапшота растёт с числом объектов, и на 20 игроках протокол, который летал на 4, может не влезать в MTU и дробиться.
Как воспроизводить сеть одному человеку
Ключевая мысль: мультиплеер тестируют не количеством людей, а качеством сети. Двух устройств хватает почти для всего, если между ними стоит контролируемая плохая сеть.
Инструменты сетевых условий.
- Network Link Conditioner для iOS и macOS. Ставится из Additional Tools for Xcode, на iPhone появляется в Настройки → Разработчик. Есть пресеты 3G, Edge, потерь, и можно задать свой профиль: задержка, потери, пропускная способность отдельно вверх и вниз.
- Android-эмулятор умеет
-netdelayи-netspeedиз командной строки, а в Extended Controls есть выбор типа сети, документация в Emulator networking. Для реального Android-устройства проще всего пустить трафик через ноутбук-роутер и ломать его там. - tc netem на Linux (man tc-netem): задержка, jitter, потери, дубли, переупорядочивание пакетов. Ставится на сервер игры или на Raspberry Pi, который раздаёт Wi-Fi телефонам. Это самый честный способ ломать сеть между реальными устройствами.
- Toxiproxy от Shopify (github): TCP-прокси с «токсинами» latency, bandwidth, timeout, slicer. Идеален для матчмейкинга, авторизации и всех HTTP-зависимостей вокруг матча.
- clumsy на Windows (jagt.github.io/clumsy): то же самое одним окошком, удобно для десктопного клиента или редактора.
- Unity Network Simulator из Multiplayer Tools (документация): пресеты сетей прямо в редакторе, работает с Netcode for GameObjects. А Multiplayer Play Mode запускает несколько клиентов из одного редактора, без сборки на устройства.
Профили сети, которые я держу под рукой.
- «Хороший 4G»: 60 мс, jitter 10, потери 0,5 %. Здесь всё должно быть идеально.
- «Плохой 4G»: 200 мс, jitter 80, потери 5 %. Здесь игра должна оставаться играбельной, резинка допустима, потеря прогресса нет.
- «Лифт»: 100 % потерь на 10 секунд, потом норма. Проверка тихого реконнекта.
- «Метро»: чередование 30 секунд нормы и 20 секунд обрыва в цикле. Проверка, что реконнект работает не один раз, а десять подряд.
- «Асимметрия»: вверх 50 кбит/с, вниз норма. Ловит игры, которые шлют слишком много от клиента.
- «Смена сети»: физически выключить Wi-Fi посреди матча, дождаться LTE.
Боты вместо людей. Чтобы заполнить комнату на 20 человек, не нужны 20 человек. Нужен headless-клиент, который подключается и шлёт случайный ввод. Обычно разработчики уже имеют такой для нагрузочного тестирования, и договориться о запуске десятка экземпляров дешевле, чем собирать команду. Если игра на Unity, тот же Multiplayer Play Mode даёт до нескольких клиентов бесплатно.
Матрица реконнекта
Это самый ценный артефакт, который можно принести в команду. Строки: момент обрыва. Столбцы: длительность. В ячейке: ожидаемое поведение, согласованное с геймдизайнером до тестирования, а не после.
Моменты обрыва, которые обязательно должны быть в строках:
- в лобби и во время поиска матча;
- в первые секунды матча, до первого снапшота;
- в середине матча в спокойный момент;
- в момент собственного действия: выстрел, покупка, использование способности;
- в момент собственной смерти или победы;
- на экране результатов, до начисления наград;
- во время сворачивания приложения и во время звонка.
Длительности: 3 секунды, 30 секунд, 3 минуты, «до конца матча». Для каждой ячейки три вопроса: что видит игрок в момент обрыва, что видят остальные, что записалось в статистику и кошелёк. Особенно последнее: если награда за матч начисляется на экране результатов, а игрок его не увидел, награда либо теряется, либо начисляется дважды при повторном входе. Оба варианта видел в проде.
На что смотреть кроме экрана
Глазами ловится треть багов. Остальное в цифрах, и хорошо, если разработчики дали к ним доступ через debug-оверлей.
- RTT и его разброс. Если игра показывает пинг, сверь его с реальным, который ты выставил в кондишенере. Расхождение в два раза само по себе баг индикатора.
- Потери и переотправки. Растут ли они на «хорошем 4G»? Значит, протокол сам создаёт проблему.
- Тикрейт сервера под нагрузкой комнаты. Просел с 30 до 15, и вся резинка внезапно «из-за сети», хотя сеть ни при чём.
- Размер снапшота и трафик в секунду. Мобильный игрок платит за трафик и батарею. Игра, которая ест 30 МБ за матч, получит отзывы «жрёт интернет».
- Хэш состояния для lockstep. Если его нет, попроси. Без него десинк не воспроизводится, а только «иногда странно».
- Счётчик реконнектов за сессию и время каждого. Реконнект, который занимает 12 секунд при обрыве на 3, тоже баг.
Чек-лист, чтобы не держать всё в голове
- Узнал схему сети: авторитарный сервер, lockstep или P2P. От этого зависит, что искать.
- Прогнал матч на шести профилях сети, а не на офисном Wi-Fi.
- Заполнил матрицу реконнекта: 7 моментов × 4 длительности, с ожидаемым поведением от геймдизайнера.
- Проверил смену Wi-Fi → LTE и сворачивание приложения посреди матча.
- Проверил обрыв в момент начисления награды: не потерялась и не удвоилась.
- Матчмейкинг: двойной тап, отмена и повтор, пустая очередь, обрыв во время поиска.
- Миграция хоста, если схема P2P: выход хоста в спокойный момент и в момент чужого действия.
- Подменил через прокси пакет с «фактом» вместо ввода и убедился, что сервер его отверг.
- Перевёл часы клиента и проверил таймеры матча.
- Заполнил комнату ботами до максимума плюс один, посмотрел тикрейт и размер трафика.
- Для lockstep: включил сравнение хэшей состояния и прогнал длинный матч.
- Посмотрел, что игра показывает игроку в момент обрыва: «Соединение…» с таймаутом и выходом, а не вечный спиннер.
Мультиплеер выглядит как самая сложная область тестирования, но на практике он сводится к одному навыку: уметь сделать сеть плохой ровно настолько, насколько нужно, и ровно в тот момент, который нужен. Два телефона, ноутбук между ними и час на настройку окупаются первым же найденным «вылетело и засчитали поражение».