Sulla rete modello in giochi per principianti

Sulla rete modello in giochi per principianti
Negli ultimi due settimane ho lavorato a un motore di rete per il mio gioco. Prima di questo non sapevo niente delle tecnologie di rete nei giochi, quindi ho letto numerosi articoli e ho eseguito molti esperimenti per capire tutti i concetti e poter scrivere il mio motore di rete.

In questa guida vorrei condividere con voi vari concetti che dovete studiare prima di scrivere il vostro motore di gioco, oltre alle migliori risorse e articoli per approfondirli.

In generale, esistono due tipi principali di architetture di rete: peer-to-peer e client-server. Nell'architettura peer-to-peer (p2p), i dati vengono trasmessi tra coppie di giocatori connessi, mentre nell'architettura client-server, i dati vengono trasmessi solo tra i giocatori e il server.

Sebbene l'architettura peer-to-peer sia ancora utilizzata in alcuni giochi, lo standard è l'architettura client-server: è più semplice da implementare, richiede una larghezza di banda inferiore e facilita la protezione contro le frodi. Pertanto, in questa guida ci concentreremo sull'architettura client-server.

In particolare, ci interessano di più i server autoritari: in questi sistemi il server ha sempre ragione. Ad esempio, se un giocatore pensa di trovarsi nelle coordinate (10, 5) e il server gli dice che si trova in (5, 3), il client deve sostituire la sua posizione con quella fornita dal server, e non viceversa. L'uso di server autoritari semplifica il riconoscimento dei cheater.

Nei sistemi di rete dei giochi ci sono tre componenti principali:

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

È molto importante capire il ruolo di ogni parte e le difficoltà ad esse associate.

Protocollo di trasporto

Il primo passo consiste nella scelta del protocollo per il trasporto dei dati tra il server e i client. A questo proposito ci sono due protocolli Internet: TCP e UDP. Ma potete anche creare un vostro protocollo di trasporto basato su uno di essi o utilizzare una libreria in cui vengono utilizzati.

Confronto tra TCP e UDP

Sia TCP che UDP sono basati su IP. L'IP consente di trasferire pacchetti dalla sorgente al destinatario, ma non garantisce che il pacchetto inviato arriverà al destinatario, almeno una volta, e che l'ordine dei pacchetti sarà corretto. Inoltre, un pacchetto può contenere solo una dimensione limitata di dati, determinata dalla grandezza MTU.

. UDP è solo un sottile strato sopra l'IP. Pertanto, ha le stesse limitazioni. A differenza di esso, il TCP ha molte caratteristiche. Fornisce una connessione affidabile e ordinata tra due nodi con un controllo degli errori. Pertanto, il TCP è molto pratico ed è utilizzato in molti altri protocolli, come ad esempio HTTP, FTP e SMTP. Ma tutte queste funzionalità hanno un costo: latenza.

Per capire perché queste funzionalità possono causare latenza, è necessario comprendere come funziona il TCP. Quando un nodo mittente invia un pacchetto a un nodo destinatario, attende di ricevere una conferma (ACK). Se dopo un certo tempo non lo riceve (perché il pacchetto o la conferma sono stati persi, o per altre ragioni), il pacchetto viene inviato di nuovo. Inoltre, il TCP garantisce la ricezione dei pacchetti nell'ordine corretto, quindi finché il pacchetto perso non viene ricevuto, tutti gli altri pacchetti non possono essere elaborati, anche se sono già stati ricevuti dal nodo destinatario.

Ma come probabilmente capite, la latenza nei giochi multiplayer è molto importante, specialmente in generi attivi come gli FPS. È per questo che molti giochi utilizzano UDP con un protocollo proprio.

Un protocollo proprio basato su UDP può essere più efficiente del TCP per vari motivi. Ad esempio, può contrassegnare alcuni pacchetti come affidabili e altri come non affidabili. Pertanto, non si preoccupa di sapere se un pacchetto non affidabile è arrivato al destinatario. Oppure può gestire più flussi di dati, in modo che un pacchetto perso in un flusso non rallenti gli altri flussi. Ad esempio, può esserci un flusso per l'input del giocatore e un altro flusso per i messaggi della chat. Se un messaggio della chat, che non è un dato urgente, viene perso, non rallenterà il rilevamento dell'input, che è urgente. Oppure il protocollo proprio può implementare l'affidabilità in modo diverso rispetto al TCP, per essere più efficiente nelle condizioni dei videogiochi.

Quindi, se il TCP è così scadente, stiamo per creare il nostro protocollo di trasporto basato su UDP?

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

