Informazioni sul modello di rete nei giochi per principianti

Informazioni sul modello di rete nei giochi per principianti
Nelle ultime due settimane ho lavorato sul motore di rete per il mio gioco. Prima di allora, non sapevo assolutamente nulla del networking nei giochi, quindi ho letto molti articoli e fatto molti esperimenti per comprendere tutti i concetti ed essere in grado di scrivere il mio motore di rete.

In questa guida, vorrei condividere con te i vari concetti che devi apprendere prima di scrivere il tuo motore di gioco, nonché le migliori risorse e articoli per apprenderli.

In generale, esistono due tipi principali di architetture di rete: peer-to-peer e client-server. In un'architettura peer-to-peer (p2p), i dati vengono trasferiti tra qualsiasi coppia di giocatori connessi, mentre in un'architettura client-server, i dati vengono trasferiti solo tra i giocatori e il server.

Sebbene l'architettura peer-to-peer sia ancora utilizzata in alcuni giochi, il client-server è lo standard: è più facile da implementare, richiede una larghezza di canale minore e rende più facile la protezione dagli imbrogli. Pertanto, in questo tutorial ci concentreremo sull'architettura client-server.

In particolare, siamo più interessati ai server autoritari: in tali sistemi il server ha sempre ragione. Ad esempio, se il giocatore pensa di essere in (10, 5) e il server gli dice che è in (5, 3), allora il client dovrebbe sostituire la sua posizione con quella segnalata dal server, e non viceversa. L'uso di server autorevoli rende più facile riconoscere gli imbroglioni.

I sistemi di gioco in rete hanno tre componenti principali:

  • Protocollo di trasporto: come vengono trasferiti i dati tra client e server.
  • Protocollo applicativo: cosa viene trasmesso dai client al server e dal server ai client e in quale formato.
  • Logica applicativa: come i dati trasmessi vengono utilizzati per aggiornare lo stato dei client e del server.

È molto importante comprendere il ruolo di ciascuna parte e le sfide ad essa associate.

Protocollo Trasporti

Il primo passo è scegliere un protocollo per il trasporto dei dati tra il server e i client. A questo scopo esistono due protocolli Internet: TCP и UDP. Ma puoi creare il tuo protocollo di trasporto basato su uno di essi o utilizzare una libreria che li utilizzi.

Confronto tra TCP e UDP

Sia TCP che UDP sono basati su IP. IP consente di trasmettere un pacchetto da una sorgente a un destinatario, ma non garantisce che il pacchetto inviato prima o poi raggiungerà il destinatario, che vi arriverà almeno una volta e che la sequenza di pacchetti arriverà in l'ordine corretto. Inoltre, un pacchetto può contenere solo una dimensione di dati limitata, data dal valore MTU.

UDP è solo un sottile strato sopra IP. Pertanto, ha le stesse limitazioni. Al contrario, TCP ha molte funzionalità. Fornisce una connessione affidabile e ordinata tra due nodi con controllo degli errori. Quindi, TCP è molto conveniente e viene utilizzato in molti altri protocolli, ad es. HTTP, FTP и SMTP. Ma tutte queste funzionalità hanno un prezzo: ritardo.

Per capire perché queste funzioni possono causare latenza, dobbiamo capire come funziona TCP. Quando l'host mittente trasmette un pacchetto all'host ricevente, si aspetta di ricevere un riconoscimento (ACK). Se dopo un certo tempo non lo riceve (perché è andato perso il pacchetto o la conferma, o per qualche altro motivo), invia nuovamente il pacchetto. Inoltre, TCP garantisce che i pacchetti vengano ricevuti nell'ordine corretto, quindi finché non viene ricevuto un pacchetto perso, tutti gli altri pacchetti non possono essere elaborati, anche se sono già stati ricevuti dal nodo ricevente.

Ma come probabilmente capirai, la latenza nei giochi multiplayer è molto importante, specialmente in generi attivi come gli FPS. Ecco perché molti giochi utilizzano UDP con il proprio protocollo.

Un protocollo nativo basato su UDP può essere più efficiente di TCP per vari motivi. Ad esempio, può contrassegnare alcuni pacchetti come attendibili e altri come non attendibili. Pertanto, non gli interessa se il pacchetto non attendibile raggiunge il destinatario. Oppure può elaborare più flussi di dati in modo che un pacchetto perso in un flusso non rallenti i flussi rimanenti. Ad esempio, potrebbe esserci un thread per l'input del giocatore e un altro thread per i messaggi di chat. Se un messaggio di chat non urgente viene perso, l'input urgente non verrà rallentato. Oppure un protocollo proprietario potrebbe implementare l'affidabilità in modo diverso rispetto al TCP per essere più efficiente in un ambiente di videogiochi.

Quindi, se il TCP fa schifo, allora costruiremo il nostro protocollo di trasporto basato su UDP?

E' un po' più complicato. Anche se TCP è quasi non ottimale per i sistemi di rete di gioco, può funzionare abbastanza bene per il tuo gioco specifico e farti risparmiare tempo prezioso. Ad esempio, la latenza potrebbe non essere un problema per un gioco a turni o per un gioco che può essere giocato solo su reti LAN, dove la latenza e la perdita di pacchetti sono molto inferiori rispetto a Internet.

Molti giochi di successo, tra cui World of Warcraft, Minecraft e Terraria, utilizzano TCP. Tuttavia, la maggior parte degli FPS utilizza i propri protocolli basati su UDP, quindi ne parleremo più approfonditamente di seguito.

Se decidi di utilizzare TCP, assicurati che sia disabilitato Algoritmo di Nagle, perché memorizza i pacchetti nel buffer prima dell'invio, il che significa che aumenta la latenza.

Per approfondire le differenze tra UDP e TCP nell'ambito dei giochi multiplayer puoi leggere l'articolo di Glenn Fiedler UDP contro TCP.

Proprio protocollo

Vuoi creare il tuo protocollo di trasporto, ma non sai da dove cominciare? Sei fortunato perché Glenn Fiedler ha scritto due articoli straordinari a riguardo. Troverai molti pensieri intelligenti in loro.

Il primo articolo Networking per programmatori di giochi 2008, più facile del secondo Costruire un protocollo di rete di gioco 2016. Ti consiglio di iniziare con quello più vecchio.

Tieni presente che Glenn Fiedler è un grande sostenitore dell'utilizzo del proprio protocollo basato su UDP. E dopo aver letto i suoi articoli, probabilmente adotterai la sua opinione secondo cui TCP presenta seri inconvenienti nei videogiochi e vorrai implementare il tuo protocollo.

Ma se sei nuovo al networking, fatti un favore e usa TCP o una libreria. Per implementare con successo il proprio protocollo di trasporto, è necessario imparare molto in anticipo.

Biblioteche in rete

Se hai bisogno di qualcosa di più efficiente di TCP, ma non vuoi preoccuparti di implementare il tuo protocollo e di entrare in molti dettagli, puoi utilizzare la libreria di rete. Ce ne sono molti:

Non li ho provati tutti, ma preferisco ENet perché è facile da usare e affidabile. Inoltre, ha una documentazione chiara e un tutorial per principianti.

Protocollo trasporti: conclusione

Riassumendo: esistono due principali protocolli di trasporto: TCP e UDP. TCP ha molte caratteristiche utili: affidabilità, conservazione dell'ordine dei pacchetti, rilevamento degli errori. UDP non ha tutto questo, ma TCP per sua natura ha una maggiore latenza, il che è inaccettabile per alcuni giochi. Cioè, per garantire una bassa latenza, puoi creare il tuo protocollo basato su UDP o utilizzare una libreria che implementa un protocollo di trasporto su UDP ed è adattata ai videogiochi multiplayer.

