
A maggio di quest'anno ho partecipato come giocatore a . Ho notato che quando il numero di giocatori raggiunge un certo numero, ogni pochi minuti parte di essi "scompare". Fortunatamente per voi (ma non per me), ero uno di quei giocatori che si disconnettevano ogni volta, anche con una buona connessione. L'ho considerato una sfida personale e ho iniziato a cercare le cause del problema. Dopo tre settimane di debug, test e correzioni, l'errore è finalmente stato risolto, ma questo viaggio non è stato affatto semplice.
Le problematiche dei giochi multiplayer sono molto difficili da tracciare. Di solito si verificano in condizioni molto specifiche riguardo ai parametri delle reti e a stati di gioco molto particolari (in questo caso, la presenza di oltre 200 giocatori). E anche quando riesci a riprodurre il problema, non è possibile fare un debug adeguato, perché l'inserimento di punti di controllo ferma il gioco, confonde i timer e di solito porta alla chiusura della connessione a causa del timeout. Ma grazie alla perseveranza e a un eccellente strumento chiamato , sono riuscito a scoprire cosa stava succedendo.
In breve: a causa di un errore e della realizzazione incompleta della simulazione dello stato di ritardo, il client si trovava a volte nella situazione di dover inviare in un solo ciclo un pacchetto di rete composto dalle azioni di input di circa 400 entità di gioco (noi lo chiamiamo "megapacchetto"). Dopodiché, il server non solo deve ricevere correttamente tutte queste azioni di input, ma deve anche inviarle a tutti gli altri client. Se hai 200 client, questo diventa rapidamente un problema. Il canale verso il server si riempie rapidamente, il che porta alla perdita di pacchetti e a un effetto a cascata di pacchetti richiesti nuovamente. Il ritardo nelle azioni di input porta poi a far sì che ancora più client inizino a inviare megapacchetti, e la loro valanga diventa ancora più forte. Ai client fortunati riesce di riprendersi, mentre tutti gli altri "scompaiono".

Il problema era abbastanza fondamentale e mi ci sono volute 2 settimane per risolverlo. È piuttosto tecnico, quindi di seguito spiegherò i dettagli tecnici succosi. Ma per cominciare, è necessario sapere che dalla versione 0.17.54, rilasciata il 4 giugno, a causa di problemi temporanei di connessione, il multiplayer è diventato molto più stabile e la mascheratura dei ritardi è molto meno glitchata (meno lag e teletrasporti). Inoltre, ho modificato il modo di mascherare i ritardi in combattimento e spero che, grazie a questo, siano un po' più fluidi.
Mega pacchetto multiplayer - dettagli tecnici
Se lo si spiega in modo semplificato, il multiplayer nel gioco funziona nel seguente modo: tutti i client simulano lo stato del gioco, ricevendo e inviando solo l'input del giocatore (chiamato "azioni di input", Input Actions). L'obiettivo principale del server è trasmettere Input Actions e controllare che tutti i client eseguano le stesse azioni in un dato ciclo. Puoi leggere di più in questo post .
Poiché il server deve prendere decisioni su quali azioni eseguire, le azioni del giocatore passano circa per questo percorso: azione del giocatore -> client di gioco -> rete -> server -> rete -> client di gioco. Questo significa che ogni azione del giocatore viene eseguita solo dopo aver completato un viaggio di andata e ritorno attraverso la rete. A causa di ciò, il gioco apparirebbe incredibilmente laggoso, dunque praticamente subito dopo l'introduzione del multiplayer nel gioco fu introdotto un meccanismo di mascheratura dei ritardi. La mascheratura dei ritardi simula l'input del giocatore senza considerare le azioni degli altri giocatori e le decisioni del server.

In Factorio c'è uno stato di gioco Game State che rappresenta lo stato completo della mappa, del giocatore, delle entità e di tutto il resto. Questo è simulato in modo deterministico in tutti i client sulla base delle azioni ricevute dal server. Lo stato di gioco è sacro e se mai inizia a differire da quello del server o di qualsiasi altro client, si verifica una desincronizzazione.
Inoltre Game State abbiamo uno stato di ritardo Latency State. Questo contiene un piccolo sottoinsieme dello stato principale. Latency State non è sacro e rappresenta semplicemente un'immagine di come sarà lo stato di gioco in futuro in base all'input dato dal giocatore. Input Actions.
Per fare ciò, conserviamo una copia delle azioni di input Input Actions in coda dei ritardi.

