
V máji tohto roka som sa zúčastnil ako hráč na . Všimol som si, že keď počet hráčov dosiahne určitý počet, čas od času sa časť z nich "odpojí". Našťastie pre vás (ale nie pre mňa) som bol jedným z tých hráčov, ktorí sa odpojili každýkrát, aj keď som mal dobré pripojenie. Vnímal som to ako osobnú výzvu a začal som hľadať príčiny problému. Po troch týždňoch ladenia, testovania a opráv bola chyba konečne odstránená, ale táto cesta nebola vôbec jednoduchá.
Problémy pri multiplayerových hrách sú veľmi ťažko sledovateľné. Zvyčajne sa vyskytujú za veľmi špecifických podmienok parametrov sietí a pri veľmi špecifických stavoch hry (v tomto prípade prítomnosti viac ako 200 hráčov). A aj keď sa podarí problém reprodukovať, nie je možné ho riadne debuggovať, pretože vloženie kontrolných bodov zastavuje hru, mätie časovače a zvyčajne vedie k ukončeniu pripojenia z dôvodu prekročenia časového limitu. Ale vďaka vytrvalosti a úžasnému nástroju s názvom sa mi podarilo zistiť, čo sa deje.
Aby som to zhrnul: kvôli chybe a neúplnej implementácii simulácie stavu oneskoreniami sa klient niekedy ocitol v situácii, keď musel za jeden krok odoslať sieťový paket, ktorý pozostával z akcií hráča výberu približne 400 herných entity (nazývame to "megapaket"). Po tom musí server nielen správne prijať všetky tieto akcie vstupu, ale aj ich odoslať všetkým ostatným klientom. Ak máte 200 klientov, to rýchlo predstavuje problém. Kanál k serveru sa rýchlo zaplní, čo vedie k strate paketov a kaskáde opätovne požadovaných paketov. Odloženie akcií vstupu potom vedie k tomu, že ešte viac klientov začína odosielať megapakety, a ich lavína sa stáva ešte silnejšou. Šťastným klientom sa podarí obnoviť, všetci ostatní "odpadávajú".

Problém bol dosť zásadný a oprava mi trvala 2 týždne. Je to dosť technické, takže nižšie vysvetlím niektoré technické detaily. Ale najskôr musíte vedieť, že od verzie 0.17.54, vydanej 4. júna, sa podmienky dočasných problémov s pripojením v multihrách stali stabilnejšími a skrývanie latencie oveľa menej glitchovalo (menej lagovania a teleportovania). Okrem toho som zmenil spôsob skrývania latencie v boji a dúfam, že vďaka tomu bude trošku plynulejšie.
Mnohopoužívateľský megapaket – technické podrobnosti
Jednoduché vysvetlenie, ako funguje multiplayer v hre: všetci klienti simulujú stav hry, prijímajú a posielajú iba vstupy hráča (nazývané „vstupné akcie“, Input Actions). Hlavná úloha servera je prenos Input Actions a kontrola toho, že všetci klienti vykonávajú rovnaké akcie v jednom takte. Viac o tom si môžete prečítať v príspevku .
Keďže server musí prijímať rozhodnutia o tom, aké akcie sa majú vykonávať, akcia hráča sa pohybuje asi takto: akcia hráča → herný klient → sieť → server → sieť → herný klient. To znamená, že každá akcia hráča sa vykoná iba po tom, ako prejde tam a späť cez sieť. Z toho dôvodu by sa hra zdala hrozne trhaná, a preto bol takmer okamžite po nástupe multiplayeru do hry zavedený mechanizmus skrývania latencie. Skrývanie latencie simuluje vstupy hráča bez ohľadu na akcie iných hráčov a rozhodovania servera.

Vo Factorio je herný stav Game State — to je kompletný stav mapy, hráča, entít a všetkého ostatného. Simuluje sa deterministicky vo všetkých klientoch na základe akcií prijatých od servera. Herný stav je svätý a ak sa niekedy začne odlišovať od servera alebo akéhokoľvek iného klienta, nastane desynchronizácia.
Okrem Game State máme stav latencie Latency State. Obsahuje malé podmnožstvo základného stavu. Latency State nie je svätý a jednoducho predstavuje obraz toho, ako bude vyzerať stav hry v budúcnosti na základe vstupu hráča. Input Actions.
Na to uchovávame kópiu vytváraných Input Actions v poradí latencií.

Teda na konci procesu na strane klienta vyzerá obrazec zhruba takto:
- Používame Input Actions všetkých hráčov na Game State tak, ako boli tieto vstupné akcie prijaté zo servera.
- Odstraňujeme z frontu oneskorení všetky Input Actions, ktoré, podľa údajov servera, už boli aplikované na Game State.
- Odstraňujeme Latency State a resetujeme ho tak, aby vyzeral presne tak ako Game State.
- Používame všetky akcie z frontu oneskorení na Latency State.
- Na základe údajov Game State a Latency State renderujeme hru hráčovi.
To všetko sa opakuje v každom takte.
Príliš zložité? Nenechávajte sa znepokojiť, to ešte nie je všetko. Aby sme vyrovnali neistoty internetových pripojení, vytvorili sme dva mechanizmy:
- Chýbajúce takty: keď server rozhodne, že Input Actions budú vykonané v hernom takte, tak ak nedostal Input Actions nejakého hráča (napríklad kvôli zvýšenej latencii), nebude čakať, ale oznámi tomuto klientovi „nezohľadnil som tvoje Input Actions, pokúsim sa ich pridať do nasledujúceho taktu“. Takto je to urobené, aby kvôli problémom s pripojením (alebo s počítačom) jedného hráča sa aktualizácia mapy nezpozdila pre všetkých ostatných. Je dôležité poznamenávať, že Input Actions nie sú ignorované, len sú odložené.
- Celková oneskorená doba cesty tam a späť: server sa snaží odhadnúť, aké je oneskorenie prenosu údajov tam a späť medzi klientom a serverom pre každého klienta. Každých 5 sekúnd diskutuje s klientom o novej latencii (v závislosti na správaní pripojenia v minulosti) a primerane zvyšuje alebo znižuje latenciu prenosu údajov tam a späť.
Samotné tieto mechanizmy sú dosť jednoduché, ale keď sa používajú spolu (čo sa často stáva pri problémoch s pripojením), logika kódu sa stáva ťažko ovládateľnou a má veľa okrajových prípadov. Okrem toho, keď sa tieto mechanizmy zapoja, server a front oneskorení musia správne aplikovať špeciálny Input Action s názvom StopMovementInTheNextTick. Vďaka tomu, pri problémoch s pripojením, sa postava nebude samovoľne pohybovať (napríklad pod vlak).
Teraz vám musíme vysvetliť, ako funguje výber entít. Jeden z prenášaných typov Input Action — je to zmena stavu výberu entity. Oznámi to všetkým, na ktorú entitu sa hráč zameral kurzorom myši. Ako možno pochopiť, je to jedna z najčastejších akcií vstupu, ktoré posielajú klienti, preto sme ju optimalizovali tak, aby zaberala čo najmenší objem pre šetrenie šírky pásma kanála. Implementované je to takto: pri výbere každej entity sa namiesto uloženia absolútnych, vysokopresných súradníc mapy uchovávajú nízkopešné relatívne posuny od predchádzajúceho výberu. Funguje to dobre, pretože výber myšou sa zvyčajne uskutočňuje veľmi blízko k predchádzajúcemu výberu. Z tohto dôvodu existujú dva dôležité požiadavky: Input Actions nikdy ich netreba vynechať a je potrebné ich vykonávať v správnom poradí. Tieto požiadavky sú splnené pre Game State. Ale pretože úloha Stav latencie je v tom, aby „vyzeral dostatočne dobre“ pre hráča, v stave latencií nie sú splnené. Latency State nerozmýšľa o , ktoré súvisia s preskakovaním takto a zmenami latencie prenosu tam a späť.
Už môžete hádať, kam to všetko smeruje. Konečne začíname vidieť príčiny problému megabalíka. Korene problému sú v tom, že pri rozhodovaní o tom, či poslať akciu zmeny výberu, logika výberu entít sa spolieha na Latency State, a tento stav neobsahuje vždy správne informácie. Preto sa megabalík generuje približne takto:
- Hráč má problémy s pripojením.
- Do hry vstupujú mechanizmy preskakovania taktov a regulácie latencie prenosu tam a späť.
- Rada stavu latencie nezohľadňuje tieto mechanizmy. To vedie k tomu, že niektoré akcie sú odstraňované predčasne alebo sa vykonávajú v nesprávnom poradí, čo vedie k nesprávnemu Latency State.
- Hráčovi zmizla problém s pripojením a aby dohnal server, simuluje až 400 taktov.
- V každom takte sa generuje a pripravuje na odoslanie na server nová akcia zmeny výberu entity.
- Klient posiela serveru megabalík zo 400 a viac zmien výberu entít (a s inými akciami: stav streľby, chôdze a pod. sa tiež stretávali s týmto problémom).
- Server prijíma 400 vstupných požiadaviek. Keďže mu nie je povolené vynechať ani jednu vstupnú požiadavku, prikáže všetkým klientom vykonať tieto požiadavky a odoslať ich cez sieť.
Ironií je, že mechanizmus navrhnutý na úsporu šírky pásma kanála nakoniec vytvoril obrovské sieťové paketové súbory.
Túto problém sme vyriešili opravou všetkých okrajových prípadov aktualizácie a podpory frontu oneskorení. Aj keď to trvalo dosť dlhú dobu, nakoniec stál za to implementovať všetko správne, a nie sa spoliehať na rýchle hacky.
Zdroj: habr.com