In molti giochi di successo, tra cui World of Warcraft, Minecraft e Terraria, viene utilizzato il TCP. Tuttavia, nella maggior parte degli FPS vengono applicati protocolli proprietari basati su UDP, pertanto parleremo di essi più in dettaglio qui sotto.

Se decidi di utilizzare il TCP, assicurati che sia disattivato l'algoritmo di Nagle, poiché esso bufferizza i pacchetti prima dell'invio, aumentando così la latenza.

Per saperne di più sulle differenze tra UDP e TCP nel contesto dei giochi multiplayer, puoi leggere l'articolo di Glenn Fielder UDP vs. TCP.

Protocollo proprietario

Quindi desideri creare un protocollo di trasporto proprietario, ma non sai da dove cominciare? Sei fortunato, poiché Glenn Fielder ha scritto due articoli fantastici su questo argomento. In essi troverai molte idee intelligenti.

Il primo articolo, Networking for Game Programmers del 2008, è più semplice del secondo, Building A Game Network Protocol del 2016. Ti consiglio di iniziare dal più vecchio.

Tieni presente che Glenn Fielder è un grande sostenitore dell'uso di protocolli proprietari basati su UDP. E dopo aver letto i suoi articoli, probabilmente condividerai la sua opinione che il TCP presenta gravi svantaggi nei videogiochi e vorrai implementare un protocollo proprietario.

Ma se sei un principiante nel lavoro con le reti, fai un favore a te stesso e utilizza il TCP o una libreria. Per implementare con successo un protocollo di trasporto proprio, c'è molto da imparare prima.

Librerie di rete

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

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

Protocollo di trasporto: conclusione

Riepiloghiamo: ci sono due protocolli di trasporto principali: TCP e UDP. TCP ha molte caratteristiche utili: affidabilità, mantenimento dell'ordine dei pacchetti, rilevamento degli errori. UDP non ha nulla di tutto ciò, ma TCP per sua natura ha latenze maggiori, inaccettabili per alcuni giochi. Questo significa che, per garantire latenze basse, si può creare un proprio protocollo basato su UDP o utilizzare una libreria che implementi un protocollo di trasporto su UDP e adattata per i giochi multiplayer.

La scelta tra TCP, UDP e una libreria dipende da diversi fattori. Innanzitutto, dalle esigenze del gioco: ha bisogno di latenze basse? In secondo luogo, dai requisiti del protocollo applicativo: ha bisogno di un protocollo affidabile? Come vedremo nella parte successiva, si può creare un protocollo applicativo per il quale un protocollo non affidabile è del tutto adatto. Infine, bisogna considerare anche l'esperienza dello sviluppatore del motore di rete.

Ho due consigli:

  • Astrai al massimo il protocollo di trasporto dal resto dell'applicazione, in modo da poterlo sostituire facilmente senza riscrivere tutto il codice.
  • Non ottimizzare prematuramente. Se non sei un esperto di reti e non sei sicuro di aver bisogno di un proprio protocollo di trasporto basato su UDP, puoi iniziare con TCP o una libreria che garantisca affidabilità, per poi testare e misurare le prestazioni. Se sorgono problemi e sei certo che la causa sia nel protocollo di trasporto, potrebbe essere il momento di creare un proprio protocollo di trasporto.

In conclusione di questa parte, ti consiglio di leggere Introduction to Multiplayer Game Programming di Brian Hook, in cui vengono trattati molti dei temi discussi qui.

Protocollo applicativo

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

Lo schema classico consiste nel fatto che i clienti inviano al server input o azioni, mentre il server invia ai clienti lo stato attuale del gioco.

Il server invia uno stato filtrato e non completo con le entità che si trovano nei pressi del giocatore. Questo avviene per tre motivi. In primo luogo, lo stato completo potrebbe essere troppo grande per essere trasmesso con alta frequenza. In secondo luogo, ai clienti interessano principalmente i dati visivi e audio, poiché la maggior parte della logica di gioco viene simulata sul server di gioco. In terzo luogo, in alcuni giochi il giocatore non deve conoscere certe informazioni, ad esempio la posizione del nemico all'altro capo della mappa, poiché altrimenti potrebbe sniffare i pacchetti e sapere esattamente dove muoversi per ucciderlo.

Serializzazione

Il primo passo consiste nel convertire i dati che vogliamo inviare (input o stato di gioco) in un formato appropriato per la trasmissione. Questo processo è chiamato serializzazione.

Ci viene subito in mente l'idea di utilizzare un formato leggibile dall'uomo, come JSON o XML. Ma sarebbe completamente inefficiente e occuperebbe gran parte della larghezza di banda.