Cioè, alla fine del processo, sul lato client, l'immagine appare più o meno così:
- Applichiamo Input Actions tutti i giocatori a Game State come se queste azioni di input fossero state ricevute dal server.
- Rimuoviamo dalla coda dei ritardi tutti Input Actions, che, secondo il server, sono già stati applicati a Game State.
- Rimuoviamo Latency State e lo ripristiniamo in modo che appaia esattamente come Game State.
- Applichiamo tutte le azioni dalla coda dei ritardi a Latency State.
- Sulla base dei dati Game State e Latency State renderizziamo il gioco per il giocatore.
Tutto ciò si ripete in ogni ciclo.
Troppo complicato? Non rilassatevi, non è tutto. Per compensare l'instabilità delle connessioni Internet, abbiamo creato due meccanismi:
- Cicli saltati: quando il server decide che Input Actions saranno eseguiti nel ciclo di gioco, se non ha ricevuto Input Actions alcun giocatore (ad esempio, a causa di un ritardo aumentato), non aspetterà, ma notificherà a quel client «non ho considerato i tuoi Input Actions, cercherò di aggiungerli al ciclo successivo». Questo è stato fatto affinché, a causa dei problemi di connessione (o del computer) di un giocatore, l'aggiornamento della mappa non rallenti per tutti gli altri. Va notato che Input Actions non vengono ignorati, ma semplicemente rimandati.
- Ritardo del percorso completo andata e ritorno: il server cerca di ipotizzare quale sia il ritardo nella trasmissione dei dati andata e ritorno tra client e server per ogni client. Ogni 5 secondi, se necessario, discute con il client un nuovo ritardo (a seconda di come si è comportata la connessione in passato) e aumenta o diminuisce di conseguenza il ritardo della trasmissione dei dati andata e ritorno.
Questi meccanismi sono piuttosto semplici in sé, ma quando sono utilizzati insieme (cosa che accade spesso in caso di problemi di connessione), la logica del codice diventa difficile da gestire e piena di casi limite. Inoltre, quando entrano in gioco questi meccanismi, il server e la coda dei ritardi devono implementare correttamente il particolare Input Action denominato StopMovementInTheNextTick. Grazie a questo, in caso di problemi di connessione, il personaggio non correrà da solo (ad esempio, sotto un treno).
Ora è necessario spiegarvi come funziona la selezione delle entità. Uno dei tipi trasmessi Input Action — è un cambiamento nello stato della selezione dell'entità. Comunica a tutti quale entità il giocatore ha puntato con il cursore del mouse. Come si può capire, si tratta di una delle azioni di input più comuni inviate dai client, quindi per risparmiare larghezza di banda abbiamo ottimizzato il processo affinché occupi il minor spazio possibile. È implementato in questo modo: durante la selezione di ogni entità, invece di memorizzare coordinate assolute e ad alta precisione della mappa, il gioco memorizza uno spostamento relativo a bassa precisione dalla selezione precedente. Questo funziona bene perché la selezione col mouse avviene di solito molto vicino alla selezione precedente. Di conseguenza, ci sono due requisiti importanti: Input Actions non devono mai essere saltati e devono essere eseguiti nell'ordine corretto. Questi requisiti sono soddisfatti per Game State. Ma poiché il compito Latency state è quello di "sembrare abbastanza buono" per il giocatore, in stato di latenza non vengono soddisfatti. Latency State non tiene conto di , legati a salti di cicli e alle variazioni di latenza della trasmissione andata e ritorno.
Puoi già indovinare dove stiamo andando. Finalmente cominciamo a vedere le cause del problema del mega pacchetto. La radice del problema sta nel fatto che, nel prendere decisioni su se trasmettere o meno l'azione di cambiamento della selezione, la logica della selezione delle entità si basa su Latency State, e questo stato non contiene sempre informazioni corrette. Pertanto, il mega pacchetto viene generato più o meno in questo modo:
- Il giocatore ha avuto problemi di connessione.
- Entrano in gioco meccanismi di salto dei cicli e regolazione della latenza di trasmissione andata e ritorno.
- La coda dello stato di latenza non tiene conto di questi meccanismi. Ciò porta a una cancellazione prematura di alcune azioni o all'esecuzione in un ordine errato, causando un comportamento anomalo. Latency State.
- Il giocatore perde il problema di connessione e, per sincronizzarsi con il server, simula fino a 400 cicli.
- Ad ogni ciclo, viene generata e preparata per l'invio al server una nuova azione di cambiamento della selezione dell'entità.
- Il client invia al server un mega pacchetto di oltre 400 cambiamenti nella selezione delle entità (e altre azioni: lo stato di sparo, di movimento, ecc. hanno subito anch'essi questo problema).
- Il server riceve 400 azioni di input. Poiché non gli è consentito saltare nemmeno un'azione di input, ordina a tutti i client di eseguire queste azioni e le invia attraverso la rete.
L'ironia è che il meccanismo progettato per risparmiare larghezza di banda del canale ha finito per creare enormi pacchetti di rete.
Abbiamo risolto questo problema correggendo tutti i casi limite per l'aggiornamento e il supporto della coda di ritardo. Anche se ci è voluto abbastanza tempo, alla fine ne è valsa la pena implementare tutto correttamente, piuttosto che fare affidamento su soluzioni veloci.
Fonte: habr.com