La scelta tra TCP, UDP e la libreria dipende da diversi fattori. Innanzitutto dalle esigenze del gioco: è necessaria una bassa latenza? In secondo luogo, dai requisiti del protocollo applicativo: è necessario un protocollo affidabile? Come vedremo nella parte successiva, è possibile creare un protocollo applicativo per il quale un protocollo inaffidabile è del tutto adatto. Infine, è necessario considerare anche l’esperienza dello sviluppatore del motore di rete.

Ho due consigli:

  • Astrarre il più possibile il protocollo di trasporto dal resto dell'applicazione in modo che possa essere facilmente sostituito senza riscrivere tutto il codice.
  • Non ottimizzare eccessivamente. Se non sei un esperto di rete e non sei sicuro di aver bisogno di un protocollo di trasporto personalizzato basato su UDP, puoi iniziare con TCP o una libreria che offra affidabilità, quindi testare e misurare le prestazioni. Se sorgono problemi e sei sicuro che la causa sia il protocollo di trasporto, potrebbe essere il momento di creare il tuo protocollo di trasporto.

Alla fine di questa parte, ti consiglio di leggere Introduzione alla programmazione di giochi multiplayer Brian Hook, che copre molti degli argomenti discussi qui.

Protocollo applicativo

Ora che possiamo scambiare dati tra client e server, dobbiamo decidere quali dati trasferire e in quale formato.

Lo schema classico prevede che i client inviino input o azioni al server e il server invii lo stato corrente del gioco ai client.

Il server non invia lo stato completo, ma filtrato con le entità vicine al giocatore. Lo fa per tre ragioni. Innanzitutto, lo stato totale potrebbe essere troppo grande per essere trasmesso ad alta frequenza. In secondo luogo, i clienti sono interessati principalmente ai dati visivi e audio, perché la maggior parte della logica del gioco viene simulata sul server di gioco. In terzo luogo, in alcuni giochi il giocatore non ha bisogno di conoscere alcuni dati, come la posizione del nemico dall'altra parte della mappa, perché altrimenti può annusare i pacchetti e sapere esattamente dove muoversi per ucciderlo.

Serializzazione

Il primo passo è convertire i dati che vogliamo inviare (input o stato del gioco) in un formato adatto alla trasmissione. Questo processo si chiama serializzazione.

Il pensiero che viene subito in mente è quello di utilizzare un formato leggibile dall'uomo, come JSON o XML. Ma questo sarebbe del tutto inefficace e sprecherebbe gran parte del canale.

Si consiglia invece di utilizzare il formato binario, che è molto più compatto. Cioè, i pacchetti conterranno solo pochi byte. Qui dobbiamo tenere conto del problema ordine dei byte, che potrebbe differire su computer diversi.

Per serializzare i dati è possibile utilizzare una libreria, ad esempio:

Assicurati solo che la biblioteca crei archivi portatili e si preoccupi dell'endianness.

Una soluzione alternativa è implementarla da soli; non è particolarmente difficile, soprattutto se si utilizza un approccio incentrato sui dati al codice. Inoltre, ti consentirà di eseguire ottimizzazioni che non sono sempre possibili quando si utilizza la libreria.

Glenn Fiedler ha scritto due articoli sulla serializzazione: Lettura e scrittura di pacchetti и Strategie di serializzazione.

compressione

La quantità di dati trasferiti tra client e server è limitata dalla larghezza di banda del canale. La compressione dei dati ti consentirà di trasferire più dati in ogni istantanea, aumentare la frequenza di aggiornamento o semplicemente ridurre i requisiti di larghezza di banda.

Confezione di pezzi

La prima tecnica è il bitpacking. Consiste nell'utilizzare esattamente il numero di bit necessari per descrivere il valore desiderato. Ad esempio, se hai un'enumerazione che può avere 16 valori diversi, invece di un intero byte (8 bit), puoi utilizzare solo 4 bit.

