Come gestire netcode?


10

Sono interessato a valutare i diversi modi in cui il netcode può "agganciarsi" a un motore di gioco. Sto progettando un gioco multiplayer ora, e finora ho deciso che devo (per lo meno) avere un thread separato per gestire i socket di rete, distinto dal resto del motore che gestisce il loop grafico e gli script.

Avevo un modo potenziale per creare un gioco in rete interamente a thread singolo, ovvero quello di farlo controllare alla rete dopo aver eseguito il rendering di ogni fotogramma, utilizzando socket non bloccanti. Tuttavia, ciò non è chiaramente ottimale poiché il tempo impiegato per il rendering di un frame viene aggiunto al ritardo della rete: i messaggi che arrivano sulla rete devono attendere fino al completamento del rendering del frame corrente (e della logica di gioco). Ma, almeno in questo modo, il gameplay rimarrebbe comunque fluido, più o meno.

Avere un thread separato per la rete consente al gioco di essere completamente reattivo alla rete, ad esempio può inviare immediatamente un pacchetto ACK alla ricezione di un aggiornamento di stato dal server. Ma sono un po 'confuso sul modo migliore di comunicare tra il codice di gioco e il codice di rete. Il thread di rete spingerà il pacchetto ricevuto in una coda e il thread del gioco leggerà dalla coda al momento opportuno durante il suo ciclo, quindi non ci siamo liberati di questo ritardo massimo di un fotogramma.

Inoltre, sembra che vorrei che il thread che gestisce l'invio dei pacchetti fosse separato da quello che sta controllando la presenza di pacchetti che scendono dalla pipe, perché non sarebbe in grado di inviarne uno mentre è nel mezzo di controllando se ci sono messaggi in arrivo. Sto pensando alla funzionalità diselect o simili.

Immagino che la mia domanda sia: qual è il modo migliore per progettare il gioco per la migliore reattività di rete? Chiaramente il client dovrebbe inviare al server l'input dell'utente il più presto possibile, in modo che il codice net-send arrivi immediatamente dopo il ciclo di elaborazione degli eventi, entrambi all'interno del loop di gioco. Questo ha senso?

Risposte:


13

Ignora la reattività. Su una LAN, il ping è insignificante. Su Internet, il viaggio di andata e ritorno di 60-100 ms è una benedizione. Prega gli dei di ritardo che non ottieni picchi di> 3K. Il tuo software dovrebbe funzionare con un numero molto basso di aggiornamenti / sec perché questo sia un problema. Se spari per 25 aggiornamenti / sec, hai un tempo massimo di 40 ms tra quando ricevi un pacchetto e agisci su di esso. E questo è il caso a thread singolo ...

Progetta il tuo sistema per flessibilità e correttezza. Ecco la mia idea su come collegare un sottosistema di rete al codice di gioco: Messaggi. La soluzione a molti problemi può essere "messaggistica". Penso che una volta la messaggistica abbia curato il cancro nei topi di laboratorio. La messaggistica mi fa risparmiare $ 200 o più sulla mia assicurazione auto. Ma seriamente, la messaggistica è probabilmente il modo migliore per collegare qualsiasi sottosistema al codice di gioco mantenendo comunque due sottosistemi indipendenti.

Utilizzare la messaggistica per qualsiasi comunicazione tra il sottosistema di rete e il motore di gioco e, tra l'altro, tra due sottosistemi. La messaggistica tra sottosistemi può essere semplice come un BLOB di dati passati dal puntatore usando std :: list.

Basta avere una coda di messaggi in uscita e un riferimento al motore di gioco nel sottosistema di rete. Il gioco può scaricare i messaggi che desidera vengano inviati nella coda in uscita e averli inviati automaticamente, o forse quando viene chiamata una funzione come "flushMessages ()". Se il motore di gioco aveva una coda di messaggi condivisa di grandi dimensioni, tutti i sottosistemi che dovevano inviare messaggi (logica, intelligenza artificiale, fisica, rete, ecc.) Possono scaricare tutti i messaggi al suo interno in cui il ciclo di gioco principale può quindi leggere tutti i messaggi e quindi agire di conseguenza.

Direi che far funzionare i socket su un altro thread va bene, anche se non è necessario. L'unico problema con questo progetto è che è generalmente asincrono (non si sa con precisione quando vengono inviati i pacchetti) e che può rendere difficile il debug e far apparire / scomparire casualmente i problemi relativi al timing. Tuttavia, se fatto correttamente, nessuno di questi dovrebbe essere un problema.

Da un livello superiore, direi una rete separata dal motore di gioco stesso. Il motore di gioco non si preoccupa di socket o buffer, si preoccupa degli eventi. Gli eventi sono cose come "Il giocatore X ha sparato un colpo" "Un'esplosione al gioco T è avvenuta". Questi possono essere interpretati direttamente dal motore di gioco. Non importa da dove vengano generati (uno script, l'azione di un cliente, un giocatore AI, ecc.).

Se trattate il vostro sottosistema di rete come un mezzo per inviare / ricevere eventi, otterrete una serie di vantaggi rispetto alla semplice chiamata di recv () su un socket.

È possibile ottimizzare la larghezza di banda, ad esempio, prendendo 50 piccoli messaggi (1-32 byte di lunghezza) e fare in modo che il sottosistema di rete li comprimi in un unico pacchetto di grandi dimensioni e lo invii. Forse potrebbe comprimerli prima di inviarli se fosse un grosso problema. Dall'altro lato, il codice potrebbe decomprimere / decomprimere il pacchetto di grandi dimensioni in 50 eventi discreti di nuovo affinché il motore di gioco possa leggere. Tutto ciò può avvenire in modo trasparente.

Altre cose interessanti includono la modalità di gioco per giocatore singolo che riutilizza il codice di rete con un client puro + un server puro in esecuzione sullo stesso computer che comunica tramite messaggistica nello spazio di memoria condivisa. Quindi, se il tuo gioco per giocatore singolo funziona correttamente, funzionerebbe anche un client remoto (cioè un vero multiplayer). Inoltre, ti costringe a considerare in anticipo quali dati sono necessari al cliente poiché il tuo gioco per giocatore singolo sembrerebbe giusto o totalmente sbagliato. Mescola e combina, esegui un server ed essere un client in una partita multiplayer: tutto funziona altrettanto facilmente.


Hai citato usando un semplice std :: list o alcuni di questi per passare messaggi. Questo può essere un argomento per StackOverflow, ma è vero che tutti i thread condividono lo stesso spazio di indirizzi e, purché impedisca a più thread di avvitarsi contemporaneamente con la memoria appartenente alla mia coda, dovrei andare bene? Posso semplicemente allocare i dati per la coda sull'heap come al solito, e usare solo alcuni mutex in esso?
Steven Lu,

Sì, è corretto. Un mutex che protegge tutte le chiamate a std :: list.
Patrick B

Grazie per la risposta! Finora ho fatto molti progressi con le mie routine di threading. È davvero una bella sensazione avere il mio motore di gioco!
Steven Lu,

4
Quella sensazione svanirà. I grandi ottoni che ottieni, però, restano con te.
ChrisE,

@StevenLu Un po '[estremamente] in ritardo, ma voglio sottolineare che impedire ai thread di avvitarsi con la memoria contemporaneamente può essere estremamente difficile, a seconda di come si tenta di farlo e di quanto si desidera essere efficienti. Se lo facessi oggi, ti indirizzerei a una delle molte eccellenti implementazioni di code simultanee open source, quindi non è necessario reinventare una ruota complicata.
Finanzia la causa di Monica il

4

Devo (almeno) avere un thread separato per gestire i socket di rete

No non lo fai.

Avevo un modo potenziale per creare un gioco in rete interamente a thread singolo, ovvero quello di farlo controllare alla rete dopo aver eseguito il rendering di ogni fotogramma, utilizzando socket non bloccanti. Tuttavia, ciò non è chiaramente ottimale perché il tempo impiegato per il rendering di un frame viene aggiunto al ritardo di rete:

Non importa necessariamente. Quando si aggiorna la tua logica? È inutile ottenere dati dalla rete se non si riesce ancora a farci nulla. Allo stesso modo, non ha senso rispondere se non hai ancora niente da dire.

ad esempio può inviare immediatamente un pacchetto ACK al ricevimento di un aggiornamento di stato dal server.

Se il tuo gioco è così veloce che attendere il rendering del fotogramma successivo è un ritardo significativo, allora invierà abbastanza dati da non dover inviare pacchetti ACK separati - includi solo i valori ACK all'interno dei tuoi dati normali payload, se ne hai bisogno.

Per la maggior parte dei giochi in rete, è perfettamente possibile avere un loop di gioco come questo:

while 1:
    read_network_messages()
    read_local_input()
    update_world()
    send_network_updates()
    render_world()

Puoi disaccoppiare gli aggiornamenti dal rendering, che è altamente raccomandato, ma tutto il resto può rimanere così semplice a meno che tu non abbia un'esigenza specifica. Esattamente che tipo di gioco stai realizzando?


7
Il disaccoppiamento dell'aggiornamento dal rendering non è solo altamente raccomandato, è necessario se si desidera un motore non di merda.
Attaccando

La maggior parte delle persone non produce motori, e probabilmente la maggior parte dei giochi non disaccoppia ancora i due. Nella maggior parte dei casi, accontentarsi di passare un valore di tempo trascorso alla funzione di aggiornamento funziona in modo accettabile.
Kylotan,

2

Tuttavia, ciò non è chiaramente ottimale poiché il tempo impiegato per il rendering di un frame viene aggiunto al ritardo della rete: i messaggi che arrivano sulla rete devono attendere fino al completamento del rendering del frame corrente (e della logica di gioco).

Questo non è affatto vero. Il messaggio passa attraverso la rete mentre il ricevitore esegue il rendering del frame corrente. Il ritardo di rete è bloccato su un numero intero di frame sul lato client; sì, ma se il client ha così pochi FPS che questo è un grosso problema, allora hanno problemi più grandi.


0

Le comunicazioni di rete dovrebbero essere raggruppate. Dovresti cercare un singolo pacchetto inviato ad ogni segno di spunta del gioco (che è spesso quando viene visualizzato un frame ma in realtà dovrebbe essere indipendente).

Le tue entità di gioco parlano con il sottosistema di rete (NSS). L'NSS raggruppa messaggi, ACK, ecc. E invia alcuni (si spera uno) pacchetti UDP di dimensioni ottimali (~ 1500 byte in genere). L'NSS emula pacchetti, canali, priorità, rinvia, ecc. Mentre invia solo pacchetti UDP singoli.

Leggi la gaffer sul tutorial dei giochi o usa semplicemente ENet che implementa molte delle idee di Glenn Fiedler.

Oppure puoi semplicemente usare TCP se il tuo gioco non ha bisogno di reazioni di contrazione. Quindi tutti i problemi di batch, rinvio e ACK scompaiono. Tuttavia, vorresti comunque un NSS per gestire la larghezza di banda e i canali.


0

Non "ignorare completamente la reattività". C'è poco da guadagnare dall'aggiunta di un'altra latenza di 40 ms a pacchetti già ritardati. Se stai aggiungendo un paio di frame (a 60fps), stai ritardando l'elaborazione di una posizione aggiorna un altro paio di frame. È meglio accettare i pacchetti rapidamente ed elaborarli rapidamente in modo da migliorare l'accuratezza della simulazione.

Ho avuto un grande successo nell'ottimizzare la larghezza di banda pensando alle informazioni minime sullo stato necessarie per rappresentare ciò che è visibile sullo schermo. Quindi guardare ogni bit di dati e scegliere un modello per esso. Le informazioni sulla posizione possono essere espresse come valori delta nel tempo. Puoi usare i tuoi modelli statistici per questo e passare anni a debug, oppure puoi usare una libreria per aiutarti. Preferisco usare il modello a virgola mobile di questa libreria DataBlock_Predict_Float Rende davvero facile l'ottimizzazione della larghezza di banda utilizzata per il grafico della scena di gioco.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.