Мегапакет: як розробникам Factorio вдалося вирішити проблему з мультиплеєром на 200 гравців

Мегапакет: як розробникам Factorio вдалося вирішити проблему з мультиплеєром на 200 гравців
У травні цього року я брав участь як гравець у MMO-заході KatherineOfSky. Я помітив, що коли кількість гравців досягає певної кількості, через кожні кілька хвилин частина з них «відвалюється». На щастя для вас (але не для мене), я був одним із тих гравців, які відключалися кожен разнавіть при наявності хорошого підключення. Я сприйняв це як особистий виклик та почав шукати причини проблеми. Через три тижні налагодження, тестування та виправлень помилка нарешті усунена, але ця подорож була не такою простою.

Проблеми розрахованих на багато ігор дуже важко відстежити. Зазвичай вони виникають у дуже конкретних умовах параметрів мереж та за дуже специфічних станів гри (у даному випадку – наявність понад 200 гравців). І навіть коли вдається відтворити проблему, її неможливо належним чином налагоджувати, тому що вставка контрольних точок зупиняє гру, плутає таймери і зазвичай призводить до завершення з'єднання через перевищення терміну очікування. Але завдяки впертості та чудовому інструменту під назвою незграбний мені вдалося з'ясувати, що відбувається.

Якщо коротко: через помилку і неповну реалізацію симуляції стану затримки клієнт іноді опинявся в ситуації, коли йому доводиться за один такт відправляти мережевий пакет, що складається з дій вибору, що вводяться гравцем, приблизно 400 ігрових сутностей (ми називаємо його «мегапакетом»). Після цього сервер не тільки повинен правильно отримати всі ці дії введення, але й надіслати їх решті клієнтів. Якщо ти маєш 200 клієнтів, це швидко стає проблемою. Канал до сервера швидко забивається, що призводить до втрати пакетів та каскаду повторно запитаних пакетів. Відкладення дій введення потім призводить до того, що ще більше клієнтів починає відправляти мегапакети, і їхня лавина стає ще сильнішою. Успішним клієнтам вдається відновитися, решта «відвалюються».

Мегапакет: як розробникам Factorio вдалося вирішити проблему з мультиплеєром на 200 гравців
Проблема була досить фундаментальною, і я пішов 2 тижні на її усунення. Вона досить технічна, тому нижче поясню соковиті технічні подробиці. Але для початку вам потрібно знати, що з версії 0.17.54, випущеної 4 червня, в умовах тимчасових проблем з підключенням мультиплеєр став більш стабільним, а приховування затримок набагато менш глючним (менше гальмувань і телепортування). Крім того, я змінив спосіб приховування затримок у бою і сподіваюся, що завдяки цьому вони будуть трохи плавнішими.

Розрахований на багато користувачів мегапакет — технічні подробиці

Якщо пояснювати спрощено, то мультиплеєр у грі працює наступним чином: всі клієнти симулюють стан гри, отримуючи та відправляючи тільки введення гравця (називається «діями введення», Input Actions). Основне завдання сервера – передача Input Actions та контроль того, що всі клієнти виконують однакові дії в одному такті. Докладніше про це можна прочитати в пості FFF-149.

Оскільки сервер повинен приймати рішення про те, які дії потрібно виконувати, дії гравця рухаються приблизно таким шляхом: дія гравця -> клієнт гри -> мережа -> сервер -> мережа -> клієнт гри. Це означає, що кожна дія гравця виконується тільки після того, як здійснить шлях туди-назад по мережі. Через це гра здавалася б жахливо гальмівною, тому майже відразу ж після появи в грі мультиплеєра було введено механізм приховування затримок. Приховування затримки імітує введення гравця без урахування дій інших гравців та прийняття рішень сервером.

Мегапакет: як розробникам Factorio вдалося вирішити проблему з мультиплеєром на 200 гравців
У Factorio є ігровий стан Game State - Це повний стан карти, гравця, сутностей та всього іншого. Воно детерміновано симулюється в усіх клієнтах виходячи з дій, отриманих від сервера. Ігровий стан священний, і якщо він колись починає відрізнятися від сервера або будь-якого іншого клієнта, виникає розсинхронізація.

Крім Game State у нас є стан затримок Latency State. Воно містить невелику підмножину основного стану. Latency State не священно і просто представляє картину того, як виглядатиме стан гри в майбутньому на підставі введених гравцем Input Actions.

Для цього ми зберігаємо копію створюваних Input Actions у черзі затримок.