Glenn Fiedler spiega come implementarlo nella seconda parte dell'articolo Lettura e scrittura di pacchetti.

Il bitpacking funziona particolarmente bene con la discretizzazione, che sarà l'argomento della prossima sezione.

Campionamento

Campionamento è una tecnica di compressione con perdita che utilizza solo un sottoinsieme di possibili valori per codificare un valore. Il modo più semplice per implementare la discretizzazione è arrotondare i numeri in virgola mobile.

Glenn Fiedler (ancora!) mostra come applicare nella pratica la discretizzazione nel suo articolo Compressione delle istantanee.

Algoritmi di compressione

La prossima tecnica saranno gli algoritmi di compressione senza perdite.

Ecco, secondo me, i tre algoritmi più interessanti che devi conoscere:

  • Codifica di Huffman con codice precalcolato, che è estremamente veloce e può produrre buoni risultati. È stato utilizzato per comprimere i pacchetti nel motore di rete Quake3.
  • zlib è un algoritmo di compressione generico che non aumenta mai la quantità di dati. Come puoi vedere? qui, è stato utilizzato in una varietà di applicazioni. Per l'aggiornamento degli stati, potrebbe essere ridondante. Ma può tornare utile se devi inviare risorse, testi lunghi o terreno ai client dal server.
  • Copia delle tirature - Questo è probabilmente l'algoritmo di compressione più semplice, ma è molto efficace per determinati tipi di dati e può essere utilizzato come fase di pre-elaborazione prima di zlib. È particolarmente adatto per comprimere terreni composti da piastrelle o voxel in cui si ripetono molti elementi adiacenti.

compressione delta

L'ultima tecnica di compressione è la compressione delta. Consiste nel fatto che vengono trasmesse solo le differenze tra lo stato attuale del gioco e l'ultimo stato ricevuto dal client.

È stato utilizzato per la prima volta nel motore di rete Quake3. Ecco due articoli che spiegano come usarlo:

Anche Glenn Fiedler lo ha utilizzato nella seconda parte del suo articolo. Compressione delle istantanee.

Шифрование

Inoltre, potrebbe essere necessario crittografare la trasmissione di informazioni tra client e server. Ci sono diverse ragioni per questo:

  • privacy/riservatezza: i messaggi possono essere letti solo dal destinatario e nessun'altra persona che sniffa la rete sarà in grado di leggerli.
  • autenticazione: una persona che vuole interpretare il ruolo di giocatore deve conoscere la sua chiave.
  • prevenzione dei cheat: sarà molto più difficile per i malintenzionati creare i propri pacchetti cheat, dovranno replicare lo schema di crittografia e trovare la chiave (che cambia ad ogni connessione).

Consiglio vivamente di utilizzare una libreria per questo. Suggerisco di utilizzare libsodico, perché è particolarmente semplice e contiene ottimi tutorial. Di particolare interesse è il tutorial su scambio di chiavi, che consente di generare nuove chiavi ad ogni nuova connessione.

Protocollo applicativo: conclusione

Questo conclude il nostro protocollo applicativo. Credo che la compressione sia del tutto facoltativa e la decisione di utilizzarla dipenda solo dal gioco e dalla larghezza di banda richiesta. La crittografia, secondo me, è obbligatoria, ma nel primo prototipo puoi farne a meno.

Logica applicativa

Ora siamo in grado di aggiornare lo stato nel client, ma potremmo riscontrare problemi di latenza. Il giocatore, dopo aver effettuato un input, deve attendere un aggiornamento dello stato del gioco dal server per vedere quale effetto ha avuto sul mondo.

Inoltre, tra due aggiornamenti di stato, il mondo è completamente statico. Se la velocità di aggiornamento dello stato è bassa, i movimenti saranno molto a scatti.

Esistono diverse tecniche per ridurre l'impatto di questo problema e le tratterò nella sezione successiva.

Tecniche di lisciatura del ritardo

