
I maj var jeg med som spiller i . Jeg bemærkede, at når antallet af spillere nåede et bestemt antal, så "forlod" en del af dem efter et par minutter. Heldigvis for dig (men ikke for mig) var jeg en af de spillere, der disconnectede hver gang, selv med en god forbindelse. Jeg opfattede dette som en personlig udfordring og begyndte at søge efter årsagerne til problemet. Efter tre ugers fejlfinding, testning og reparationer var fejlen endelig løst, men denne rejse var ikke så let som man skulle tro.
Problemer i multiplayer-spil er meget svære at spore. De opstår ofte under meget specifikke forhold af netværksparametre og i meget bestemte spilstater (i dette tilfælde - når der er mere end 200 spillere). Og selv når man formår at reproducere problemet, er det umuligt at debugge korrekt, fordi indsættelse af kontrolpunkter stopper spillet, forvirrer timere og normalt fører til, at forbindelsen afsluttes på grund af timeout. Men takket være vedholdenhed og et fantastisk værktøj kaldet , lykkedes det mig at finde ud af, hvad der foregik.
Kort sagt: på grund af en fejl og en ufuldstændig implementering af simuleringen af forsinkelsesstatus, befandt klienten nogle gange sig i en situation, hvor den for én cyklus skulle sende et netværkspakke bestående af spillerens valg af omkring 400 spil-entiteter (vi kalder det "megapakken"). Efter dette skal serveren ikke kun korrekt modtage alle disse inputhandlinger, men også sende dem til alle andre klienter. Hvis du har 200 klienter, bliver det hurtigt et problem. Kanalen til serveren bliver hurtigt overfyldt, hvilket fører til pakke-tab og et kaskade af genanmodede pakker. Forsinkelse af inputhandlinger fører så til, at endnu flere klienter begynder at sende megapakker, og deres lavine bliver endnu stærkere. De heldige klienter formår at komme sig, alle andre "forlader".

Problemet var ret grundlæggende, og jeg brugte 2 uger på at løse det. Det er ret teknisk, så nedenfor vil jeg forklare nogle saftige tekniske detaljer. Men først skal du vide, at siden version 0.17.54, der blev udgivet den 4. juni, er multiplayer blevet mere stabil under tidsproblemer med forbindelsen, og latensskjultningen er blevet meget mindre fejlbehæftet (mindst forsinkelse og teleportering). Desuden ændrede jeg måden at skjule latensen i kamp, og jeg håber, at det vil gøre dem lidt mere glatte.
Multiplayer Megapakke — tekniske detaljer
For at forklare det simpelt fungerer multiplayer i spillet sådan her: alle klienter simulerer spillets tilstand ved kun at modtage og sende spillerens input (kaldet 'input handlinger', Input Actions). Hovedopgaven for serveren er at transmittere Input Actions og kontrollere, at alle klienter udfører de samme handlinger i en given runde. Du kan læse mere om dette i indlægget .
Da serveren skal træffe beslutninger om, hvilke handlinger der skal udføres, følger spillerens handlinger omtrent denne rute: spillerens handling -> spilklient -> netværk -> server -> netværk -> spilklient. Dette betyder, at hver spillerhandling kun udføres efter, at den har gjort turen frem og tilbage gennem netværket. På grund af dette ville spillet føles frygteligt langsomt, så næsten straks efter introduktionen af multiplayer i spillet blev der indført en mekanisme til skjuling af latenser. Latenserens skjulning simulerer spillerens input uden at tage højde for handlingerne fra andre spillere og serverens beslutninger.

I Factorio er der en spiltilstand Game State — det er den fulde tilstand af kortet, spilleren, entiteterne og alt andet. Det simuleres deterministisk på alle klienter baseret på de modtagne handlinger fra serveren. Spiltilstanden er hellig, og hvis den nogensinde begynder at afvige fra serveren eller en hvilken som helst anden klient, opstår der en desynkronisering.
Udover Game State Vi har en latensstatus Latency State. Den indeholder en lille delmængde af den primære tilstand. Latency State Den er ikke hellig og repræsenterer blot et billede af, hvordan spiltilstanden vil se ud i fremtiden baseret på input givet af spilleren. Input Actions.
For dette formål gemmer vi en kopi af de oprettede Input Actions i latenqueuen.

