
През май тази година участвах като играч в . Забелязах, че когато броят на играчите достигне определен брой, на всеки няколко минути част от тях "отпадат". За ваше щастие (но не и за мен), бях един от тези играчи, които се изключваха всеки път, дори с добро свързване. Възприех това като лично предизвикателство и започнах да търся причините за проблема. След три седмици отстраняване на проблеми, тестване и корекции, грешката най-накрая беше отстранена, но това пътуване не беше толкова просто.
Проблемите в многопользовательските игри са много трудни за проследяване. Обикновено те се появяват при много специфични условия на мрежовите параметри и при много специфични състояния на играта (в този случай — наличие на повече от 200 играчи). И даже когато успееш да възпроизведеш проблема, той не може да бъде отстранен правилно, защото вмъкването на контролни точки спира играта, обърква таймерите и обикновено води до прекратяване на свързването поради изтичане на времето за изчакване. Но благодарение на настойчивостта и страхотния инструмент на име , успях да разбера какво се случва.
Накратко: заради грешка и непълна реализация на симулацията на състоянието на забавянето, клиентът понякога се оказва в ситуация, в която трябва да изпрати мрежови пакет, съдържащ входните действия на играча за около 400 игрални същности (наричаме го "мегапакет"). След това сървърът не само трябва да получи правилно всички тези действия на входа, но и да ги изпрати на всички останали клиенти. Ако имаш 200 клиенти, това бързо се превръща в проблем. Каналът към сървъра бързо се запушва, което води до загуба на пакети и каскадни повторно поискани пакети. Отлагането на действията на входа след това води до това, че още повече клиенти започват да изпращат мегапакети, а лавината им става още по-силна. Късметлиите успяват да се възстановят, всички останали "отпадат".

Проблемата беше доста основна и ми отне 2 седмици, за да я отстраню. Тя е доста техническа, така че по-долу ще обясня детайлите. Но първо трябва да знаете, че от версия 0.17.54, пусната на 4 юни, при времеви проблеми с връзката многопотребителският режим стана по-стабилен, а скриването на закъсненията — много по-малко проблематично (по-малко забавяне и телепортиране). Освен това промених начина на скриване на закъсненията в битка и се надявам, че благодарение на това те ще бъдат малко по-плавни.
Многопотребителски мегапакет — технически подробности
Ако обясня по-просто, многопотребителският режим в играта работи по следния начин: всички клиенти симулират състоянието на играта, като получават и изпращат само входа на играча (наречен „входни действия“, Input Actions). Основната задача на сървъра е да предава Input Actions и да контролира това, което всички клиенти извършват по едно и също време. По-подробно за това можете да прочетете в поста .
Тъй като сървърът трябва да взема решения относно това, кои действия да се извършат, действията на играча преминават приблизително по следния път: действие на играча -> клиент на играта -> мрежа -> сървър -> мрежа -> клиент на играта. Това означава, че всяко действие на играча се изпълнява само след като премине пътя натам и обратно през мрежата. Поради това играта би изглеждала ужасно забавена, така че почти веднага след появата на многопотребителския режим е въведен механизъм за скриване на закъсненията. Скриването на закъсненията имитира входа на играча без да се вземат предвид действията на другите играчи и решенията на сървъра.

В Factorio има игрово състояние Game State — това е пълното състояние на картата, играча, съществата и всичко останало. То се симулира детерминистично на всички клиенти на базата на действията, получени от сървъра. Игровото състояние е свещено и ако то някога започне да се различава от сървъра или който и да е друг клиент, възниква разсинхронизация.
Освен това Game State имаме състояние на закъснения Latency State. То съдържа малка подмножество от основното състояние. Latency State не е свещено и просто представя картината на това как ще изглежда състоянието на играта в бъдеще на базата на въведеното от играча Input Actions.
За това съхраняваме копие на създадените Input Actions в опашката за закъснения.

