Come sono possibili i giochi deterministici di fronte al non determinismo in virgola mobile?


52

Per creare un gioco come un RTS in rete, ho visto una serie di risposte che suggeriscono di rendere il gioco completamente deterministico; quindi devi solo trasferire le azioni degli utenti l'una con l'altra e ritardare un po 'ciò che viene visualizzato per "bloccare" l'input di tutti prima che il rendering del fotogramma successivo venga eseguito. Quindi cose come le posizioni delle unità, la salute, ecc. Non devono essere costantemente aggiornate sulla rete, perché la simulazione di ogni giocatore sarà esattamente la stessa. Ho anche sentito la stessa cosa suggerita per fare replay.

Tuttavia, poiché i calcoli in virgola mobile non sono deterministici tra macchine o anche tra diverse compilazioni dello stesso programma sulla stessa macchina, è davvero possibile farlo? Come possiamo evitare che questo fatto causi piccole differenze tra i giocatori (o i replay) che si increspano durante il gioco ?

Ho sentito alcune persone suggerire di evitare del tutto i numeri in virgola mobile e di usarli intper rappresentare il quoziente di una frazione, ma ciò non mi sembra pratico - e se avessi bisogno, per esempio, di prendere il coseno di un angolo? Devo seriamente riscrivere un'intera libreria matematica?

Nota che sono principalmente interessato a C #, che per quanto posso dire, ha esattamente gli stessi problemi di C ++ a questo proposito.


5
Questa non è una risposta alla tua domanda, ma per risolvere il problema di rete RTS raccomanderei di tanto in tanto di sincronizzare le posizioni e l'integrità dell'unità per correggere eventuali deviazioni. Le informazioni esattamente da sincronizzare dipenderanno dal gioco; puoi ottimizzare l'utilizzo della larghezza di banda senza preoccuparti di sincronizzare elementi come gli effetti di stato.
deridere il

@jhocking Eppure è probabilmente la risposta migliore.
Jonathan Connell,

starcraft II infacti si sincronizzava esattamente solo ogni volta, ho guardato il gameplay replay con i miei amici ogni computer fianco a fianco e lì dove differenze (anche significative) soprattutto con un ritardo elevato (1 secondo). Avevo alcune unità in cui i quadrati delle mappe 6/7 erano molto lontani sul mio schermo rispetto al suo
GameDeveloper

Risposte:


15

I punti in virgola mobile sono deterministici?

Ho letto molte cose su questo problema qualche anno fa, quando volevo scrivere un RTS usando la stessa architettura lockstep che fai.

Le mie conclusioni sui punti mobili hardware sono:

  • Lo stesso codice assembly nativo è molto probabilmente deterministico, a patto che tu stia attento con flag a virgola mobile e impostazioni del compilatore.
  • Esisteva un progetto RTS open source che sosteneva di ottenere compilazioni C / C ++ deterministiche su diversi compilatori utilizzando una libreria wrapper. Non ho verificato tale affermazione. (Se ricordo bene si trattava della STREFLOPbiblioteca)
  • Il .IT JIT è concesso un po 'di margine di manovra. In particolare, è consentito utilizzare una precisione superiore a quella richiesta. Inoltre usa diversi set di istruzioni su x86 e AMD64 (penso su x86 usa x87, AMD64 usa alcune istruzioni SSE il cui comportamento differisce per i denorm).
  • Le istruzioni complesse (inclusa la funzione trigonometrica, esponenziali, logaritmi) sono particolarmente problematiche.

Ho concluso che è impossibile utilizzare in modo deterministico i tipi in virgola mobile integrati in .net.

Possibili soluzioni alternative

