Come progettare un sistema di riproduzione


75

Quindi, come dovrei progettare un sistema di riproduzione?

Potresti conoscerlo da alcuni giochi come Warcraft 3 o Starcraft in cui puoi rivedere il gioco dopo che è già stato giocato.

Si finisce con un file di riproduzione relativamente piccolo. Quindi le mie domande sono:

  • Come salvare i dati? (formato personalizzato?) (dimensioni file ridotte)
  • Cosa deve essere salvato?
  • Come renderlo generico in modo che possa essere utilizzato in altri giochi per registrare un periodo di tempo (e non una corrispondenza completa per esempio)?
  • Rendere possibile l'avanzamento e il riavvolgimento (WC3 non è riuscito a riavvolgere per quanto ricordo)

3
Sebbene le risposte di seguito forniscano molte preziose informazioni, volevo solo sottolineare l'importanza di sviluppare il tuo gioco / motore in modo altamente deterministico ( en.wikipedia.org/wiki/Deterministic_algorithm ), in quanto è essenziale per raggiungere il tuo obiettivo.
Ari Patrick,

2
Nota anche che i motori fisici non sono deterministici (Havok afferma che lo è ...), quindi la soluzione per memorizzare solo input e timestamp produrrà risultati diversi ogni volta che il tuo gioco userà la fisica.
Samaursa,

5
La maggior parte dei motori fisici sono deterministici fintanto che usi un timestep fisso, che dovresti comunque fare. Sarei molto sorpreso se Havok non lo fosse. Il non determinismo è abbastanza difficile da trovare sui computer ...

4
Deterministico significa stessi input = stessi output. Se hai float su una piattaforma e raddoppia su un'altra (per esempio), o disabiliti intenzionalmente l'implementazione standard in virgola mobile IEEE, ciò significa che non stai eseguendo gli stessi input, non che non sia deterministico.

3
Sono io o questa domanda ottiene una taglia ogni due settimane?
Il comunista Duck l'

Risposte:


39

Questo eccellente articolo tratta molti problemi: http://www.gamasutra.com/view/feature/2029/developing_your_own_replay_system.php

Alcune cose che l'articolo menziona e fa bene:

  • il tuo gioco deve essere deterministico.
  • registra lo stato iniziale dei sistemi di gioco sul primo fotogramma e solo l'input del giocatore durante il gioco.
  • quantizzare gli input per un numero inferiore di bit. Vale a dire. rappresentano i galleggianti all'interno di vari intervalli (ad es. [0, 1] o [-1, 1] entro un numero inferiore di bit. Anche gli input quantizzati devono essere ottenuti durante il gioco reale.
  • utilizzare un singolo bit per determinare se un flusso di input ha nuovi dati. Poiché alcuni flussi non cambieranno frequentemente, questo sfrutta la coerenza temporale negli input.

Un modo per migliorare ulteriormente il rapporto di compressione per la maggior parte dei casi sarebbe quello di disaccoppiare tutti i flussi di input e codificarli completamente in modo indipendente. Questa sarà una vittoria sulla tecnica di codifica delta se codifichi la tua corsa in 8 bit e la corsa stessa supera gli 8 frame (molto probabilmente a meno che il tuo gioco non sia un vero schiacciatore di pulsanti). Ho usato questa tecnica in un gioco di corse per comprimere 8 minuti di input da 2 giocatori mentre correvo su una pista fino a poche centinaia di byte.

In termini di riutilizzabilità di un tale sistema, ho fatto in modo che il sistema di riproduzione gestisse flussi di input generici, ma anche fornendo hook per consentire la logica specifica del gioco al marshalling di input tastiera / gamepad / mouse a questi flussi.