Invece, si consiglia di utilizzare un formato binario, che è molto più compatto. Ciò significa che i pacchetti conterranno solo pochi byte. Qui bisogna tenere presente il problema dell'ordine dei byte, che può variare tra diversi computer.

Per serializzare i dati si può utilizzare una libreria, ad esempio:

Assicurati solo che la libreria crei archivi portabili e gestisca l'ordine dei byte.

Una soluzione alternativa potrebbe essere un'implementazione autonoma, che non è particolarmente difficile, soprattutto se nel codice adotti un approccio orientato ai dati. Inoltre, ti consentirà di effettuare ottimizzazioni che non sempre sono possibili usando una libreria.

Glenn Fidler ha scritto due articoli sulla serializzazione: Reading and Writing Packets e Serialization Strategies.

Compressione

La quantità di dati trasmessi tra clienti e server è limitata dalla larghezza di banda del canale. La compressione dei dati consentirà di trasmettere più dati in ciascun snapshot, aumentare la frequenza di aggiornamento o semplicemente ridurre i requisiti del canale.

Imballaggio bit per bit

La prima tecnica è l'imballaggio dei bit. Consiste nell'utilizzare esattamente il numero di bit necessari per descrivere la grandezza richiesta. Ad esempio, se hai un elenco che può avere 16 valori diversi, puoi utilizzare solo 4 bit invece di un intero byte (8 bit).

Glenn Fiedler spiega come implementarlo nella seconda parte dell'articolo Reading and Writing Packets.

L'imballaggio dei bit funziona particolarmente bene con la discretizzazione, che sarà l'argomento della prossima sezione.

Discretizzazione

Discretizzazione è una tecnica di compressione lossy basata sull'utilizzo di un sottoinsieme dei valori possibili per la codificazione di una grandezza. La discretizzazione è più facile da implementare arrotondando i numeri in virgola mobile.

Glenn Fiedler (di nuovo!) mostra come applicare la discretizzazione nella pratica nel suo articolo Compressione Snapshot.

Algoritmi di compressione

La prossima tecnica riguarda gli algoritmi di compressione senza perdita.

Ecco, a mio avviso, tre degli algoritmi più interessanti da conoscere:

  • Codifica di Huffman con codice pre-calcolato, che è estremamente veloce e può dare buoni risultati. È stato utilizzato per comprimere i pacchetti nel motore di rete Quake3.
  • zlib è un algoritmo di compressione generale che non aumenta mai la dimensione dei dati. Come puoi vedere qui, è stato utilizzato in molte applicazioni. Per l'aggiornamento degli stati, potrebbe rivelarsi ridondante. Ma può essere utile se hai bisogno di inviare asset, testi lunghi o terreni ai client dal server.
  • Copiatura di lunghe serie è probabilmente l'algoritmo di compressione più semplice, ma è molto efficace per determinati tipi di dati e può essere utilizzato come fase di pre-processamento prima di zlib. È particolarmente adatto per comprimere terreni composti da tile o voxel, in cui molti elementi adiacenti si ripetono.

Compressione delta

L'ultima metodologia di compressione è la compressione delta. Consiste nell'inviare solo le differenze tra lo stato attuale del gioco e l'ultimo stato ricevuto dal client.

È stata utilizzata per la prima volta nel motore di rete Quake3. Ecco due articoli che spiegano come utilizzarla:

Glenn Fidler ha anche utilizzato questo nella seconda parte del suo articolo Compressione Snapshot.

Crittografia

Inoltre, potreste avere bisogno di crittografare la trasmissione delle informazioni tra clienti e server. Ci sono diverse ragioni per questo:

  • privacy/confidenzialità: i messaggi possono essere letti solo dal destinatario e nessun'altra persona che svolge il sniffing della rete può leggerli.
  • autenticazione: la persona che desidera recitare il ruolo di giocatore deve conoscere la sua chiave.
  • prevenzione della frode: ai giocatori malintenzionati sarà molto più difficile creare i propri pacchetti per barare, dovranno riprodurre lo schema di crittografia e trovare la chiave (che cambia ad ogni connessione).

Raccomando vivamente di usare una libreria per questo. Suggerisco di utilizzare libsodium, perché è particolarmente semplice e ha ottimi tutorial. Particolarmente interessante è il tutorial su scambio delle chiavi, che consente di generare nuove chiavi ad ogni nuova connessione.

Protocollo dell'applicazione: conclusione

Concludiamo con il protocollo dell'applicazione. Ritengo che la compressione sia del tutto superflua e la decisione di utilizzarla dipende solo dal gioco e dalla larghezza di banda richiesta. La crittografia, secondo me, è obbligatoria, ma nel primo prototipo si può fare a meno di essa.