Quindi avevo bisogno di soluzioni alternative. Ho considerato:

  1. Implementare FixedPoint32in C #. Anche se questo non è troppo difficile (ho un'implementazione quasi finita), l'intervallo molto piccolo di valori lo rende fastidioso da usare. Devi stare attento in ogni momento in modo da non traboccare, né perdere troppa precisione. Alla fine l'ho trovato non più semplice dell'utilizzo diretto di numeri interi.
  2. Implementare FixedPoint64in C #. Ho trovato questo piuttosto difficile da fare. Per alcune operazioni sarebbero utili numeri interi intermedi di 128 bit. Ma .net non offre questo tipo.
  3. Utilizzare il codice nativo per le operazioni matematiche che è deterministico su una piattaforma. Supporta l'overhead di una chiamata delegata su ogni operazione matematica. Perde la capacità di eseguire multipiattaforma.
  4. Usa Decimal. Ma è lento, richiede molta memoria e genera facilmente eccezioni (divisione per 0, overflow). È molto utile per uso finanziario, ma non è adatto ai giochi.
  5. Implementare un virgola mobile a 32 bit personalizzato. All'inizio sembrava piuttosto difficile. La mancanza di un intrinseco BitScanReverse provoca alcuni fastidi durante l'implementazione.

My SoftFloat

Ispirato al tuo post su StackOverflow , ho appena iniziato a implementare un software a virgola mobile a 32 bit e i risultati sono promettenti.

  • La rappresentazione della memoria è binaria compatibile con i float IEEE, quindi posso reinterpretare il cast quando li trasmetto al codice grafico.
  • Supporta SubNorms, infinity e NaNs.
  • I risultati esatti non sono identici ai risultati IEEE, ma di solito non importa per i giochi. In questo tipo di codice importa solo che il risultato sia lo stesso per tutti gli utenti, non che sia preciso fino all'ultima cifra.
  • Le prestazioni sono decenti. Un banale test ha dimostrato che può fare circa 75MFLOPS rispetto a 220-260MFLOPS con floataddizione / moltiplicazione (Single thread su un i66 a 2,66 GHz ). Se qualcuno ha buoni benchmark in virgola mobile per .net, per favore, mandameli, dato che il mio test attuale è molto rudimentale.
  • L'arrotondamento può essere migliorato. Attualmente si tronca, che corrisponde all'incirca all'arrotondamento verso lo zero.
  • È ancora molto incompleto. Attualmente mancano divisione, cast e complesse operazioni matematiche.

Se qualcuno vuole contribuire con i test o migliorare il codice, contattami o invia una richiesta pull su github. https://github.com/CodesInChaos/SoftFloat

Altre fonti di indeterminismo

Ci sono anche altre fonti di indeterminismo in .net.

  • iterando su a Dictionary<TKey,TValue>o HashSet<T>restituisce gli elementi in un ordine indefinito.
  • object.GetHashCode() differisce da corsa a corsa.
  • L'implementazione della Randomclasse integrata non è specificata, usa la tua.
  • Il multithreading con blocco ingenuo porta al riordino e a risultati diversi. Fai molta attenzione a usare correttamente i thread.
  • Quando WeakReferenceperdono il loro obiettivo è indeterministico perché il GC può funzionare in qualsiasi momento.

23

La risposta a questa domanda proviene dal link che hai pubblicato. In particolare, dovresti leggere la citazione da Gas Powered Games:

Lavoro a Gas Powered Games e posso dirti in prima persona che la matematica in virgola mobile è deterministica. Hai solo bisogno dello stesso set di istruzioni e compilatore e, naturalmente, il processore dell'utente aderisce allo standard IEEE754, che include tutti i nostri PC e clienti 360. Il motore che esegue DemiGod, Supreme Commander 1 e 2 si basa sullo standard IEEE754. Per non parlare probabilmente di tutti gli altri giochi peer to peer RTS presenti sul mercato.

E poi sotto quello è questo:

Se si memorizzano replay come input del controller, questi non possono essere riprodotti su macchine con diverse architetture CPU, compilatori o impostazioni di ottimizzazione. In MotoGP, ciò significava che non potevamo condividere i replay salvati tra Xbox e PC.

Un gioco deterministico sarà deterministico solo quando si utilizzano file compilati in modo identico ed eseguito su sistemi che aderiscono agli standard IEEE. Simulazioni o replay di reti sincronizzate multipiattaforma non saranno possibili.


