Come verrebbe implementato un sistema di snapshot dello stato di gioco per i giochi in rete in tempo reale?


12

Voglio creare un semplice gioco multiplayer client-server in tempo reale come progetto per la mia classe di networking.

Ho letto molto sui modelli di rete multiplayer in tempo reale e capisco le relazioni tra il client e il server e le tecniche di compensazione del ritardo.

Quello che voglio fare è qualcosa di simile al modello di rete di Quake 3: in sostanza, il server memorizza un'istantanea dell'intero stato del gioco; alla ricezione di input dai client, il server crea una nuova istantanea che riflette le modifiche. Quindi, calcola le differenze tra la nuova istantanea e l'ultima e le invia ai client, in modo che possano essere sincronizzate.

Questo approccio mi sembra molto solido: se il client e il server hanno una connessione stabile, verrà inviata solo la minima quantità necessaria di dati per mantenerli sincronizzati. Se il client non è sincronizzato, è possibile richiedere anche un'istantanea completa.

Tuttavia, non riesco a trovare un buon modo per implementare il sistema di istantanee. Trovo davvero difficile abbandonare l'architettura di programmazione per giocatore singolo e pensare a come memorizzare lo stato del gioco in modo tale che:

  • Tutti i dati sono separati dalla logica
  • Le differenze possono essere calcolate tra un'istantanea degli stati del gioco
  • Le entità di gioco possono ancora essere facilmente manipolate tramite codice

Come viene implementata una classe di istantanee ? Come vengono archiviate le entità e i loro dati? Ogni entità client ha un ID che corrisponde a un ID sul server?

Come vengono calcolate le differenze di snapshot?

In generale: come verrebbe implementato un sistema di snapshot dello stato di gioco?


4
+1. Questo è un po 'troppo ampio per una singola domanda, ma IMO è un argomento interessante che può essere trattato approssimativamente in una risposta.
Kromster,

Perché non memorizzi solo 1 Snapshot (il mondo reale), salva tutte le modifiche in entrata in questo stato-mondo regolare E memorizzi le modifiche in un elenco o qualcosa del genere. Quindi, quando è il momento di inviare le modifiche a tutti i client, basta inviare il contenuto dell'elenco a tutti loro e cancellare l'elenco, iniziare da zero (modifiche). Forse questo non è buono come memorizzare 2 snapshot ma con questo approccio non devi preoccuparti degli algoritmi su come velocizzare 2 snapshot diff.
tkausl,

Hai letto questo: fabiensanglard.net/quake3/network.php - la revisione del modello di rete di Quake 3 include la discussione sull'attuazione.
Steven,

Che tipo di gioco stanno tentando di costruire? La configurazione della rete dipende fortemente dal tipo di gioco che stai realizzando. Un RTS non si comporta come un FPS in termini di rete.
AturSams,

Risposte:


3

È possibile calcolare il delta dell'istantanea (passa al precedente stato sincronizzato) mantenendo due istanze di istantanee: quella corrente e l'ultima sincronizzata.

Quando arriva l'input client, si modifica l'istantanea corrente. Quindi, quando è il momento di inviare delta ai client, si calcola l'ultima istantanea sincronizzata con un campo per campo (ricorsivamente) corrente e si calcola e si serializza il delta. Per la serializzazione è possibile assegnare un ID univoco a ciascun campo nell'ambito della sua classe (al contrario dell'ambito dello stato globale). Il client e il server devono condividere la stessa struttura di dati per lo stato globale, in modo che il client comprenda a cosa viene applicato un determinato ID.

Quindi, quando viene calcolato il delta, clonare lo stato corrente e renderlo l'ultimo sincronizzato, quindi ora si ha lo stato attuale e l'ultimo sincronizzati identici ma istanze diverse in modo da poter modificare lo stato corrente e non influire sull'altro.

Questo approccio può essere più semplice da implementare, in particolare con l'aiuto della riflessione (se si dispone di un tale lusso), ma può essere lento, anche se si opta per ottimizzare la parte della riflessione (creando lo schema dei dati per memorizzare nella cache la maggior parte delle chiamate di riflessione). Principalmente perché è necessario confrontare due copie di stato potenzialmente di grandi dimensioni. Ovviamente dipende da come implementate il confronto e la vostra lingua. Può essere veloce in C ++ con un comparatore hardcoded ma non così flessibile: qualsiasi cambiamento nella struttura dello stato globale richiede la modifica di questo comparatore e questi cambiamenti sono così frequenti nelle fasi iniziali del progetto.

Un altro approccio è usare bandiere sporche. Ogni volta che arriva l'input del client, lo applichi alla tua singola copia dello stato globale e contrassegna i campi corrispondenti come sporchi. Quindi, quando è il momento di sincronizzare i client, serializzare i campi sporchi (in modo ricorsivo) utilizzando gli stessi ID univoci. L'inconveniente (minore) è che a volte si inviano più dati di quanto strettamente richiesto: ad es. int field1Inizialmente 0, quindi assegnato 1 (e contrassegnato come sporco) e successivamente assegnato di nuovo 0 (ma rimane sporco). Il vantaggio è che avere un'enorme struttura gerarchica di dati non è necessario analizzarlo completamente per calcolare il delta, ma solo percorsi sporchi.

In generale, questo compito può essere piuttosto complicato, dipende da quanto flessibile dovrebbe essere la soluzione finale. Ad esempio Unity3D 5 (in arrivo) utilizzerà gli attributi per specificare i dati che dovrebbero essere sincronizzati automaticamente con i client (approccio molto flessibile, non è necessario fare altro che aggiungere un attributo ai campi) e quindi generare il codice come fase post-build. Maggiori dettagli qui.


2

Per prima cosa devi sapere come rappresentare i tuoi dati rilevanti in modo conforme al protocollo. Questo dipende dai dati rilevanti per il gioco. Userò un gioco RTS come esempio.

Ai fini del collegamento in rete, tutte le entità nel gioco sono elencate (ad esempio pickup, unità, edifici, risorse naturali, distruttibili).

I giocatori devono avere i dati rilevanti per loro (ad esempio tutte le unità visibili):

  • Sono vivi o morti?
  • Di che tipo sono?
  • Quanta salute hanno lasciato?
  • Posizione attuale, rotazione, velocità (velocità + direzione), percorso nel prossimo futuro ...
  • Attività: attaccare, camminare, costruire, riparare, guarire, ecc ...
  • effetti di stato buff / debuff
  • e forse altre statistiche come mana, scudi e cosa no?

Inizialmente il giocatore deve ricevere lo stato completo prima di poter entrare nel gioco (o in alternativa tutte le informazioni rilevanti per quel giocatore).

Ogni unità ha un ID intero. Gli attributi sono enumerati e quindi hanno anche identificatori integrali. Gli ID unità non devono essere lunghi 32 bit (potrebbe essere se non siamo frugali). Potrebbe benissimo essere 20 bit (lasciando 10 bit per gli attributi). L'ID dell'unità deve essere univoco, potrebbe benissimo essere assegnato da un segnalino quando l'unità viene istanziata e / o aggiunta al mondo di gioco (edifici e risorse sono considerati un'unità immobile e alle risorse potrebbe essere assegnato un ID quando la mappa è caricato).

Il server memorizza lo stato globale corrente. Lo stato aggiornato più recente di ogni giocatore è rappresentato da un puntatore a una listdelle modifiche recenti (tutte le modifiche dopo che il puntatore non sono state ancora inviate a quel giocatore). Le modifiche vengono aggiunte al listmomento in cui si verificano. Una volta terminato l'invio dell'ultimo aggiornamento, il server può iniziare a scorrere l'elenco: il server sposta il puntatore del giocatore lungo l'elenco alla sua coda, raccogliendo tutte le modifiche lungo il percorso e inserendole in un buffer che verrà inviato a il giocatore (ovvero il formato del protocollo può essere qualcosa del genere: unit_id; attr_id; new_value) Anche le nuove unità sono considerate modifiche e vengono inviate con tutti i loro valori di attributo ai giocatori riceventi.

Se non stai usando una lingua con un Garbage Collector, dovrai impostare un puntatore pigro che rimarrà indietro e raggiungerà il puntatore del giocatore più obsoleto nell'elenco, liberando oggetti lungo il percorso. Puoi ricordare quale giocatore è il più obsoleto all'interno di un heap prioritario o semplicemente iterare e liberare fino a quando il puntatore pigro è uguale (cioè punta allo stesso oggetto di uno dei puntatori dei giocatori).

Alcune domande che non hai sollevato e penso che siano interessanti sono:

  1. I clienti dovrebbero ricevere un'istantanea con tutti i dati in primo luogo? Che dire degli oggetti al di fuori del loro campo visivo? Che dire della nebbia di guerra nei giochi RTS? Se invii tutti i dati, il client potrebbe essere violato per visualizzare dati che non dovrebbero essere disponibili per il giocatore (a seconda delle altre misure di sicurezza che prendi). Se invii solo dati rilevanti, il problema è risolto.
  2. Quando è fondamentale inviare le modifiche anziché inviare tutte le informazioni? Considerando la larghezza di banda disponibile sulle macchine moderne, otteniamo qualcosa dall'invio di un "delta" anziché dall'invio di tutte le informazioni, in caso affermativo quando?
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.