Мегапакет: як розробникам Factorio вдалося вирішити проблему з мультиплеєром на 200 гравців
Тобто в кінці процесу на стороні клієнта картина виглядає приблизно так:

  1. Застосовуємо Input Actions всіх гравців до Game State оскільки ці дії введення були отримані від сервера.
  2. Видаляємо з черги затримок усі Input Actions, які, за даними сервера, вже були застосовані до Game State.
  3. видаляємо Latency State і скидаємо його, щоб воно виглядало так само, як і Game State.
  4. Застосовуємо всі дії із черги затримок до Latency State.
  5. На підставі даних Game State и Latency State рендері гру гравцю.

Все це повторюється у кожному такті.

Занадто важко? Не розслаблюйтесь, це ще не все. Щоб компенсувати ненадійність Інтернет-з'єднань, ми створили два механізми:

  • Пропущені такти: коли сервер вирішує, що Input Actions будуть виконані в такті гри, то якщо він не отримав Input Actions якогось гравця (наприклад, через затримку, що збільшилася), він не чекатиме, а повідомить цього клієнта «я не врахував твої Input Actions, постараюся додати їх у наступний такт». Так зроблено для того, щоб через проблеми зі з'єднанням (або комп'ютером) одного гравця оновлення карти не сповільнювалося у всіх інших. Варто зауважити, що Input Actions не ігноруються, а просто відкладаються.
  • Затримка повного шляху туди-назад: сервер намагається припустити, яка затримка передачі даних туди-назад між клієнтом та сервером для кожного клієнта. Кожні 5 секунд він за необхідності обговорює з клієнтом нову затримку (залежно від того, як поводилося підключення в минулому), і відповідним чином збільшує або зменшує затримку передачі даних туди-назад.

Самі по собі ці механізми досить прості, але коли вони використовуються спільно (що часто трапляється при проблемах зі з'єднанням), логіка коду стає важкокерованою та з купою прикордонних випадків. Крім того, коли в справу вступають ці механізми, сервер та черга затримок повинні правильно впроваджувати особливе. Вхідна дія під назвою StopMovementInTheNextTick. Завдяки цьому при проблемах зі з'єднанням персонаж не бігтиме сам по собі (наприклад, під поїзд).

Тепер потрібно пояснити, як працює вибір сутностей. Один з типів, що передаються Вхідна дія - Це зміна стану вибору сутності. Воно повідомляє всім, яку сутність гравець навів курсор миші. Як можна зрозуміти, це одна з найчастіших дій введення, що надсилаються клієнтами, тому для економії пропускної спроможності каналу ми оптимізували його так, щоб воно займало якнайменше місця. Це реалізовано так: при виборі кожної сутності замість збереження абсолютних високоточних координат карти гра зберігає низькоточне відносне зсув від попереднього вибору. Це добре працює, тому що виділення мишею зазвичай відбувається дуже близько до попереднього виділення. Через це виникають дві важливі вимоги: Input Actions ніколи не можна пропускати і необхідно виконувати їх у правильному порядку. Ці вимоги задовольняються для Game State. Але оскільки завдання Latency state в тому, щоб «виглядати досить добре» для гравця, у стані затримок вони не задовольняються. Latency State не враховує багато прикордонних випадків, пов'язані з пропуском тактів і зміною затримок передачі туди-назад.

Ви вже можете здогадатися, до чого йдеться. Зрештою ми починаємо бачити причини проблеми мегапакету. Корінь проблеми полягає в тому, що у прийнятті рішення про те, чи потрібно передавати дію зміни вибору, логіка вибору сутностей покладається на Latency Stateа цей стан не завжди містить правильну інформацію. Тому мегапакет генерується приблизно так:

  1. У гравця виникли проблеми зі з'єднанням.
  2. У справу вступають механізми пропуску тактів і регулювання затримки передачі туди-назад.
  3. Черга стану затримок не враховує цих механізмів. Це призводить до того, що деякі дії видаляються передчасно або виконуються в неправильному порядку, що призводить до неправильного Latency State.
  4. У гравця пропадає проблема зі з'єднанням, і він, щоб наздогнати сервер, симулює до 400 тактів.
  5. У кожному такті генерується та готується до відправки на сервер нову дію зміну вибору сутності.
  6. Клієнт відправляє серверу мегапакет із 400 з лишком змін вибору сутностей (і з іншими діями: стан стрільби, ходьби тощо теж страждали від цієї проблеми).
  7. Сервер отримує 400 дій уведення. Так як йому не дозволено пропускати жодної дії введення, він наказує всім клієнтам виконувати ці дії та відправляє їх через мережу.

Іронія у тому, що механізм, призначений для економії пропускної спроможності каналу, у результаті створював величезні мережеві пакети.

Ми вирішили цю проблему, виправивши всі прикордонні випадки оновлення та підтримки черги затримок. Хоча це й зайняло чимало часу, зрештою варто було реалізувати все правильно, а не покладатися на швидкі хакі.

Джерело: habr.com

Додати коментар або відгук