2
Come ho già detto, sono interessato principalmente a C #; poiché JIT esegue la compilazione effettiva, non posso garantire che sia "compilato con le stesse impostazioni di ottimizzazione" anche quando si esegue lo stesso eseguibile sulla stessa macchina!
BlueRaja - Danny Pflughoeft

2
A volte la modalità di arrotondamento è impostata in modo diverso nella fpu, su alcune architetture la fpu ha un registro interno più grande della precisione per i calcoli ... mentre IEEE754 non dà margine di manovra per quanto riguarda il funzionamento delle operazioni FP, non lo fa ' t specificare come deve essere implementata una particolare funzione matematica, che può esporre le differenze architettoniche.
Stephen,

4
@ 3nixios Con quel tipo di installazione, il gioco non è deterministico. Solo i giochi con un timestep fisso possono essere deterministici. Stai sostenendo che qualcosa non è già deterministico quando si esegue una simulazione su una singola macchina.
Attaccando

4
@ 3nixios, "Stavo affermando che anche con un timestep fisso, nulla garantisce che il timestep rimarrà sempre fisso." Ti sbagli completamente. Il punto di un timestep fisso è di avere sempre lo stesso delta time per ogni tick in aggiornamento
AttackingHobo

3
@ 3nixios L'utilizzo di un timestep fisso assicura che il timestep venga corretto perché noi sviluppatori non consentiamo diversamente. Stai fraintendendo il modo in cui il ritardo deve essere compensato quando si utilizza una fase temporale fissa. Ogni aggiornamento del gioco deve utilizzare lo stesso tempo di aggiornamento; pertanto, quando la connessione di un peer è in ritardo, deve comunque calcolare ogni singolo aggiornamento perso.
Keeblebrox,

3

Usa l'aritmetica a punto fisso. Oppure scegli un server autorevole e sincronizza lo stato del gioco una volta ogni tanto: ecco cosa fa MMORTS. (Almeno Elements of War funziona così. È scritto anche in C #.) In questo modo, gli errori non hanno la possibilità di accumularsi.


Sì, potrei renderlo un client-server e gestire il mal di testa della previsione e dell'estrapolazione sul lato client. Questa domanda riguarda le architetture peer-to-peer, che dovrebbero essere più facili ...
BlueRaja - Danny Pflughoeft

Bene, non è necessario eseguire una previsione in uno scenario "tutti i client eseguono lo stesso codice". Il ruolo del server deve essere l'autorità solo sulla sincronizzazione.
Nevermind

Server / client è solo un metodo per creare un gioco in rete. Un altro metodo popolare (specialmente per es. Giochi RTS, come C&C o Starcraft) è il peer-to-peer, in cui non esiste un server autorevole. Affinché ciò sia possibile, tutti i calcoli devono essere completamente deterministici e coerenti tra tutti i clienti, quindi la mia domanda.
BlueRaja - Danny Pflughoeft,

Beh, non è che DEVI assolutamente rendere il gioco rigorosamente p2p senza server / client elevati. Se devi assolutamente farlo, però, usa il punto fisso.
Nevermind

3

Modifica: un collegamento a una classe a virgola fissa (attenzione dell'acquirente! - Non l'ho usato ...)

Potresti sempre ricorrere all'aritmetica a punto fisso . Qualcuno (facendo un rts, non meno) ha già fatto il lavoro di gamba su StackOverflow .

Pagherai una penalità per le prestazioni, ma questo potrebbe non essere un problema, poiché .net non sarà particolarmente performante qui in quanto non utilizzerà le istruzioni simd. Prova delle prestazioni!

NB Apparentemente qualcuno a Intel sembra avere una soluzione per permetterti di usare la libreria di primitive di Intel Intel da c #. Ciò può aiutare a vettorializzare il codice a virgola fissa per compensare le prestazioni più lente.


decimalfunzionerebbe bene anche per quello; ma, questo ha ancora gli stessi problemi dell'utilizzo int- come posso fare trig con esso?
BlueRaja - Danny Pflughoeft il

1
Il modo più semplice sarebbe quello di creare una tabella di ricerca e inviarla insieme all'applicazione. È possibile interpolare tra valori se la precisione è un problema.
Luca,

In alternativa, potresti essere in grado di sostituire le chiamate trig con numeri o quaternioni immaginari (se 3D). Questa è una spiegazione eccellente
Luca,

Questo può anche aiutare.
Luca,

Nell'azienda per cui lavoravo, abbiamo costruito una macchina ad ultrasuoni utilizzando la matematica a punto fisso, quindi funzionerà sicuramente per te.
Luca,

3

Ho lavorato su numerosi titoli di grandi dimensioni.

Risposta breve: è possibile se sei follemente rigoroso, ma probabilmente non ne vale la pena. A meno che tu non abbia un'architettura fissa (leggi: console) è pignolo, fragile e presenta una serie di problemi secondari come il late join.

Se leggi alcuni degli articoli citati, noterai che mentre puoi impostare la modalità CPU ci sono casi bizzarri come quando un driver di stampa cambia la modalità CPU perché era nello stesso spazio degli indirizzi. Ho avuto un caso in cui un'applicazione era bloccata da un frame a un dispositivo esterno, ma un componente difettoso causava il throttling della CPU a causa del calore e riportava in modo diverso al mattino e al pomeriggio, rendendolo in effetti una macchina diversa dopo pranzo.

Queste differenze di piattaforma sono nel silicio, non nel software, quindi per rispondere alla tua domanda anche C # è interessato. Istruzioni come FMA (Fly) moltiplicato vs ADD + MUL cambiano il risultato perché viene arrotondato internamente solo una volta anziché due volte. C ti dà più controllo per forzare la CPU a fare ciò che vuoi fondamentalmente escludendo operazioni come FMA per rendere le cose "standard" - ma a costo della velocità. Gli intrinseci sembrano essere i più inclini a differire. In un progetto ho dovuto estrarre un libro di tabelle acos di 150 anni per ottenere valori da confrontare per determinare quale CPU fosse "giusta". Molte CPU usano approssimazioni polinomiali per le funzioni di trigger ma non sempre con gli stessi coefficienti.

La mia raccomandazione:

Indipendentemente dal fatto che tu vada in blocco intero o sincronizzi, gestisci le meccaniche di gioco principali separatamente dalla presentazione. Rendi il gioco accurato, ma non preoccuparti della precisione nel livello di presentazione. Ricorda inoltre che non è necessario inviare tutti i dati del mondo in rete con lo stesso frame rate. Puoi dare la priorità ai tuoi messaggi. Se la tua simulazione è abbinata al 99,999%, non dovrai trasmettere più spesso per rimanere accoppiato. (Trucchi prevenzione a parte.)

C'è un bell'articolo sul motore di Source che spiega un modo per eseguire la sincronizzazione: https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking

Ricorda, se sei interessato a partecipare in ritardo, dovrai mordere il proiettile e sincronizzare comunque.


1

Nota che sono principalmente interessato a C #, che per quanto posso dire, ha esattamente gli stessi problemi di C ++ a questo proposito.

Sì, C # ha gli stessi problemi di C ++. Ma ha anche molto di più.

Ad esempio, prendi questa affermazione da Shawn Hawgraves:

Se si memorizzano replay come input del controller, questi non possono essere riprodotti su macchine con diverse architetture CPU, compilatori o impostazioni di ottimizzazione.

È "abbastanza facile" per garantire che ciò avvenga in C ++. In C # , tuttavia, sarà molto più difficile da affrontare. Questo grazie a JIT.

Cosa pensi che succederebbe se l'interprete eseguisse il tuo codice interpretato una volta, ma poi JIT l'avesse la seconda volta? O forse lo interpreta due volte sulla macchina di qualcun altro, ma dopo è JIT?

JIT non è deterministico, perché hai pochissimo controllo su di esso. Questa è una delle cose a cui rinunciare per usare il CLR.

E Dio ti aiuti se una persona sta usando .NET 4.0 per eseguire il tuo gioco, mentre qualcun altro sta usando il CLR Mono (usando ovviamente le librerie .NET). Anche .NET 4.0 vs. .NET 5.0 potrebbero essere diversi. Hai semplicemente bisogno di un maggiore controllo sui dettagli di basso livello di una piattaforma per garantire questo tipo di cose.

Dovresti essere in grado di cavartela con la matematica a virgola fissa. Ma questo è tutto.


A parte la matematica, cos'altro potrebbe essere diverso? Presumibilmente, le azioni "input" vengono inviate come azioni logiche in un RT. Ad esempio, spostare l'unità "a" in posizione "b". Riesco a vedere un problema con la generazione di numeri casuali, ma ovviamente deve essere inviato anche come input di simulazione.
Luca,

@LukeN: Il problema è che la posizione di Shawn suggerisce fortemente che le differenze del compilatore possono avere effetti reali sulla matematica in virgola mobile. Saprebbe più di me; Sto semplicemente estrapolando il suo avvertimento nel regno della compilazione / interpretazione di JIT e C #.
Nicol Bolas,

C # ha un sovraccarico dell'operatore e ha anche un tipo incorporato per la matematica a virgola fissa. Tuttavia, questo ha gli stessi problemi dell'utilizzo di un int- come posso fare trig con esso?
BlueRaja - Danny Pflughoeft,

1
> Cosa pensi che succederebbe se l'interprete eseguisse il tuo codice> interpretato una volta, ma poi JIT l'avrebbe fatto la seconda volta? O forse lo> lo interpreta due volte sulla macchina di qualcun altro, ma dopo lo è JIT? 1. Il codice CLR viene sempre emesso prima dell'esecuzione. 2. .net utilizza ovviamente IEEE 754 per i float. > JIT non è deterministico, perché hai pochissimo controllo su di esso. La tua conclusione è causa abbastanza debole di false dichiarazioni false. > E Dio ti aiuti se una persona sta usando .NET 4.0 per eseguire il tuo gioco,> mentre qualcun altro sta usando il CLR Mono (usando le librerie .NET del> ovviamente). Anche .NET
Romanenkov Andrey,

@Romanenkov: "La tua conclusione è causa piuttosto debole di false dichiarazioni false." Per favore, dimmi quali affermazioni sono false e perché, in modo che possano essere corrette.
Nicol Bolas,

1

Mi sono reso conto dopo aver scritto questa risposta che in realtà non risponde alla domanda, che era specificamente sul non determinismo in virgola mobile. Ma forse questo gli sarà comunque utile se farà un gioco in rete in questo modo.

Insieme alla condivisione degli input che stai trasmettendo a tutti i giocatori, può essere molto utile creare e trasmettere un checksum di importanti stati del gioco, come posizioni dei giocatori, salute, ecc. Durante l'elaborazione dell'input, asserisci che lo stato del gioco viene verificato per tutti i lettori remoti sono sincronizzati. Hai la certezza di avere bug non sincronizzati (OOS) da correggere e questo renderà più semplice - avrai una nota in anticipo che qualcosa è andato storto (che ti aiuterà a capire i passaggi della riproduzione) e dovresti essere in grado di aggiungere altro registrazione dello stato del gioco in codice sospetto per consentire di raggruppare qualsiasi cosa stia causando la OOS.


0

Penso che l'idea del blog collegata abbia ancora bisogno della sincronizzazione periodica per essere praticabile: ho visto abbastanza bug nei giochi RTS in rete che non adottano questo approccio.

Le reti sono in perdita, lente, hanno latenza e potrebbero persino inquinare i tuoi dati. Il "determinismo in virgola mobile", (che suona abbastanza confuso da rendermi scettico) è la minima delle tue preoccupazioni nella realtà ... specialmente se usi una fase temporale fissa. con fasce orarie variabili dovrai interpolare tra fasce orarie fisse per evitare anche problemi di determinismo. Penso che questo sia ciò che si intende per comportamento "determinante" in virgola mobile non deterministico - solo che i passi variabili nel tempo fanno divergere le integrazioni - niente a che fare con le librerie matematiche o le funzioni di basso livello.

La sincronizzazione è la chiave però.


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.