Tutte le tecniche descritte in questa sezione vengono discusse in dettaglio nella serie. Multigiocatore dal ritmo frenetico Gabriele Gambetta. Consiglio vivamente la lettura di questa eccellente serie di articoli. Include anche una demo interattiva per vedere come funzionano queste tecniche nella pratica.

La prima tecnica consiste nell'applicare direttamente il risultato dell'input senza attendere una risposta dal server. È chiamato previsione lato client. Tuttavia, quando il client riceve un aggiornamento dal server, deve verificare che la sua previsione fosse corretta. Se così non fosse, dovrà semplicemente cambiare il suo stato in base a ciò che ha ricevuto dal server, perché il server è autoritario. Questa tecnica è stata utilizzata per la prima volta in Quake. Puoi leggere di più a riguardo nell'articolo. Revisione del codice di Quake Engine Fabien Sanglarstraduzione su Habré].

La seconda serie di tecniche viene utilizzata per agevolare il movimento di altre entità tra due aggiornamenti di stato. Esistono due modi per risolvere questo problema: interpolazione ed estrapolazione. Nel caso dell'interpolazione vengono presi gli ultimi due stati e viene mostrato il passaggio dall'uno all'altro. Lo svantaggio è che provoca una piccola frazione del ritardo, perché il cliente vede sempre cosa è successo in passato. L'estrapolazione consiste nel prevedere dove dovrebbero trovarsi ora le entità in base all'ultimo stato ricevuto dal client. Il suo svantaggio è che se l'entità cambia completamente la direzione del movimento, si verificherà un grande errore tra la posizione prevista e quella reale.

L'ultima tecnica più avanzata, utile solo negli FPS, è compensazione del ritardo. Quando si utilizza la compensazione del ritardo, il server tiene conto dei ritardi del client quando spara al bersaglio. Ad esempio, se un giocatore eseguisse un colpo alla testa sul proprio schermo, ma in realtà il suo bersaglio si trovasse in una posizione diversa a causa del ritardo, allora sarebbe ingiusto negare al giocatore il diritto di uccidere a causa del ritardo. Quindi il server riavvolge il tempo fino a quando il giocatore ha sparato per simulare ciò che il giocatore ha visto sullo schermo e verificare la collisione tra il tiro e il bersaglio.

Glenn Fiedler (come sempre!) scrisse un articolo nel 2004 Fisica della rete (2004), in cui ha gettato le basi per la sincronizzazione delle simulazioni fisiche tra il server e il client. Nel 2014 ha scritto una nuova serie di articoli fisica delle reti, che descriveva altre tecniche per sincronizzare le simulazioni fisiche.

Ci sono anche due articoli sul wiki di Valve, Rete multigiocatore di origine и Metodi di compensazione della latenza nella progettazione e ottimizzazione del protocollo di gioco client/server che prevedono un risarcimento per i ritardi.

Prevenire gli imbrogli

Esistono due principali tecniche di prevenzione dei cheat.

Primo: rendere più difficile per gli imbroglioni inviare pacchetti dannosi. Come accennato in precedenza, un buon modo per implementarlo è la crittografia.

In secondo luogo, il server autorevole dovrebbe ricevere solo comandi/input/azioni. Il client non dovrebbe essere in grado di modificare lo stato sul server se non inviando input. Quindi il server, ogni volta che riceve input, deve verificarne la validità prima di applicarlo.

Logica dell'applicazione: conclusione

Ti consiglio di implementare un modo per simulare un'elevata latenza e una bassa frequenza di aggiornamento in modo da poter testare il comportamento del tuo gioco in cattive condizioni, anche quando il client e il server sono in esecuzione sulla stessa macchina. Ciò semplifica notevolmente l'implementazione delle tecniche di livellamento del ritardo.

Altre risorse utili

Se desideri esplorare altre risorse sul modello di rete, puoi trovarle qui:

Fonte: habr.com

Aggiungi un commento