Det vil sige, at billedet på klientens side ser omtrent sådan ud i slutningen af processen:
- Vi anvender Input Actions alle spillerne til Game State sådan som disse inputhandlinger blev modtaget fra serveren.
- Vi fjerner alle Input Actions, som ifølge serveren allerede er blevet anvendt på Game State.
- Fjerne Latency State og nulstiller det, så det ser nøjagtigt ud som Game State.
- Vi anvender alle handlinger fra forsinkelsesqueue'en til Latency State.
- Baseret på dataene Game State og Latency State renderer vi spillet for spilleren.
Alt dette gentages i hver takt.
For komplekst? Slap ikke af, det er ikke alt. For at kompensere for den ustabilitet, der kan være ved internetforbindelser, har vi udviklet to mekanismer:
- Udsatte takter: når serveren beslutter, at Input Actions vil finde sted i spillets takt, så hvis den ikke har modtaget Input Actions fra en spiller (for eksempel på grund af øget forsinkelse), vil den ikke vente, men informere denne klient om "jeg har ikke taget dine Input Actions, jeg vil forsøge at tilføje dem i næste takt." Dette er gjort for at sikre, at kortopdateringen ikke forsinkes for alle andre på grund af forbindelsesproblemer (eller computerproblemer) for en enkelt spiller. Det er værd at bemærke, at Input Actions ikke ignoreres, men blot udsættes.
- Den samlede tur-retur-forsinkelse: serveren forsøger at forudsige, hvad datatransmissionens tur-retur-forsinkelse er mellem klienten og serveren for hver klient. Hver 5. sekund diskuterer den nødvendigvis med klienten en ny forsinkelse (afhængig af, hvordan forbindelsen har opført sig tidligere), og justerer tur-retur-forsinkelsen derefter.
Disse mekanismer er i sig selv ret enkle, men når de anvendes sammen (hvilket ofte sker ved forbindelsesproblemer), bliver kode-logikken svær at håndtere og fyldt med en masse kanttilfælde. Derudover skal serveren og forsinkelsesqueue'en, når disse mekanismer træder i kraft, korrekt implementere den særlige Input Action med navnet StopMovementInTheNextTick. Dermed vil karakteren ikke løbe af sig selv ved forbindelsesproblemer (for eksempel ind under et tog).
Nu skal jeg forklare, hvordan valg af entiteter fungerer. En af de overførte typer Input Action — dette er ændringen af tilstand for valg af entitet. Den informerer alle om, hvilken entitet spilleren har peget musen på. Som man kan forstå, er dette en af de mest hyppige inputhandlinger, der sendes af klienterne, så for at spare båndbredde har vi optimeret det, så det fylder så lidt som muligt. Dette er implementeret ved, at ved valg af hver entitet, gemmes der ikke absolutte, højpræcise kortkoordinater, men der gemmes en lavpræcisions relativ forskydning fra den tidligere valg. Dette fungerer godt, fordi markering med musen typisk sker meget tæt på det tidligere valg. Derfor er der to vigtige krav: Input Actions de må aldrig springes over, og de skal udføres i den rigtige rækkefølge. Disse krav opfyldes for Game State. Men da opgaven Latency state handler om at "se godt nok ud" for spilleren, opfyldes de ikke i tilstande med forsinkelse. Latency State tager ikke højde for , der er relateret til at springe takter over og ændringer i forsinkelse ved tovejskommunikation.
Du kan allerede gætte, hvor dette fører hen. Til sidst begynder vi at se årsagerne til megapak-problemet. Problemet ligger i, at beslutningen om, hvorvidt handlingen for ændring af valg skal sendes, logikken for valg af entiteter er afhængig af Latency State, og denne tilstand indeholder ikke altid korrekt information. Derfor genereres megapakken på følgende måde:
- Spilleren har problemer med forbindelsen.
- Der træder mekanismer for at springe takter over og regulere forsinkelse ved tovejskommunikation ind.
- Køen af tilstande med forsinkelse tager ikke højde for disse mekanismer. Dette fører til, at nogle handlinger fjernes for tidligt eller udføres i forkert rækkefølge, hvilket resulterer i en forkert Latency State.
- Spilleren får forbindelsesproblemet løst, og han simulerer op til 400 takter for at indhente serveren.
- I hver takt genereres og forberedes en ny handling for ændring af valg af entitet til at blive sendt til serveren.
- Klienten sender til serveren en megapakke med mere end 400 ændringer af valg af entiteter (og andre handlinger: tilstand af skydning, gang osv. led også under dette problem).
- Serveren modtager 400 indtastningshandlinger. Da det ikke er tilladt at springe nogen indtastningshandlinger over, beder den alle klienter om at udføre disse handlinger og sende dem over netværket.
Ironien er, at mekanismen, der skulle spare båndbredde, i virkeligheden skabte enorme netværkspakker.
Vi løste dette problem ved at rette alle grænsetilfælde for opdatering og støtte af køer med forsinkelser. Selvom det tog en del tid, var det i sidste ende værd at få det hele gjort korrekt i stedet for at stole på hurtige hacks.
Kilde: habr.com