Se si desidera un riavvolgimento rapido o ricerche casuali, è possibile salvare un checkpoint (l'intero gamestate) ogni N frame. N dovrebbe essere scelto per ridurre al minimo le dimensioni del file di riproduzione e anche assicurarsi che il tempo che il giocatore deve attendere sia ragionevole mentre lo stato viene riprodotto nel punto scelto. Un modo per aggirare questo è quello di garantire che le ricerche casuali possano essere effettuate solo in queste posizioni esatte del checkpoint. Il riavvolgimento è una questione di impostazione dello stato del gioco sul checkpoint immediatamente prima del frame in questione, quindi di riprodurre nuovamente gli input fino ad arrivare al frame corrente. Tuttavia, se N è troppo grande, potresti ottenere un intoppo ogni pochi fotogrammi. Un modo per smussare questi intoppi è pre-cache in modo asincrono dei frame tra i 2 checkpoint precedenti mentre si sta riproducendo un frame nella cache dall'attuale area del checkpoint.


se è coinvolto RNG, includere i risultati di tale RNG nei flussi
maniaco del cricchetto

1
@ratchet maniaco: con l'uso deterministico del PRNG puoi cavartela conservando solo il suo seme durante i checkpoint.
NonNumeric

22

Oltre alla soluzione "assicurati che i tasti siano riproducibili", che può essere sorprendentemente difficile, puoi semplicemente registrare l'intero stato del gioco su ogni fotogramma. Con un po 'di compressione intelligente puoi comprimerlo in modo significativo. Ecco come Braid gestisce il suo codice di riavvolgimento temporale e funziona abbastanza bene.

Poiché avrai comunque bisogno di checkpoint per il riavvolgimento, potresti provare a implementarlo in modo semplice prima di complicare le cose.


2
+1 Con una compressione intelligente puoi davvero ridurre la quantità di dati che devi archiviare (ad esempio, non memorizzare lo stato se non è cambiato rispetto all'ultimo stato che hai archiviato per l'oggetto corrente) . Ho già provato questo con la fisica e funziona davvero bene. Se non hai fisica e non vuoi riavvolgere il gioco completo, sceglierei la soluzione di Joe semplicemente perché produrrà i file più piccoli possibili nel qual caso se vuoi anche riavvolgere, puoi memorizzare solo gli ultimi nsecondi di il gioco.
Samaursa,

@Samaursa - Se usi una libreria di compressione standard (es. Gzip) otterrai la stessa (probabilmente migliore) compressione senza la necessità di fare manualmente cose come verificare se lo stato è cambiato o no.
Giustino,

2
@Kragen: non proprio vero. Le librerie di compressione standard sono certamente buone, ma spesso non saranno in grado di sfruttare le conoscenze specifiche del dominio. Se puoi aiutarli un po ', mettendo i dati simili adiacenti e rimuovendo le cose che in realtà non sono cambiate, puoi ridurre notevolmente le cose.
ZorbaTHut

1
@ZorbaTHut In teoria sì, ma in pratica ne vale davvero la pena?
Giustino,

4
Il valore dello sforzo dipende interamente dalla quantità di dati che hai. Se hai un RTS con centinaia o migliaia di unità, probabilmente è importante. Se è necessario memorizzare i replay in memoria come Braid, probabilmente è importante.

21

È possibile visualizzare il sistema come se fosse composto da una serie di stati e funzioni, in cui una funzione f[j]con input x[j]modifica lo stato del sistema s[j]in stato s[j+1], in questo modo:

s[j+1] = f[j](s[j], x[j])

Uno stato è la spiegazione di tutto il tuo mondo. Le posizioni del giocatore, la posizione del nemico, il punteggio, le munizioni rimanenti, ecc. Tutto il necessario per disegnare una cornice del gioco.

Una funzione è tutto ciò che può influenzare il mondo. Un cambio di frame, un tasto premuto, un pacchetto di rete.

L'input sono i dati acquisiti dalla funzione. Un cambio di fotogramma può richiedere la quantità di tempo trascorsa dall'ultimo fotogramma passato, la pressione del tasto può includere l'effettivo tasto premuto, nonché se il tasto Maiusc è stato premuto o meno.

Per motivi di questa spiegazione, farò le seguenti ipotesi:

Assunzione 1:

La quantità di stati per una determinata corsa del gioco è molto più grande della quantità di funzioni. Probabilmente hai centinaia di migliaia di stati, ma solo una dozzina di funzioni (cambio di frame, keypress, pacchetto di rete, ecc.). Naturalmente, la quantità di input deve essere uguale alla quantità di stati meno uno.

Assunzione 2:

Il costo spaziale (memoria, disco) della memorizzazione di un singolo stato è molto maggiore di quello della memorizzazione di una funzione e del suo input.

Assunzione 3:

Il costo temporale (tempo) della presentazione di uno stato è simile, o solo uno o due ordini di grandezza più lunghi di quelli del calcolo di una funzione su uno stato.

A seconda dei requisiti del tuo sistema di riproduzione, esistono diversi modi per implementare un sistema di riproduzione, quindi possiamo iniziare con quello più semplice. Farò anche un piccolo esempio usando il gioco degli scacchi, registrato su pezzi di carta.

Metodo 1:

Store s[0]...s[n]. Questo è molto semplice, molto semplice. A causa del presupposto 2, il costo spaziale di questo è piuttosto elevato.

Per gli scacchi, questo sarebbe realizzato disegnando l'intera scacchiera per ogni mossa.

Metodo 2:

Se hai solo bisogno di replay in avanti, puoi semplicemente archiviare s[0]e quindi memorizzare f[0]...f[n-1](ricorda, questo è solo il nome dell'id della funzione) e x[0]...x[n-1](qual è stato l'input per ciascuna di queste funzioni). Per riprodurre, devi semplicemente iniziare s[0]e calcolare

s[1] = f[0](s[0], x[0])
s[2] = f[1](s[1], x[1])

e così via...

Voglio fare una piccola annotazione qui. Diversi altri commentatori hanno affermato che il gioco "deve essere deterministico". Chiunque dica che ha bisogno di prendere di nuovo Computer Science 101, perché a meno che il gioco non debba essere eseguito su computer quantistici, TUTTI I PROGRAMMI INFORMATICI SONO DETERMINISTICI¹. Questo è ciò che rende i computer così fantastici.

Tuttavia, poiché il tuo programma dipende molto probabilmente da programmi esterni, che vanno dalle librerie all'implementazione effettiva della CPU, assicurarsi che le tue funzioni si comportino allo stesso modo tra le piattaforme potrebbe essere piuttosto difficile.

Se si utilizzano numeri pseudo-casuali, è possibile memorizzare i numeri generati come parte dell'input xoppure memorizzare lo stato della funzione prng come parte del proprio stato se la sua implementazione come parte della funzione f.

Per gli scacchi, questo sarebbe realizzato disegnando la scacchiera iniziale (che è nota) e quindi descrivendo ogni mossa dicendo quale pezzo è andato dove. A proposito, in questo modo lo fanno davvero.

Metodo 3:

Ora, molto probabilmente vorrai essere in grado di cercare nel tuo replay. Cioè, calcolare s[n]per un arbitrario n. Utilizzando il metodo 2, è necessario calcolare s[0]...s[n-1]prima di poter calcolare s[n], che, secondo l'ipotesi 2, potrebbe essere piuttosto lento.

Per implementarlo, il metodo 3 è una generalizzazione dei metodi 1 e 2: store f[0]...f[n-1]e x[0]...x[n-1]proprio come il metodo 2, ma anche store s[j], per tutti j % Q == 0per una data costante Q. In termini più semplici, ciò significa che memorizzi un segnalibro in uno di tutti gli Qstati. Ad esempio, per Q == 100, si memorizzas[0], s[100], s[200]...

Per calcolare s[n]un arbitrario n, devi prima caricare quello precedentemente memorizzato s[floor(n/Q)], quindi calcolare tutte le funzioni da floor(n/Q)a n. Al massimo, calcolerai le Qfunzioni. Valori più piccoli di Qsono più veloci da calcolare ma consumano molto più spazio, mentre valori più grandi di Qconsumano meno spazio, ma impiegano più tempo per il calcolo.

Il metodo 3 con Q==1è uguale al metodo 1, mentre il metodo 3 con Q==infè uguale al metodo 2.

Per gli scacchi, questo sarebbe realizzato disegnando ogni mossa, nonché una su ogni 10 tavole (per Q==10).

Metodo 4:

Se si vuole invertire la riproduzione, è possibile effettuare una piccola variazione del metodo di 3. Si supponga Q==100, e si vuole calcolare s[150]attraverso s[90]in senso inverso. Con il metodo 3 non modificato, sarà necessario eseguire 50 calcoli per ottenere, s[150]quindi altri 49 calcoli per ottenere s[149]e così via. Ma poiché hai già calcolato s[149]per ottenere s[150], puoi creare una cache con s[100]...s[150]quando esegui il calcolo s[150]per la prima volta, e quindi sei già s[149]nella cache quando devi visualizzarla.

Hai solo bisogno di rigenerare la cache ogni volta che devi calcolare s[j], j==(k*Q)-1per ogni dato k. Questa volta, aumentando si Qavranno dimensioni inferiori (solo per la cache), ma tempi più lunghi (solo per ricreare la cache). È Qpossibile calcolare un valore ottimale per se si conoscono le dimensioni e i tempi richiesti per calcolare stati e funzioni.

Per gli scacchi, questo sarebbe realizzato disegnando ogni mossa, oltre a una su ogni 10 tavole (per Q==10), ma avrebbe anche bisogno di disegnare in un pezzo di carta separato, le ultime 10 tavole che hai calcolato.

Metodo 5:

Se gli stati consumano semplicemente troppo spazio o le funzioni consumano troppo tempo, è possibile creare una soluzione che implementa effettivamente (non falsa) la riproduzione inversa. Per fare ciò, è necessario creare funzioni inverse per ciascuna delle funzioni disponibili. Tuttavia, ciò richiede che ciascuna delle tue funzioni sia un'iniezione. Se questo è fattibile, quindi per f'indicare l'inverso della funzione f, il calcolo s[j-1]è semplice come

s[j-1] = f'[j-1](s[j], x[j-1])

Si noti che qui, la funzione e l'input sono entrambi j-1, no j. Questa stessa funzione e input sarebbero quelli che avresti usato se stessi calcolando

s[j] = f[j-1](s[j-1], x[j-1])

Creare l'inverso di queste funzioni è la parte difficile. Tuttavia, di solito non è possibile, poiché alcuni dati di stato vengono solitamente persi dopo ogni funzione in un gioco.

Questo metodo, così com'è, può invertire il calcolo s[j-1], ma solo se lo hai s[j]. Ciò significa che puoi solo guardare il replay all'indietro, a partire dal punto in cui hai deciso di ripeterlo all'indietro. Se si desidera riprodurre all'indietro da un punto arbitrario, è necessario mescolarlo con il metodo 4.

Per gli scacchi, questo non può essere implementato, poiché con una determinata tavola e la mossa precedente, puoi sapere quale pezzo è stato spostato, ma non da dove si è mosso.

Metodo 6:

Infine, se non puoi garantire che tutte le tue funzioni siano iniezioni, puoi fare un piccolo trucco per farlo. Invece di fare in modo che ogni funzione restituisca solo un nuovo stato, puoi anche far sì che restituisca i dati scartati, in questo modo:

s[j+1], r[j] = f[j](s[j], x[j])

Dove r[j]sono i dati scartati. E quindi crea le tue funzioni inverse in modo che prendano i dati scartati, in questo modo:

s[j] = f'[j](s[j+1], x[j], r[j])

Oltre a f[j]e x[j], è necessario anche memorizzare r[j]per ciascuna funzione. Ancora una volta, se si desidera poter cercare, è necessario memorizzare i segnalibri, ad esempio con il metodo 4.

Per gli scacchi, questo sarebbe lo stesso del metodo 2, ma a differenza del metodo 2, che dice solo quale pezzo va dove, devi anche conservare da dove proviene ogni pezzo.

Implementazione:

Dal momento che questo funziona per tutti i tipi di stati, con tutti i tipi di funzioni, per un gioco specifico, puoi fare diversi presupposti, che renderanno più semplice l'implementazione. In realtà, se implementi il ​​metodo 6 con l'intero stato del gioco, non solo sarai in grado di riprodurre i dati, ma anche tornare indietro nel tempo e riprendere a giocare da un dato momento. Sarebbe fantastico.

Invece di memorizzare tutto lo stato del gioco, puoi semplicemente archiviare il minimo indispensabile per disegnare un determinato stato e serializzare questi dati per un periodo di tempo fisso. I tuoi stati saranno queste serializzazioni e il tuo input sarà ora la differenza tra due serializzazioni. La chiave per far funzionare tutto questo è che la serializzazione dovrebbe cambiare poco se anche lo stato mondiale cambia poco. Questa differenza è completamente inversibile, quindi è molto possibile implementare il metodo 5 con i segnalibri.

L'ho visto implementato in alcuni giochi importanti, principalmente per la riproduzione istantanea di dati recenti quando si verifica un evento (un frammento in fps o un punteggio nei giochi sportivi).

Spero che questa spiegazione non sia stata troppo noiosa.

¹ Ciò non significa che alcuni programmi si comportino come se non fossero deterministici (come MS Windows ^^). Ora seriamente, se riesci a fare un programma non deterministico su un computer deterministico, puoi essere abbastanza sicuro che vincerai contemporaneamente la medaglia Fields, il premio Turing e probabilmente anche un Oscar e Grammy per tutto ciò che vale.


In "TUTTI I PROGRAMMI DEI COMPUTER SONO DETERMINISTICI", stai trascurando di considerare i programmi che si basano sul threading. Mentre il threading viene utilizzato principalmente per caricare risorse o per separare il loop di rendering, ci sono eccezioni a questo, e a quel punto potresti non essere più in grado di rivendicare il vero determinismo, a meno che tu non sia adeguatamente severo nel far valere il determinismo. I meccanismi di blocco da soli non saranno sufficienti. Non saresti in grado di condividere QUALSIASI dato mutabile senza lavoro aggiuntivo. In molti scenari, un gioco non ha bisogno di quel livello di rigore per se stesso, ma potrebbe per cose come i replay.
krdluzni,

1
@krdluzni Threading, parallelismo e numeri casuali da vere fonti casuali non rendono i programmi non deterministici. Tempi di thread, deadlock, memoria non inizializzata e persino condizioni di gara sono solo input aggiuntivi che il tuo programma accetta. La tua scelta di scartare questi input o di non considerarli affatto (per qualsiasi motivo) non influenzerà il fatto che il tuo programma eseguirà esattamente lo stesso dato gli stessi input esatti. "non deterministico" è un termine di Informatica molto preciso, quindi per favore evita di usarlo se non sai cosa significa.

@oscar (può essere un po 'conciso, occupato, potrebbe essere modificato in un secondo momento): sebbene in qualche senso teorico e rigoroso potresti rivendicare i tempi di thread ecc. come input, questo non è utile in senso pratico, dal momento che generalmente non possono essere osservati dal programma stesso o completamente controllato dallo sviluppatore. Inoltre, un programma che non è deterministico è significativamente diverso in quanto non deterministico (nel senso della macchina di stato). Capisco il significato del termine. Vorrei che avessero scelto qualcos'altro, piuttosto che sovraccaricare un termine preesistente.
krdluzni,

@krdluzni Il mio punto nel progettare sistemi di replay con elementi imprevedibili come i tempi dei thread (se influenzano la tua capacità di calcolare accuratamente un replay), è di trattarli come qualsiasi altra fonte di input, proprio come l'input dell'utente. Non vedo nessuno lamentarsi che un programma sia "non deterministico" perché richiede un input dell'utente completamente imprevedibile. Per quanto riguarda il termine, è inaccurato e confuso. Preferirei che usassero qualcosa del tipo "praticamente imprevedibile" o qualcosa del genere. E no, non è impossibile, controlla il debug replay di VMWare.

9

Una cosa che altre risposte non hanno ancora coperto sono il pericolo di galleggianti. Non è possibile creare un'applicazione completamente deterministica utilizzando i float.

Usando i float, puoi avere un sistema completamente deterministico, ma solo se:

  • Utilizzando esattamente lo stesso binario
  • Utilizzando esattamente la stessa CPU

Questo perché la rappresentazione interna dei float varia da una CPU all'altra, il più drammaticamente tra CPU AMD e Intel. Finché i valori sono nei registri FPU, sono più precisi di quanto sembrino dal lato C, quindi tutti i calcoli intermedi vengono eseguiti con maggiore precisione.

È abbastanza ovvio come questo influenzerà il bit AMD vs Intel - diciamo che uno usa float a 80 bit e l'altro 64, ad esempio - ma perché lo stesso requisito binario?

Come ho detto, la maggiore precisione è in uso fintanto che i valori sono nei registri FPU . Ciò significa che ogni volta che si ricompila, l'ottimizzazione del compilatore può scambiare valori dentro e fuori dai registri FPU, ottenendo risultati leggermente diversi.

Potresti essere in grado di aiutarti impostando i flag _control87 () / _ controlfp () per utilizzare la precisione più bassa possibile. Tuttavia, alcune librerie possono anche toccarlo (almeno alcune versioni di d3d lo hanno fatto).


3
Con GCC puoi usare -ffloat-store per forzare i valori al di fuori dei registri e troncare a 32/64 bit di precisione, senza doverti preoccupare che altre librerie rovinino i tuoi flag di controllo. Ovviamente, questo avrà un impatto negativo sulla tua velocità (ma lo sarà anche su qualsiasi altra quantizzazione).

8

Salva lo stato iniziale dei generatori di numeri casuali. Quindi salva, timestamp, ogni input (mouse, tastiera, rete, qualunque cosa). Se hai un gioco in rete, probabilmente hai già tutto a posto.

Reimpostare gli RNG e riprodurre l'ingresso. Questo è tutto.

Questo non risolve il riavvolgimento, per il quale non esiste una soluzione generale, se non quella di riprodurre dall'inizio il più velocemente possibile. Puoi migliorare le prestazioni per questo controllando l'intero stato del gioco ogni X secondi, quindi dovrai sempre rigiocarlo, ma l'intero stato del gioco potrebbe anche essere proibitivo in termini di costi.

I dettagli del formato del file non contano, ma la maggior parte dei motori ha un modo per serializzare i comandi e dichiararli già - per il networking, il salvataggio o altro. Usalo e basta.


4

Vorrei votare contro il replay deterministico. È FAR più semplice e FAR meno soggetto a errori per salvare lo stato di ogni entità ogni 1/3 di secondo.

Salva solo quello che vuoi mostrare sulla riproduzione - se è solo posizione e direzione, va bene, se vuoi anche mostrare le statistiche, salva anche quello, ma in generale risparmia il meno possibile.

Modifica la codifica. Usa il minor numero di bit possibile per tutto. Il replay non deve essere perfetto fintanto che sembra abbastanza buono. Anche se usi un float per, diciamo, intestazione, puoi salvarlo in un byte e ottenere 256 possibili valori (precisione 1.4º). Potrebbe essere sufficiente o addirittura troppo per il tuo problema specifico.

Usa la codifica delta. A meno che le tue entità non si teletrasportino (e, in tal caso, trattano il caso separatamente), codificano le posizioni come la differenza tra la nuova posizione e la vecchia posizione - per movimenti brevi, puoi cavartela con molto meno bit di quanto avresti bisogno per le posizioni complete .

Se vuoi un riavvolgimento semplice, aggiungi i fotogrammi chiave (dati completi, senza delta) ogni N frame. In questo modo puoi cavartela con una precisione inferiore per i delta e altri valori, gli errori di arrotondamento non saranno così problematici se ripristini periodicamente i valori "veri".

Infine, gzip il tutto :)


1
Questo dipende comunque dal tipo di gioco.
Jari Komppa,

Starei molto attento con questa affermazione. Soprattutto per i progetti più grandi con dipendenze di terzi il salvataggio dello stato può essere impossibile. Durante il ripristino e la riproduzione dell'ingresso è sempre possibile.
TomSmartBishop,

2

È difficile. Prima di tutto leggi le risposte di Jari Komppa.

Un replay fatto sul mio computer potrebbe non funzionare sul tuo computer perché il risultato float è LEGGERMENTE diverso. È un grosso problema.

Ma dopo, se hai numeri casuali è memorizzare il valore seed nel replay. Quindi carica tutti gli stati predefiniti e imposta il numero casuale su quel seme. Da lì puoi semplicemente registrare lo stato attuale del tasto / mouse e il tempo trascorso in quel modo. Quindi eseguire tutti gli eventi utilizzando quello come input.

Per saltare i file (che è molto più difficile) dovrai scaricare THE MEMORY. Ad esempio, dove si trova ogni unità, i soldi, il tempo passa, tutto lo stato del gioco. Quindi avanzamento veloce ma riproduzione di tutto tranne saltare rendering, suono, ecc. Fino a raggiungere la destinazione temporale desiderata. Questo potrebbe accadere ogni minuto o 5 minuti a seconda della velocità di avanzamento.

I punti principali sono: - Gestire numeri casuali - Copiare input (player (s), e player (s) remoti) - Stato di dumping per saltare i file e ... - HAVING FLOAT NOT BREAK THINGS (sì, ho dovuto urlare)


2

Sono un po 'sorpreso che nessuno abbia menzionato questa opzione, ma se il tuo gioco ha un componente multiplayer, potresti aver già fatto molto del duro lavoro necessario per questa funzione. Dopotutto, cos'è il multiplayer se non un tentativo di riprodurre i movimenti di qualcun altro in un momento (leggermente) diverso sul tuo computer?

Questo ti dà anche i vantaggi di una dimensione di file più piccola come effetto collaterale, sempre supponendo che tu abbia lavorato su un codice di rete che rispetta la larghezza di banda.

In molti modi, combina entrambe le opzioni "sii estremamente deterministico" e "conserva un registro di tutto". Avrai ancora bisogno di determinismo: se il tuo re-play consiste essenzialmente nel riattivare il gioco esattamente come lo hai fatto originariamente, qualunque azione intraprendano che può avere esiti casuali deve avere lo stesso risultato.

Il formato dei dati potrebbe essere semplice come un dump del traffico di rete, anche se immagino che non farebbe male a ripulirlo un po '(dopo tutto non devi preoccuparti di un ritardo su una riproduzione). Puoi ri-giocare solo una parte del gioco usando il meccanismo del checkpoint che altre persone hanno menzionato - in genere un gioco multiplayer invierà comunque uno stato completo dell'aggiornamento del gioco ogni tanto, quindi potresti aver già fatto questo lavoro.


0

Per ottenere il file di replay più piccolo possibile devi assicurarti che il tuo gioco sia deterministico. Di solito questo implica guardare il tuo generatore di numeri casuali e vedere dove viene utilizzato nella logica di gioco.

Molto probabilmente dovrai avere un RNG logico di gioco e un altro RNG per cose come GUI, effetti particellari, suoni. Una volta fatto questo, è necessario registrare lo stato iniziale della logica di gioco RNG, quindi i comandi di gioco di tutti i giocatori in ogni frame.

Per molti giochi esiste un livello di astrazione tra l'input e la logica di gioco in cui l'input viene trasformato in comandi. Ad esempio, premendo il pulsante A sul controller, il comando digitale "jump" viene impostato su true e la logica di gioco reagisce ai comandi senza controllare direttamente il controller. In questo modo, dovrai solo registrare i comandi che incidono sulla logica di gioco (non è necessario registrare il comando "Pausa") e molto probabilmente questi dati saranno più piccoli della registrazione dei dati del controller. Inoltre, non devi preoccuparti di registrare lo stato dello schema di controllo nel caso in cui il giocatore decida di rimappare i pulsanti.

Il riavvolgimento è un problema difficile utilizzando il metodo deterministico e oltre all'utilizzo dell'istantanea dello stato del gioco e l'avanzamento rapido al momento in cui si desidera guardare, non c'è molto che si possa fare se non registrare l'intero stato del gioco in ciascun fotogramma.

D'altro canto, l'avanzamento rapido è certamente fattibile. Finché la logica di gioco non dipende dal rendering, è possibile eseguire la logica di gioco tutte le volte che si desidera prima di eseguire il rendering di un nuovo fotogramma del gioco. La velocità di avanzamento rapido sarà limitata dalla tua macchina. Se si desidera saltare avanti con incrementi di grandi dimensioni, è necessario utilizzare lo stesso metodo di snapshot necessario per il riavvolgimento.

Forse la parte più importante della scrittura di un sistema di riproduzione che si basa sul determinismo è la registrazione di un flusso di dati di debug. Questo flusso di debug contiene un'istantanea di quante più informazioni possibili per ogni fotogramma (seed RNG, trasformazioni di entità, animazioni, ecc.) Ed è in grado di testare quel flusso di debug registrato sullo stato del gioco durante i replay. Ciò ti consentirà di informarti rapidamente delle discrepanze alla fine di un determinato frame. Ciò consentirà di risparmiare innumerevoli ore nel voler strappare i capelli da sconosciuti bug non deterministici. Qualcosa di semplice come una variabile non inizializzata rovinerà tutto all'undicesima ora.

NOTA: se il tuo gioco prevede lo streaming dinamico dei contenuti o hai una logica di gioco su più thread o su core diversi ... buona fortuna.


0

Per abilitare sia la registrazione che il riavvolgimento, registra tutti gli eventi (generati dall'utente, generati dal timer, generati dalla comunicazione, ...).

Per ogni ora di registrazione dell'evento, cosa è stato modificato, valori precedenti, nuovi valori.

I valori calcolati non devono essere registrati a meno che il calcolo non sia casuale
(in questi casi è anche possibile registrare valori calcolati o registrare le modifiche al seed dopo ogni calcolo casuale).

I dati salvati sono un elenco di modifiche.
Le modifiche possono essere salvate in vari formati (binario, xml, ...).
La modifica consiste in ID entità, nome proprietà, vecchio valore, nuovo valore.

Assicurarsi che il sistema sia in grado di riprodurre queste modifiche (accedere all'entità desiderata, modificare la proprietà desiderata in avanti al nuovo stato o all'indietro al vecchio stato).

Esempio:

  • tempo dall'inizio = t1, entità = giocatore 1, proprietà = posizione, cambiato da a a b
  • tempo dall'inizio = t1, entità = sistema, proprietà = modalità gioco, modificato da c a d
  • tempo dall'inizio = t2, entità = giocatore 2, proprietà = stato, modificato da e a f
  • Per abilitare il riavvolgimento / avanzamento rapido o la registrazione solo di determinati intervalli di tempo,
    sono necessari i fotogrammi chiave , se si registra continuamente, ogni tanto si salva l'intero stato del gioco.
    Se si registra solo un determinato intervallo di tempo, all'inizio salvare lo stato iniziale.


    -1

    Se hai bisogno di idee su come implementare il tuo sistema di riproduzione, cerca su Google come implementare annulla / ripristina in un'applicazione. Per alcuni potrebbe essere ovvio, ma forse non per tutti, che l'annullamento / ripetizione è concettualmente la stessa della riproduzione dei giochi. È solo un caso speciale in cui è possibile riavvolgere e, a seconda dell'applicazione, cercare un momento specifico.

    Vedrai che nessuno implementando annulla / ripristina si lamenta di variabili deterministiche / non deterministiche, float o CPU specifiche.


    L'annullamento / ripetizione si verifica in applicazioni che sono esse stesse fondamentalmente deterministiche, guidate da eventi e di stato (ad esempio, lo stato di un documento elaboratore di testi è solo il testo e la selezione, non l'intero layout, che può essere ricalcolato).

    Quindi è ovvio che non hai mai usato applicazioni CAD / CAM, software di progettazione di circuiti, software di tracciamento del movimento o qualsiasi applicazione con annulla / ripristina più sofisticata di un elaboratore di testi. Non sto dicendo che il codice per annullare / ripetere possa essere copiato per la riproduzione su un gioco, solo che è concettualmente lo stesso (salvare gli stati e riprodurli in seguito). Tuttavia, la struttura di dati principale non è una coda ma uno stack.
    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.