Logica dell'applicazione

Ora siamo in grado di aggiornare lo stato nel client, ma potremmo affrontare problemi di latenza. Il giocatore, effettuando un input, deve attendere l'aggiornamento dello stato del gioco dal server per vedere quale impatto ha avuto sul mondo.

Inoltre, tra due aggiornamenti dello stato, il mondo è completamente statico. Se la frequenza di aggiornamento degli stati è bassa, i movimenti saranno molto scattosi.

Esistono diverse tecniche per ridurre l'impatto di questo problema e nel prossimo paragrafo parlerò di esse.

Tecniche di alleviamento della latenza

Tutte le tecniche descritte in questo paragrafo sono dettagliatamente trattate nella serie Fast-Paced Multiplayer di Gabriël Gambetta. Raccomando vivamente di leggere questa straordinaria serie di articoli. Include anche una demo interattiva che mostra come queste tecniche funzionano nella pratica.

La prima tecnica consiste nell'applicare il risultato dell'input direttamente, senza aspettare la risposta del server. Questo si chiama previsione da parte del client. Tuttavia, quando il cliente riceve un aggiornamento dal server, deve assicurarsi che la sua previsione sia stata corretta. In caso contrario, deve semplicemente modificare il proprio stato in base a quanto ricevuto dal server, poiché il server è autoritario. Questa tecnica è stata utilizzata per la prima volta in Quake. Maggiori dettagli possono essere trovati nell'articolo Revisione del codice Quake Engine Fabien Sanglar [traduzione su Habr].

Un secondo insieme di tecniche viene utilizzato per levigare il movimento di altre entità tra due aggiornamenti di stato. Ci sono due modi per affrontare questo problema: interpolazione ed estrapolazione. Nel caso dell'interpolazione, vengono presi i due stati più recenti e viene mostrato il passaggio da uno all'altro. Il suo svantaggio è che provoca un piccolo ritardo, poiché il client vede sempre ciò che è accaduto in passato. L'estrapolazione consiste nel prevedere dove dovrebbero trovarsi attualmente le entità sulla base dell'ultimo stato ricevuto dal client. Il suo svantaggio è che se un'entità cambia completamente direzione, ci sarà un grande errore tra la previsione e la posizione reale.

L'ultima tecnica, la più avanzata, utile solo in FPS, è la compensazione del lag. Con l'uso della compensazione del lag, il server tiene conto dei ritardi del cliente quando spara a un bersaglio. Per esempio, se un giocatore ha effettuato un headshot sul proprio schermo, ma in realtà il suo bersaglio a causa del lag si trovava in un'altra posizione, sarebbe sleale negare al giocatore il diritto di uccidere a causa del ritardo. Pertanto, il server riavvolge il tempo fino al momento in cui il giocatore ha sparato, per simulare ciò che il giocatore vedeva sul proprio schermo e verificare la collisione tra il suo proiettile e il bersaglio.

Glenn Fidler (come sempre!) ha scritto nel 2004 un articolo Network Physics (2004), nel quale ha posto le basi per la sincronizzazione della simulazione fisica tra server e client. Nel 2014 ha scritto una nuova serie di articoli Networking Physics, in cui ha descritto altre tecniche per sincronizzare la simulazione fisica.

C'è anche nel wiki della compagnia Valve due articoli, Source Multiplayer Networking e Metodi di compensazione del lag nella progettazione e ottimizzazione del protocollo in-game Client/Server , in cui viene discussa la compensazione dei ritardi.

Prevenzione delle frodi

Esistono due tecniche principali per prevenire le frodi.

Primo: complicazione dell'invio di pacchetti dannosi da parte dei cheater. Come già detto, un buon modo per implementarlo è la crittografia.

Secondo: il server autoritario deve ricevere solo comandi/inserimenti/azioni. Il client non deve avere la possibilità di modificare lo stato sul server, se non inviando input. Pertanto, il server, ogni volta che riceve un input, deve verificarne la validità prima di applicarlo.

Logica dell'applicazione: conclusione

Ti consiglio di implementare un modo per simulare grandi ritardi e frequenze di aggiornamento basse, in modo da poter testare il comportamento del tuo gioco in condizioni avverse, anche quando client e server sono eseguiti sullo stesso computer. Questo semplificherà notevolmente l'implementazione delle metodologie di smussamento dei ritardi.

Altre risorse utili

Se desideri esplorare altre risorse dedicate ai modelli di rete, puoi trovarle qui:

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster