Мегапакет: как разработчиците на Factorio успяха да решат проблема с мултиплейера за 200 играчи

Мегапакет: как разработчиците на Factorio успяха да решат проблема с мултиплейера за 200 играчи
През май тази година участвах като играч в MMO събитието KatherineOfSky. Забелязах, че когато броят на играчите достигне определен брой, на всеки няколко минути част от тях "отпадат". За ваше щастие (но не и за мен), аз бях един от тези играчи, които се отключваха всеки път, дори при добро интернет свързване. Възприех това като лично предизвикателство и започнах да търся причините за проблема. След три седмици отстраняване на грешки, тестване и корекции, грешката най-накрая бе отстранена, но това пътуване не беше толкова просто.

Проблемите с мултиплейър игрите са много трудни за проследяване. Обикновено те възникват при много специфични условия на мрежовите параметри и в много специфични състояния на играта (в този случай — наличието на повече от 200 играчи). И дори когато успеете да възпроизведете проблема, той не може да бъде правилно отстранен, защото добавянето на контролни точки спира играта, обърква таймерите и обикновено води до прекратяване на връзката заради изтичане на времето. Но благодарение на упоритостта и страхотния инструмент, наречен clumsy , успях да разбера какво точно се случва.

В кратце: поради грешка и непълна реализация на симулацията на забавянето клиентът понякога се оказва в ситуация, в която трябва в един такт да изпрати мрежови пакет, състоящ се от действията на играча, избиращи около 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 секунди, ако е необходимо, той обсъжда с клиента нова забавяне (в зависимост от това как се е държала връзката в миналото) и съответно увеличава или намалява забавянето при пренос на данни там-назад.

Самите тези механизми са доста прости, но когато се използват заедно (както често се случва при проблеми с връзката), логиката на кода става трудно управляемa и с много гранични случаи. Освен това, когато тези механизми влязат в действие, сървърът и опашката на забавянията трябва правилно да внедрят специфичен Input Action с имената StopMovementInTheNextTick. Благодарение на това, при проблеми с връзката, персонажът няма да тича сам по себе си (например, под влака).

Сега трябва да ви обясня как работи изборът на единици. Един от предаваните типове Input Action — това е промяна в състоянието на избора на същество. Тя информира всички за това, на кое същество играчът е насочил курсора на мишката. Както можете да разберете, това е едно от най-честите входни действия, изпращани от клиентите, така че за да спестим пропускната способност на канала, ние го оптимизирахме да заема колкото се може по-малко място. Това е реализирано така: при избора на всяко същество, вместо да се запазват абсолютните, високопрецизни координати на картата, играта запазва нископрецизно относително изместване от предишния избор. Това работи добре, защото отметка с мишката обикновено се извършва много близо до предишната отметка. Поради това възникват две важни изисквания: Input Actions никога не трябва да се пропускат и трябва да се изпълняват в правилния ред. Тези изисквания се изпълняват за Game State. Но тъй като задачата Latency state е да "изглежда достатъчно добре" за играча, в състояние на забавяне те не се изпълняват. Latency State не отчита много гранични случаи, свързани с пропускането на тактове и промяната на забавяния при предаване напред-назад.

Вие вече можете да се досетите накъде вървим. Накрая започваме да виждаме причините за проблема с мегапакета. Коренът на проблема е, че в вземането на решение дали да се предаде действието за промяна на избора, логиката на избора на същества разчита на Latency State, а това състояние не винаги съдържа вярна информация. Следователно мегапакетът се генерира по следния начин:

  1. Играчът има проблеми с връзката.
  2. Влиза в работа механизми за пропускане на тактове и регулиране на забавянето на предаването напред-назад.
  3. Опашката на забавянията не взема предвид тези механизми. Това води до факта, че някои действия се изтриват преждевременно или се изпълняват в грешен ред, което води до неправилно Latency State.
  4. Играчът получава обратно проблема с връзката и той, за да навакса сървъра, симулира до 400 такта.
  5. Във всеки такт се генерира и подготвя за изпращане на сървъра ново действие за промяна на избора на същество.
  6. Клиентът изпраща на сървъра мегапакет от над 400 промени на избора на същества (и с други действия: състояние на стрелба, ходене и т.н. също страдаха от този проблем).
  7. Сървърът получава 400 действия на входа. Тъй като не му е разрешено да пропусне нито едно действие на входа, той заповядва на всички клиенти да изпълнят тези действия и ги изпраща по мрежата.

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

Решихме този проблем, като поправихме всички гранични случаи при обновленията и поддръжката на опашките на забавяне. Въпреки че отне доста време, в крайна сметка си заслужаваше да реализираме всичко правилно, вместо да се ограничим до бързи решения.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster