Modelli di rete nei giochi per principianti

Modelli di rete nei giochi per principianti
Negli ultimi due settimane ho lavorato a un motore di rete per il mio gioco. Prima di questo, non sapevo nulla delle tecnologie di rete nei giochi, quindi ho letto molti articoli e fatto numerosi esperimenti per comprendere tutti i concetti e avere la possibilità di scrivere il mio motore di rete.

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

In generale, esistono due principali tipi di architetture di rete: peer-to-peer e client-server. Nell'architettura peer-to-peer (p2p), i dati vengono trasmessi tra qualsiasi coppia 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 venga 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, siamo maggiormente interessati ai server autoritari: in tali sistemi, il server ha sempre ragione. Ad esempio, se un giocatore pensa di trovarsi alle coordinate (10, 5), ma il server gli comunica che si trova a (5, 3), il client deve sostituire la propria posizione con quella fornita dal server, e non viceversa. L'uso di server autoritari semplifica il riconoscimento dei cheater.

Nei sistemi di rete di gioco, ci sono tre componenti principali:

  • Protocollo di trasporto: come vengono trasmessi i dati tra i client e il server.
  • Protocollo applicativo: cosa viene inviato dai client al server e viceversa 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 difficoltà correlate.

Protocollo di trasporto

Il primo passo consiste nella scelta di un protocollo per il trasporto dei dati tra il server e i client. A tale scopo, esistono due protocolli Internet: TCP e UDP. Ma puoi anche crearne uno tuo basato su uno di essi o utilizzare una libreria in cui vengono adottati.

Confronto tra TCP e UDP

Sia TCP che UDP si basano su IP. L'IP consente di trasferire pacchetti dall'origine al destinatario, ma non garantisce che il pacchetto inviato arrivi mai al destinatario, che lo riceva almeno una volta e che la sequenza dei pacchetti arrivi nell'ordine corretto. Inoltre, un pacchetto può contenere solo una quantità limitata di dati, determinata dalla dimensione MTU.

UDP è semplicemente uno strato sottile sopra l'IP. Pertanto, ha gli stessi limiti. Al contrario, TCP offre molte funzionalità. Garantisce una connessione affidabile e ordinata tra due nodi con controlli di errore. Di conseguenza, TCP è molto conveniente ed è utilizzato in molti altri protocolli, ad esempio in HTTP, FTP e SMTP. Ma tutte queste funzionalità hanno un costo: latenza.

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

Ma come sicuramente saprete, il ritardo nei videogiochi multiplayer è molto importante, specialmente in generi attivi come gli FPS. Ecco perché molti giochi utilizzano UDP con un proprio protocollo.

Un protocollo proprietario 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 se un pacchetto non affidabile arrivi al destinatario. Può anche gestire più flussi di dati, in modo che un pacchetto perso in un flusso non rallenti gli altri flussi. Ad esempio, potrebbe esserci un flusso per l'input del giocatore e un altro per i messaggi della chat. Se un messaggio della chat, che non è considerato urgente, va perso, non rallenterà l'elaborazione dell'input, che è invece fondamentale. Inoltre, il protocollo proprietario può implementare l'affidabilità in modo diverso rispetto al TCP, rendendolo più efficace nelle condizioni dei videogiochi.

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

Le cose sono un po' più complesse. Anche se TCP è quasi subottimale per i sistemi di rete di gioco, può funzionare piuttosto bene nella tua specifica applicazione, facendoti risparmiare tempo prezioso. Ad esempio, la latenza può non essere un problema in un gioco a turni o in un gioco che si può giocare 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, nella maggior parte dei giochi FPS vengono impiegati protocolli proprietari basati su UDP, quindi di seguito discuteremo di questi in modo più dettagliato.

Se decidi di utilizzare TCP, assicurati che sia disabilitato l'algoritmo di Nagle, perché questo algoritmo memorizza 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 Fidler UDP vs. TCP.

Protocollo proprietario

Quindi, vuoi creare un protocollo di trasporto personalizzato ma non sai da dove cominciare? Sei fortunato, perché Glenn Fidler ha scritto due articoli straordinari su questo argomento. In essi troverai molte idee brillanti.

Il primo articolo, Networking for Game Programmers dal 2008, più semplice della seconda, Costruire un Protocollo di Rete per Giochi dal 2016. Ti consiglio di iniziare con qualcosa di più vecchio.

Tieni presente che Glenn Fidler è un grande sostenitore dell'uso di un protocollo personalizzato basato su UDP. Dopo aver letto i suoi articoli, probabilmente condividerai la sua opinione sul fatto che TCP presenta gravi svantaggi nei videogiochi e vorrà implementare il tuo protocollo.

Ma se sei un principiante nel lavoro con le reti, fai un favore a te stesso e usa TCP o una libreria. Per implementare con successo un protocollo di trasporto personalizzato, è necessario prima imparare molte cose.

Librerie di rete

Se hai bisogno di qualcosa di più efficiente di TCP, ma non vuoi preoccuparti di implementare un protocollo tuo e di entrare nei dettagli, puoi utilizzare una libreria di rete. Ce ne sono moltissime:

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 di trasporto: conclusione

In sintesi: ci sono due principali protocolli di trasporto: TCP e UDP. TCP ha molte caratteristiche utili: affidabilità, mantenimento dell'ordine dei pacchetti, e rilevamento degli errori. UDP non ha nulla di tutto ciò, ma TCP ha in natura latenze elevate, inaccettabili per alcuni giochi. Quindi, per garantire latenze basse, si può creare un protocollo personalizzato basato su UDP o utilizzare una libreria che implementa un protocollo di trasporto su UDP e adattata per i videogiochi multiplayer.

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

Ho due consigli:

  • Astrazione massima del 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 protocollo di trasporto personalizzato basato su UDP, puoi iniziare con TCP o librerie che garantiscono affidabilità e poi testare e misurare le prestazioni. Se si verificano problemi e sei sicuro che la causa sia il protocollo di trasporto, allora potrebbe essere il momento di creare un protocollo di trasporto personalizzato.

Per concludere questa parte, ti consiglio di leggere Introduction to Multiplayer Game Programming Il saggio di Bryan Hook, che tratta molte delle tematiche discusse qui.

Protocollo dell'applicazione

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

Lo schema classico prevede che i client inviino al server input o azioni, mentre il server restituisce ai client lo stato attuale del gioco.

Il server invia uno stato non completo, ma filtrato, con le entità vicine al giocatore. Lo fa per tre motivi. Innanzitutto, lo stato completo potrebbe essere troppo grande per una trasmissione ad alta frequenza. In secondo luogo, i clienti sono principalmente interessati ai dati visivi e audio, poiché gran parte della logica di gioco è simulata sul server. Infine, in alcuni giochi, il giocatore non deve conoscere determinate informazioni, come la posizione del nemico dall'altra parte della mappa, altrimenti potrebbe sniffare i pacchetti e sapere esattamente dove muoversi per eliminarlo.

Serializzazione

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

Ci viene subito in mente di utilizzare un formato leggibile dall'uomo, come JSON o XML. Tuttavia, ciò sarebbe completamente inefficace e occuperebbe gran parte della larghezza di banda.

Invece, è consigliabile utilizzare un formato binario, che è molto più compatto. Ciò significa che i pacchetti conterranno solo pochi byte. Qui bisogna tener conto del problema dell'ordine dei byte, che può differire tra computer diversi.

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

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

Una soluzione alternativa potrebbe essere una realizzazione autonoma, che non è particolarmente complessa, soprattutto se nel codice utilizzi un approccio orientato ai dati. Inoltre, ti consentirà di eseguire ottimizzazioni che non sono sempre possibili quando si utilizza una libreria.

Glenn Fiedler ha scritto due articoli sulla serializzazione: Lettura e Scrittura dei Pacchetti e Strategie di Serializzazione.

Compressione

La quantità di dati trasmessi tra client e server è limitata dalla larghezza di banda del canale. La compressione dei dati consente di inviare più informazioni in ogni snapshot, aumentare la frequenza di aggiornamento o semplicemente ridurre i requisiti del canale.

Imballaggio dei Bit

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

Glenn Fiedler spiega come implementare questo nella seconda parte dell'articolo. Lettura e Scrittura dei Pacchetti.

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

Discretizzazione

Discretizzazione è una tecnica di compressione lossy che consiste nell'utilizzare solo un sottoinsieme dei possibili valori per codificare una grandezza. La discretizzazione si realizza più semplicemente 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 sarà quella degli algoritmi di compressione senza perdita.

Ecco, secondo me, tre degli algoritmi più interessanti da conoscere:

  • Codifica di Huffman con codice precalcolato, che è estremamente veloce e può dare buoni risultati. È stato utilizzato per comprimere pacchetti nel motore di rete Quake3.
  • zlib — algoritmo di compressione generico che non aumenta mai la dimensione dei dati. Come puoi vedere qui, è stato utilizzato in molteplici campi di applicazione. Per gli aggiornamenti di stato, potrebbe risultare ridondante. Ma può essere utile se hai bisogno di inviare asset, testi lunghi o rilievi ai clienti dal server.
  • Copia di lunghe serie — probabilmente l'algoritmo di compressione più semplice, ma è molto efficace per alcuni tipi di dati e può essere utilizzato come fase di preelaborazione prima di zlib. È particolarmente adatto per comprimere rilievi composti da tessere o voxel, in cui molti elementi vicini si ripetono.

Compressione Delta

L'ultima metodologia di compressione è la compressione delta. Essa consiste nel trasmettere solo le differenze tra lo stato attuale del gioco e l'ultimo stato ricevuto dal client.

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

Anche Glenn Fiedler l'ha utilizzata nella seconda parte del suo articolo. Compressione Snapshot.

Crittografia

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

  • privacy/confidenzialità: i messaggi possono essere letti solo dal destinatario, e nessun'altra persona che esegue il sniffing della rete potrà leggerli.
  • autenticazione: una persona che desidera impersonare un giocatore deve conoscere la sua chiave.
  • prevenzione della cheating: sarà molto più difficile per i giocatori malintenzionati creare i propri pacchetti per imbrogliare; dovranno replicare lo schema di cifratura e trovare la chiave (che cambia ad ogni connessione).

Consiglio vivamente di utilizzare questa libreria. Propongo di utilizzare libsodium, perché è particolarmente semplice e ha ottimi tutorial. In particolare, il tutorial sul scambio delle chiavi, che consente di generare nuove chiavi ad ogni nuova connessione.

Protocollo dell'applicazione: conclusione

Con questo concludiamo con il protocollo dell'applicazione. Ritengo che la compressione sia assolutamente opzionale e la decisione sul suo utilizzo dipenda solo dal gioco e dalla larghezza di banda richiesta. La crittografia, a mio avviso, è fondamentale, 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. Dopo aver eseguito un input, il giocatore deve attendere l'aggiornamento dello stato del gioco dal server per vedere quale impatto ha avuto sul mondo.

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

Esistono diverse tecniche che possono ridurre l'impatto di questo problema, e nel prossimo paragrafo ne parlerò.

Tecniche di smoothing del ritardo

Tutte le tecniche descritte in questa sezione sono dettagliatamente trattate in una serie Multiplayer ad alta velocità di Gabriel Gambetta. Consiglio 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 direttamente il risultato dell'input, senza attendere una risposta dal server. Questo è chiamato predictive client-side. Tuttavia, quando il client riceve un aggiornamento dal server, deve assicurarsi che la sua previsione fosse corretta. Se non lo è, deve semplicemente aggiornare il proprio stato in base a quanto ricevuto dal server, poiché il server è autorevole. Questa tecnica è stata utilizzata per la prima volta in Quake. Maggiori informazioni sono disponibili nell'articolo Quake Engine code review di Fabien Sanglar [traduzione su Habr].

Il secondo set di tecniche viene utilizzato per lisciare 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, si prendono i due ultimi stati e si mostra la transizione da uno all'altro. La sua limitazione è che comporta un piccolo ritardo, perché il client vede sempre ciò che è accaduto nel passato. L'estrapolazione consiste nel prevedere dove dovrebbero trovarsi attualmente le entità sulla base dell'ultima posizione ricevuta dal client. La sua limitazione è che se l'entità cambia completamente direzione, ci sarà un grande margine di errore tra la previsione e la posizione reale.

L'ultima, più avanzata tecnica, utile solo nei giochi FPS, è la compensazione del lag. Quando si utilizza la compensazione del lag, il server tiene conto dei ritardi del cliente quando spara a un obiettivo. Ad esempio, se un giocatore esegue un colpo alla testa sul proprio schermo, ma in realtà il suo bersaglio si trovava in un altro luogo a causa del ritardo, sarebbe ingiusto negare al giocatore il diritto di ottenere un'uccisione a causa di questo ritardo. Pertanto, il server riavvolge il tempo al momento in cui il giocatore ha sparato per simulare ciò che il giocatore vedeva sul proprio schermo e per verificare la collisione tra il suo proiettile e il bersaglio.

Glenn Fidler (come sempre!) ha scritto nel 2004 un articolo Network Physics (2004), in cui 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 descrive altre tecniche per la sincronizzazione della simulazione fisica.

C'è anche un wiki di Valve con due articoli, Source Multiplayer Networking e Latency Compensating Methods in Client/Server In-game Protocol Design and Optimization , in cui viene trattata la compensazione dei ritardi.

Prevenzione dell'imbroglio

Esistono due tecniche principali per prevenire l'imbroglio.

Primo: complicazione nell'invio di pacchetti dannosi da parte dei cheater. Come accennato in precedenza, un buon modo per implementarlo è la crittografia.

Secondo: il server autoritario deve ricevere solo comandi/inserimenti/azioni. Il client non dovrebbe avere la possibilità di modificare lo stato del server se non attraverso l'invio di input. Il server deve verificare la validità dell'input ogni volta che lo riceve prima di applicarlo.

Logica dell'applicazione: conclusione

Ti consiglio di implementare un modo per simulare grandi ritardi e basse frequenze di aggiornamento, in modo da poter testare il comportamento del tuo gioco in condizioni difficili, anche quando il client e il server sono in esecuzione sullo stesso computer. Questo semplificherà notevolmente l'implementazione delle tecniche di smoothing 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