
În luna mai a acestui an am participat ca jucător la . Am observat că, atunci când numărul jucătorilor atinge o anumită limită, la fiecare câteva minute o parte dintre ei „cad”. Din fericire pentru voi (dar nu pentru mine), am fost unul dintre acei jucători care s-au deconectat de fiecare dată, chiar și cu o conexiune bună. Am perceput acest lucru ca pe o provocare personală și am început să caut cauzele problemei. După trei săptămâni de depanare, testare și soluționări, eroarea a fost în sfârșit remediată, dar această călătorie nu a fost deloc simplă.
Problemele multiplayer sunt foarte dificile de urmărit. De obicei, ele apar în condiții foarte specifice ale rețelei și în stări foarte precise ale jocului (în acest caz - cu mai mult de 200 de jucători). Chiar și atunci când reușești să reproduci problema, nu poți să o depanezi corect, deoarece inserarea punctelor de control oprește jocul, derutează temporizatoarele și de obicei duce la încheierea conexiunii din cauza expirării timpului de așteptare. Dar datorită perseverenței și unui instrument minunat numit , am reușit să aflu ce se întâmplă.
Pe scurt: din cauza unei erori și a implementării incomplete a simulării stării de întârziere, clientul se găsea uneori într-o situație în care trebuia să trimită un pachet de rețea format din acțiunile de input ale jucătorului pentru aproximativ 400 de entități de joc (numim aceasta „mega pachet”). După aceasta, serverul trebuie nu doar să primească corect toate aceste acțiuni de input, ci și să le trimită tuturor celorlalți clienți. Dacă ai 200 de clienți, devine rapid o problemă. Canalul către server se blochează repede, ceea ce duce la pierderea pachetelor și la un val de pachete solicitate din nou. Întârzierea acțiunilor de input duce apoi la trimiterea de mega pachete de către și mai mulți clienți, iar avalanșa se intensifică. Clienții norocoși reușesc să se recupereze, toți ceilalți „cad”.

Problema a fost destul de fundamentală și mi-a luat 2 săptămâni pentru a o rezolva. Este destul de tehnică, așa că mai jos voi explica în detaliu. Dar pentru început, trebuie să știți că, începând cu versiunea 0.17.54, lansată pe 4 iunie, în condițiile de probleme temporare de conectivitate, multiplayerul a devenit mai stabil, iar ascunderea latențelor mult mai puțin glitchy (mai puține întârzieri și teleportări). De asemenea, am schimbat modul în care se ascund latențele în luptă și sper că, grație acestui lucru, vor fi ceva mai fluente.
Detalii tehnice despre mega-pachetul multiplayer
Dacă simplificăm explicația, multiplayerul din joc funcționează astfel: toate clienții simulează starea jocului, primind și trimițând doar inputul jucătorului (numit „acțiuni de input”, Input Actions). Sarcina principală a serverului este transmiterea Input Actions și controlul faptului că toți clienții efectuează aceleași acțiuni într-un singur ciclu. Pentru mai multe detalii, puteți citi în postarea .
Deoarece serverul trebuie să ia decizii cu privire la ce acțiuni trebuie efectuate, acțiunile jucătorului urmează aproximativ acest traseu: acțiunea jucătorului -> clientul jocului -> rețea -> server -> rețea -> clientul jocului. Aceasta înseamnă că fiecare acțiune a jucătorului este executată doar după ce a parcurs drumul înapoi prin rețea. Din cauza acestui lucru, jocul ar părea extrem de întârziat, așa că aproape imediat după introducerea multiplayer-ului în joc a fost implementat un mecanism de ascundere a latențelor. Ascunderea latențelor imită inputul jucătorului fără a lua în considerare acțiunile altor jucători și deciziile serverului.

În Factorio există un stat al jocului Game State — acesta este starea completă a hărții, jucătorului, entităților și tot ce este altceva. Este determinist simulat în toți clienții pe baza acțiunilor primite de la server. Starea jocului este sacră, și dacă aceasta începe vreodată să difere de server sau de orice alt client, apare o desincronizare.
În plus, Game State avem un stat al latențelor Latency State. Acesta conține un subset mic al stării principale. Latency State nu este sacru și reprezintă pur și simplu o imagine a felului în care va arăta starea jocului în viitor pe baza inputului jucătorului. Input Actions.
Pentru aceasta, păstrăm o copie a latențelor create Input Actions în coada de latențe.

Adică, la finalul procesului, pe partea clientului, imaginea arată aproximativ așa:
- Aplicăm Input Actions toți jucătorii la Game State astfel încât aceste acțiuni de introducere au fost primite de la server.
- Eliminăm din coada întârzierilor toate Input Actions, care, conform datelor serverului, au fost deja aplicate la Game State.
- Ștergem Latency State și îl resetăm, astfel încât să arate exact așa cum Game State.
- Aplicăm toate acțiunile din coada întârzierilor la Latency State.
- Pe baza datelor Game State și Latency State redăm jocul jucătorului.
Toate acestea se repetă în fiecare ciclu.
Prea complicat? Nu vă relaxați, nu este totul. Pentru a compensa fiabilitatea conexiunilor la Internet, am creat două mecanisme:
- Cicluri ratate: când serverul decide că Input Actions vor fi executate în ciclu de joc, dacă nu a primit Input Actions de la vreun jucător (de exemplu, din cauza unei întârzieri crescute), nu va aștepta, ci va informa acel client «nu ți-am luat în considerare Input Actions, voi încerca să le adaug în următorul ciclu». A fost conceput astfel încât, din cauza problemelor de conexiune (sau ale computerului) ale unui jucător, actualizarea hărții să nu fie întârziată pentru toți ceilalți. Merită menționat că Input Actions nu sunt ignorate, ci pur și simplu amânate.
- Întârzierea totală pe calea de întoarcere: serverul încearcă să presupună care este întârzierea transmiterii datelor de la client la server și înapoi pentru fiecare client. La fiecare 5 secunde, el discută cu clientul o nouă întârziere (în funcție de comportamentul conexiunii din trecut) și, în consecință, crește sau scade întârzierea transmiterii datelor de la client la server și înapoi.
Aceste mecanisme în sine sunt destul de simple, dar atunci când sunt utilizate împreună (ceea ce se întâmplă adesea în condiții de probleme de conexiune), logica codului devine dificil de gestionat și cu multe cazuri limită. În plus, atunci când aceste mecanisme sunt implicate, serverul și coada întârzierilor trebuie să implementeze corect o Acțiune de intrare called StopMovementInTheNextTick. Datorită acestui fapt, în condiții de probleme de conexiune, personajul nu va alerga de unul singur (de exemplu, sub tren).
Acum trebuie să vă explic cum funcționează selecția entităților. Unul dintre tipurile transmise Acțiune de intrare — aceasta este schimbarea stării de selecție a entității. Aceasta le comunică tuturor ce entitate a fost evidențiată cu cursorul de către jucător. Așa cum se poate înțelege, este una dintre cele mai frecvente acțiuni de input trimise de clienți, de aceea, pentru a economisi lățimea de bandă a canalului, am optimizat-o astfel încât să ocupe cât mai puțin loc. Aceasta a fost realizată astfel: la selectarea fiecărei entități, în loc să salvăm coordonatele absolute, de mare precizie ale hărții, jocul salvează un offset relativ de mică precizie față de selecția anterioară. Aceasta funcționează bine deoarece selectarea cu mouse-ul are loc de obicei foarte aproape de selecția anterioară. Din acest motiv, există două cerințe importante: Input Actions nu trebuie ocolite și trebuie îndeplinite în ordinea corectă. Aceste cerințe sunt satisfăcute pentru Game State. Dar, deoarece sarcina Starea de latență este de a „arăta suficient de bine” pentru jucător, în starea de latență acestea nu sunt satisfăcute. Latency State nu ia în considerare , legate de ocolirea ciclică și de modificarea latenței de transfer bidirecțional.
Probabil că deja vă puteți da seama încotro ne îndreptăm. În sfârșit, începem să vedem cauzele problemei megapugetului. Rădăcina problemei constă în faptul că în luarea deciziei de a transmite acțiunea de modificare a selecției, logica selecției entității se bazează pe Latency State, iar această stare nu conține întotdeauna informații corecte. Prin urmare, megapugetul este generat în aproximativ următorul mod:
- Jucătorul are probleme cu conexiunea.
- Intră în acțiune mecanismele de ocolire ciclică și de gestionare a latenței de transfer bidirecțional.
- Coada stării de latență nu ia în considerare aceste mecanisme. Acest lucru duce la faptul că unele acțiuni sunt eliminate prematur sau sunt efectuate în ordinea greșită, ceea ce duce la o Latency State.
- Jucătorul își recuperează conexiunea și, pentru a ajunge din urmă serverul, simulează până la 400 de cicluri.
- În fiecare ciclu se generează și se pregătește pentru trimitere către server o nouă acțiune de modificare a selecției entității.
- Clientul trimite serverului un megapuget de peste 400 de modificări ale selecției entităților (iar alte acțiuni, precum starea de împușcare, mers etc., au fost de asemenea afectate de această problemă).
- Serverul primește 400 de acțiuni de intrare. Deoarece nu i se permite să sară peste nicio acțiune de intrare, acesta ordonă tuturor clienților să execute aceste acțiuni și să le trimită prin rețea.
Ironia constă în faptul că mecanismul destinat economisirii lățimii de bandă a canalului, în cele din urmă, genera pachete de rețea enorme.
Am rezolvat această problemă corectând toate cazurile limită de actualizare și sprijin pentru coada de întârzieri. Deși a durat destul de mult, într-un final a meritat să implementăm totul corect, în loc să ne bazăm pe soluții rapide.
Sursa: habr.com
