През май тази година участвах като играч в
Проблемите в мултиплейър игрите са много трудни за проследяване. Те обикновено се случват при много специфични мрежови параметри и при много специфични състояния на играта (в този случай над 200 играча). И дори когато проблемът може да бъде възпроизведен, той не може да бъде правилно отстранен, защото вмъкването на точки на прекъсване спира играта, обърква таймерите и обикновено причинява изчакване на връзката поради изчакване. Но благодарение на постоянството и прекрасен инструмент, наречен
Накратко, поради грешка и непълно внедряване на симулацията на състоянието на закъснение, клиентът понякога се оказваше в ситуация, в която трябваше да изпрати мрежов пакет за един такт, състоящ се от въведени от играча действия за избиране на приблизително 400 обекта на играта ( ние го наричаме "мегапакет"). След това сървърът трябва не само да получи правилно всички тези входни действия, но и да ги изпрати на всички други клиенти. Ако имате 200 клиента, това бързо се превръща в проблем. Каналът към сървъра бързо се задръства, което води до изгубени пакети и каскада от повторно заявени пакети. След това отлагането на входните действия кара повече клиенти да започнат да изпращат мегапакети и тяхната лавина става още по-силна. Успешните клиенти успяват да се възстановят, всички останали падат.
Проблемът беше доста фундаментален и ми отне 2 седмици да го поправя. Доста е технически, така че ще обясня пикантните технически подробности по-долу. Но първо трябва да знаете, че след версия 0.17.54, пусната на 4 юни, в лицето на временни проблеми с връзката, мултиплейърът стана по-стабилен и забавянето на скриването е много по-малко бъгово (по-малко спиране и телепортиране). Освен това промених начина, по който се скриват закъсненията на битката и се надявам, че това ще ги направи малко по-плавни.
Мега пакет за мултиплейър - технически подробности
Казано по-просто, мултиплейърът в една игра работи по следния начин: всички клиенти симулират състоянието на играта, като получават и изпращат само информация от играча (наречена „действия за въвеждане“ Действия за въвеждане). Основната задача на сървъра е да прехвърля Действия за въвеждане и гарантиране, че всички клиенти извършват едни и същи действия в един и същи цикъл. Можете да прочетете повече за това в публикацията.
Тъй като сървърът трябва да взема решения какви действия да предприеме, действията на играча се движат по следния път: действие на играча -> клиент на игра -> мрежа -> сървър -> мрежа -> клиент на игра. Това означава, че всяко действие на играча се извършва само след като е направил обиколен път през мрежата. Поради това играта би изглеждала ужасно бавна, така че почти веднага след появата на мултиплейър в играта беше въведен механизъм за скриване на забавяния. Скриването на латентността симулира въвеждането на играч, без да отчита действията на другите играчи и вземането на решения от сървъра.
Factorio има състояние на играта състояние на играта е пълното състояние на картата, играча, обектите и всичко останало. Той се симулира детерминистично във всички клиенти въз основа на действия, получени от сървъра. Състоянието на играта е свещено и ако някога започне да се различава от сървъра или друг клиент, тогава настъпва десинхронизация.
с изключение на състояние на играта имаме състояние на забавяне Състояние на латентност. Той съдържа малко подмножество от основното състояние. Състояние на латентност не е свещено и просто представлява картина на това как ще изглежда състоянието на играта в бъдеще въз основа на входовете от играча Действия за въвеждане.
За целта запазваме копие на генерирания Действия за въвеждане в опашката за забавяне.
Тоест в края на процеса от страна на клиента картината изглежда така:
- Приложи Действия за въвеждане всички играчи да състояние на играта начина, по който тези входни действия са получени от сървъра.
- Премахнете всичко от опашката за забавяне Действия за въвеждане, които според сървъра вече са приложени към състояние на играта.
- Изтрий Състояние на латентност и го нулирайте, така че да изглежда точно както състояние на играта.
- Прилагане на всички действия от опашката за забавяне към Състояние на латентност.
- Въз основа на данни състояние на играта и Състояние на латентност рендирайте играта на играча.
Всичко това се повтаря във всеки такт.
Твърде трудно? Не се отпускайте, това не е всичко. За да компенсираме ненадеждните интернет връзки, създадохме два механизма:
- Пропуснати отметки: когато сървърът реши това Действия за въвеждане ще бъде изпълнен в такта на играта, тогава ако не е получил Действия за въвеждане някой играч (например поради увеличено забавяне), той няма да чака, а ще информира този клиент „Не взех предвид вашето Действия за въвеждане, ще се опитам да ги добавя в следващата лента. Това се прави, така че поради проблеми с връзката (или с компютъра) на един играч, актуализацията на картата да не се забави за всички останали. Заслужава да се отбележи, че Действия за въвеждане не се игнорират, а просто се отлагат.
- Пълно двупосочно забавяне: Сървърът се опитва да отгатне какво е двупосочното забавяне между клиент и сървър за всеки клиент. На всеки 5 секунди той договаря ново забавяне с клиента, ако е необходимо (в зависимост от това как връзката се е държала в миналото) и съответно увеличава или намалява забавянето на двупосочното пътуване.
Сами по себе си тези механизми са доста прости, но когато се използват заедно (което често се случва при проблеми с връзката), логиката на кода става трудна за управление и с много крайни случаи. В допълнение, когато тези механизми влязат в действие, сървърът и опашката за забавяне трябва правилно да внедрят специален Действие за въвеждане нарича StopMovementInTheNextTick. Благодарение на това, в случай на проблеми с връзката, героят няма да тича сам (например под влак).
Сега трябва да ви обясня как работи изборът на обект. Един от преминалите видове Действие за въвеждане е промяна в състоянието на избор на обект. Той казва на всеки над кой обект играчът е задържал курсора на мишката. Както можете да видите, това е едно от най-честите действия за въвеждане, изпращани от клиенти, така че за да спестим честотна лента, ние го оптимизирахме така, че да заема възможно най-малко място. Това се реализира по следния начин: когато всеки обект е избран, вместо да съхранява абсолютни координати на картата с висока точност, играта съхранява относително отместване с ниска точност спрямо предишния избор. Това работи добре, защото изборът с мишката обикновено се случва много близо до предишния избор. Това поражда две важни изисквания: Действия за въвеждане никога не трябва да се пропуска и трябва да се прави в правилния ред. Тези изисквания са изпълнени за състояние на играта. Но тъй като задачата състояние на латентност в "изглежда достатъчно добре" за играча, те не са доволни в състоянието на забавяне. Състояние на латентност не взема предвид
Вече можете да познаете накъде отива това. Най-накрая започваме да виждаме причините за проблема с мегапакетите. Коренът на проблема е, че логиката за избор на обект разчита на Състояние на латентности това състояние не винаги съдържа правилната информация. Така мегапакетът се генерира по следния начин:
- Плейърът има проблеми с връзката.
- Механизмите за пропускане на цикли и регулиране на забавянето на двупосочното предаване влизат в действие.
- Опашката със състояние на забавяне не отчита тези механизми. Това кара някои действия да бъдат премахнати преждевременно или да се изпълняват в грешен ред, което води до неправилен Състояние на латентност.
- Плейърът няма проблем с връзката и симулира до 400 цикъла, за да настигне сървъра.
- Във всеки цикъл се генерира ново действие и се подготвя за изпращане до сървъра, променяйки избора на обект.
- Клиентът изпраща мегапакет от 400+ промени в избора на обект към сървъра (и с други действия: състояние на задействане, състояние на ходене и т.н. също страдат от този проблем).
- Сървърът получава 400 входни действия. Тъй като не е разрешено да се пропусне нито едно действие за въвеждане, той инструктира всички клиенти да изпълнят тези действия и ги изпраща по мрежата.
Иронията е, че механизъм, предназначен да запазва честотната лента, доведе до огромни мрежови пакети.
Разрешихме този проблем, като коригирахме всички крайни случаи на актуализация и поддръжката на опашка за забавяне. Въпреки че отне доста време, струваше си да го оправим накрая, вместо да разчитаме на бързи хакове.
Източник: www.habr.com