Тоест в края на процеса, от страна на клиента, картината изглежда приблизително така:
- Прилагаме Input Actions всички играчи към Game State така, както са получени тези входни действия от сървъра.
- Премахваме от опашката на забавянията всички Input Actions, които, според данните на сървъра, вече са били приложени към Game State.
- Премахваме Latency State и го нулираме, за да изглежда точно така, както и Game State.
- Прилагаме всички действия от опашката на забавянията към Latency State.
- На база на данните Game State и Latency State рендерим играта на играча.
Всичко това се повтаря в всеки такт.
Твърде сложно? Не се отпускайте, това все още не е всичко. За да се компенсира ненадеждността на интернет връзките, сме създали два механизма:
- Пропуснати тактове: когато сървърът решава, че Input Actions ще бъдат изпълнени в игровия такт, то ако не е получил Input Actions някой играч (например, заради увеличена закъснение), той няма да чака, а ще уведоми този клиент: "не взех предвид твоите Input Actions, ще опитам да ги добавя в следващия такт." Това е направено, за да не се забавя обновяването на картата за всички останали, заради проблеми с връзката (или компьютъра) на един играч. Струва си да се отбележи, че Input Actions не се игнорират, а просто се отлагат.
- Закъснение на целия път напред-назад: сървърът се опитва да предположи какво е закъснението за предаване на данни напред-назад между клиента и сървъра за всеки клиент. На всеки 5 секунди, при необходимост, той обсъжда с клиента ново закъснение (в зависимост от поведението на връзката в миналото) и съответно увеличава или намалява закъснението за предаване на данни напред-назад.
Тези механизми сами по себе си са сравнително простички, но когато се използват заедно (което често се случва при проблеми с връзката), логиката на кода става трудна за управление и с много гранични случаи. Освен това, когато тези механизми влязат в действие, сървърът и опашката на забавянията трябва правилно да внедрят специалното Input Action с името StopMovementInTheNextTick. Благодарение на това, при проблеми с връзката, персонажът няма да тича сам по себе си (например, под влак).
Сега трябва да ви обясня как работи избора на същности. Един от предаваните типове Input Action — това е промяна в състоянието на избора на същност. То информира всички, върху коя същност играчът е насочил курсора на мишката. Както може да се разбере, това е едно от най-честите действия, изпращани от клиентите, затова за икономия на пропускателната способност на канала сме го оптимизирали така, че да заема колкото се може по-малко място. Това е реализирано по следния начин: при избор на всяка същност, вместо да се запазват абсолютни, прецизни координати на картата, играта запазва неточна относителна стойност спрямо предишния избор. Това работи добре, тъй като изборът с мишката обикновено се извършва много близо до предишния избор. Поради това възникват две важни изисквания: Input Actions никога не трябва да се пропускат и трябва да се изпълняват в правилния ред. Тези изисквания са изпълнени за Game State. Но тъй като задачата Latency state е да „изглежда достатъчно добре“ за играча, в състояние на латентност те не се удовлетворяват. Latency State не взема предвид , свързани с пропуски в тактовете и промяна на латентността на предаване напред и назад.
Вие вече може да се досетите накъде води всичко това. Накрая започваме да виждаме причините за проблема с мегапакета. Коренът на проблема е, че при взимането на решение дали да се предаде действието за промяна на избора, логиката за избора на същности разчита на Latency State, а това състояние не винаги съдържа вярна информация. Затова мегапакетът се генерира по следния начин:
- Играчът има проблеми с връзката.
- Включват се механизми за пропускане на тактове и регулиране на латентността на предаване напред и назад.
- Опашката на състоянието на латентността не отчита тези механизми. Това води до факта, че някои действия се премахват преждевременно или се изпълняват в неправилен ред, което води до неправилно Latency State.
- Играчът вече няма проблем с връзката и за да настигне сървъра, симулира до 400 такта.
- На всеки такт се генерира и подготвя за изпращане на сървъра ново действие за промяна на избора на същност.
- Клиентът изпраща на сървъра мегапакет от над 400 промени на избора на същности (и с други действия: състояние на стрелба, ходене и т.н., също страдаха от този проблем).
- Сървърът получава 400 входни действия. Тъй като му е забранено да пропуска каквото и да е входно действие, той нарежда на всички клиенти да извършат тези действия и да ги изпратят по мрежата.
Иронията е, че механизмът, предназначен да спести пропусквателна способност на канала, всъщност създава огромни мрежови пакети.
Решихме този проблем, като коригирахме всички гранични случаи на актуализация и поддръжка на опашките на забавяне. Въпреки че отне доста време, в крайна сметка беше важно да се реализира всичко по правилен начин, вместо да разчитаме на бързи решения.
Източник: habr.com
