In che modo la compressione delta riduce la quantità di dati inviati sulla rete?


26

Molti giochi utilizzano la tecnica della compressione delta per ridurre il carico di dati inviato. Non riesco a capire come questa tecnica riduca effettivamente il carico di dati?

Ad esempio, supponiamo di voler inviare una posizione. Senza delta compressione invio a vector3con la posizione esatta dell'entità, per esempio (30, 2, 19). Con la compressione delta invio a vector3con numeri più piccoli (0.2, 0.1, 0.04).

Non capisco come riduce il carico di dati se entrambi i messaggi sono vector3- 32 bit per ogni float - 32 * 3 = 96 bit!

So che puoi convertire ogni float in un byte e poi riconvertirlo da un byte in float ma provoca errori di precisione che sono visibili.


Ogni volta che stai collegando in rete con un gioco che utilizza qualsiasi forma di compressione delta, devi essere perfettamente deterministico (fallisci e ottieni desincronizzazione). Se "convertire da byte a float" (o qualsiasi altra cosa) causa errori di precisione non è importante - la condizione necessaria è avere tutto esattamente lo stesso in tutte le macchine / i giochi sincronizzati. Se ci sono "errori di precisione" (inevitabili, anche se hai usato float completi - la tua CPU non usa gli stessi float della mia CPU), devono essere gli stessi su tutte le macchine - e quindi non sono visibili. Hai scelto i tipi per evitare effetti visibili.
Luaan,

2
@Luaan oppure puoi aggiungere uno stato assoluto parziale ogni tanto, ad esempio puoi ogni tanto selezionare alcune entità e passare la posizione assoluta, preferendo selezionare entità vicine al giocatore.
maniaco del cricchetto,

In qualche modo, mi aspettavo che questa domanda riguardasse un parente di rsync ...
SamB,

Huffman Coding.
Ben Voigt,

Usa solo l'uomo di mezza età
John Demetriou,

Risposte:


41

Ci sono momenti in cui non puoi evitare di inviare lo stato di gioco completo, ad esempio al caricamento di una partita multiplayer salvata o quando è necessaria una risincronizzazione. Ma l'invio dello stato completo è generalmente evitabile, ed è qui che entra in gioco la codifica delta . Generalmente, è tutto ciò che riguarda la compressione delta; il tuo esempio non descrive davvero quella situazione. Il motivo per cui la compressione delta è persino menzionata è perché le implementazioni ingenue invieranno spesso state piuttosto che delta perché lo stato è di solito ciò che ogni implementazione ingenua del gioco memorizza comunque. I delta sono quindi un'ottimizzazione.

Con i delta, non invierai mai le posizioni delle unità che non si sono mosse , per niente. Questo è lo spirito.

Immagina di essere amici di penna da anni e ho perso la memoria (e avevo scartato tutte le tue lettere dopo averle lette). Invece di continuare semplicemente con le tue serie di lettere come di consueto, dovresti scrivermi tutta la storia della tua vita in un'unica e massiccia lettera, ancora una volta, per farmi recuperare.

Nel tuo caso specifico, potrebbe essere possibile (a seconda della base di codice) utilizzare un numero inferiore di bit per codificare i delta, al contrario degli ampi intervalli di bit necessari per inviare lo stato completo. Supponiamo che il mondo sia lungo molti chilometri, potrebbe essere necessario un float a 32 bit per codificare con precisione le posizioni in basso, ad esempio un centimetro in un dato asse. Tuttavia, data la velocità massima applicabile alle entità, che può essere solo di un paio di metri per tick, può essere fattibile in soli 8 bit (2 ^ 8 = 256, quindi sufficiente per memorizzare un massimo di 200 cm). Ovviamente, ciò presuppone un utilizzo in virgola fissa anziché in virgola mobile ... o una sorta di mezzo / quarto float come in OpenGL, se non si desidera seccature in virgola fissa.


7
La tua risposta non è chiara per me. Sento che non inviare le informazioni di un oggetto che non si muove è solo un effetto collaterale della codifica delta e non "Lo spirito di esso". La vera ragione per usare la codifica delta sembra essere evidenziata meglio nella risposta del maniaco del cricchetto.
Etsitpab Nioliv,

14
@EtsitpabNioliv "The Spirit" è semplicemente "non inviare ciò che non devi". Questo può essere portato a livello di bit: "usa solo la larghezza di banda necessaria per ottenere il messaggio richiesto via cavo". Questa risposta evidentemente sembra abbastanza chiara per tutti gli altri. Grazie.
Ingegnere,

4
@EtsitpabNioliv Hai mai imparato come SVN archivia i file sul lato server? Non memorizza l'intero file per ogni commit. Memorizza i delta , le modifiche contenute in ogni commit. Il termine "delta" è spesso usato in matematica e in programmazione per fare riferimento alla differenza tra due valori. Non sono un programmatore di giochi, ma sarei sorpreso se l'utilizzo è molto diverso nei giochi. L'idea ha quindi senso: "comprimi" la quantità di dati che devi inviare inviando solo le differenze, invece di tutto. (Se una parte di ciò mi confonde, è la parola "comprimere")
jpmc26

1
Inoltre, i numeri piccoli hanno un numero maggiore di zero bit e l'uso dell'algoritmo di compressione appropriato per codificare / decodificare le informazioni inviate può portare a un payload ancora più piccolo da inviare sulla rete.
liggiorgio,

22

Hai delta sbagliato. Stai guardando il delta dei singoli elementi. Devi pensare al delta dell'intera scena.

Supponiamo che tu abbia 100 elementi nella tua scena, ma solo 1 di essi sia stato spostato. Se invii 100 vettori di elementi, 99 di questi vengono sprecati. Devi solo inviare 1.

Ora, supponiamo che tu abbia un oggetto JSON che memorizza tutti i tuoi vettori di elementi. Questo oggetto è sincronizzato tra il tuo server e il tuo client. Invece di decidere "ha fatto così e così spostare?" puoi semplicemente generare il tuo prossimo tick di gioco in un oggetto JSON, creare un diff tick100.json tick101.jsone inviare quel diff. Sul lato client, applichi il diff al vettore del tick corrente e sei pronto.

In questo modo sfrutti i decenni di esperienza nel rilevare differenze nel testo (o binario!) E non devi preoccuparti di perdere nulla da solo. Ora idealmente usi anche una libreria che lo fa dietro le quinte per renderti ancora più facile per te come sviluppatore.


2
L'uso diffsuona come un hack inefficiente. Mantenere la stringa JSON all'estremità ricevente, rattopparla e deserializzarla ogni volta non è necessario. Il calcolo della differenza tra due dizionari di valori-chiave non è un compito complicato, in pratica basta passare in rassegna tutte le chiavi e verificare se i valori sono uguali. In caso contrario, li aggiungi al dict valore-chiave risultante e infine invii il dict serializzato su JSON. Semplice, non sono necessari anni di esperienza. A differenza delle differenze, questo metodo: 1) non includerà dati vecchi (sostituiti); 2) gioca meglio con UDP; 3) non fa affidamento su
newline

8
@gronostaj Questo è stato un esempio per chiarire il punto. In realtà non sostengo l'uso di diff per JSON - ecco perché dire "supponiamo".
corsiKa

2
"In questo modo sfrutti i decenni di esperienza nel rilevare differenze nel testo (o binario!) E non devi preoccuparti di perdere qualcosa da solo. Ora idealmente usi anche una libreria che fa questo dietro le quinte per renderlo uniforme più facile per te come sviluppatore ". Quella parte sicuramente fa sembrare che tu stia suggerendo di usare un diff o di usare una libreria che usa un diff quando nessuno farebbe ragionevolmente una cosa del genere. Non definirei la compressione delta "diffing", è solo la compressione delta, le somiglianze sono superficiali
Selali Adobor,

Una differenza ottimale e una compressione delta ottimale sono le stesse. Mentre l'utilità diff sulla riga di comando è orientata ai file di testo e probabilmente non fornirà un risultato ottimale, consiglierei di ricercare librerie che eseguano la compressione delta per te. Non c'è niente di speciale nella parola delta - delta e diff, in questo senso, significano letteralmente la stessa cosa. Sembra che si sia perso negli anni.
corsiKa

9

Molto spesso un altro meccanismo di compressione si combina con la codifica delta come ad esempio la compressione aritmetica.

Questi schemi di compressione funzionano molto meglio quando i possibili valori sono raggruppati insieme in modo prevedibile. La codifica Delta raggrupperà i valori attorno a 0.


1
Un altro esempio: se hai 100 astronavi ciascuna con la propria posizione ma viaggiando allo stesso vettore di velocità, devi solo inviare la velocità una volta (o almeno comprenderla davvero bene); altrimenti, dovrai invece inviare 100 posizioni. Lascia che gli altri facciano l'aggiunta. Se consideri le simulazioni di blocco in stato condiviso una forma aggressiva di delta-compressione, non invii nemmeno le velocità - solo i comandi provenienti dal giocatore. Ancora una volta, lascia che ognuno faccia il proprio.
Luaan,

Sono d'accordo. La compressione è rilevante nella risposta.
Leopoldo Sanczyk,

8

Sei ampiamente corretto, ma manca un punto importante.

Le entità in un gioco sono descritte da molti attributi, la cui posizione è solo una .

Che tipo di attributi? Senza dover pensare troppo, in un gioco in rete questi potrebbero includere:

  • Posizione.
  • Orientamento.
  • Numero di frame corrente.
  • Informazioni su colore / illuminazione.
  • Trasparenza.
  • Modello da usare.
  • Texture da usare.
  • Effetti speciali.
  • Eccetera.

Se scegli ciascuno di questi in modo isolato, puoi certamente sostenere che se in un determinato frame deve essere cambiato, deve essere ritrasmesso per intero.

Tuttavia, non tutti questi attributi cambiano alla stessa velocità .

Il modello non cambia? Senza compressione delta, deve essere comunque ritrasmesso. Con la compressione delta non è necessario.

Posizione e orientamento sono due casi più interessanti, essendo generalmente composti da 3 float ciascuno. Tra uno qualsiasi dei due frame, esiste la possibilità che solo 1 o 2 di ogni set di 3 float possano cambiare. Muoversi in linea retta? Non ruota? Non saltare? Questi sono tutti casi in cui senza la compressione delta è necessario ritrasmettere completamente, ma con la compressione delta è necessario solo ritrasmettere ciò che cambia.


8

Hai ragione che un calcolo delta ingenuo da solo, con il risultato memorizzato nella stessa struttura di dati delle dimensioni degli operandi e trasmesso senza ulteriore elaborazione, non risparmierebbe alcun traffico.

Tuttavia, ci sono due modi in cui un sistema ben progettato basato su delta può risparmiare traffico.

Innanzitutto in molti casi il delta sarà zero. Puoi progettare il tuo protocollo in modo tale che se il delta è zero non lo invii affatto. Ovviamente, vi è un certo sovraccarico in quanto devi indicare cosa stai o non stai inviando, ma nel complesso è probabile che sia una grande vittoria.

In secondo luogo, i delta avranno generalmente un intervallo di valori molto più piccolo rispetto ai numeri originali. e quell'intervallo sarà centrato su zero. Questo può essere sfruttato utilizzando un tipo di dati più piccolo per la maggior parte dei delta o passando il flusso di dati completo attraverso un algoritmo di compressione generico.


6

Mentre la maggior parte delle risposte parla di come la codifica delta riguarda solo l'invio delle modifiche allo stato nel suo insieme, esiste un'altra cosa chiamata "codifica delta" che può essere utilizzata come filtro per ridurre la quantità di dati che è necessario comprimere in pieno stato aggiornare anche, che può essere da dove proviene la confusione nella domanda come chiesto.

Quando si codifica un vettore di numeri, è possibile in alcuni casi (ad esempio numeri interi, enum, ecc.) Utilizzare una codifica a byte variabile per i singoli elementi e in alcuni di questi casi è possibile ridurre ulteriormente la quantità di dati di cui ciascun elemento ha bisogno se lo memorizzi come somme correnti o come valore minimo e differenza tra ciascun valore e quel minimo.

Ad esempio, se si desidera codificare il vettore, {68923, 69012, 69013, 69015}è possibile codificarlo come {68923, 89, 1, 2}. Utilizzando una banale codifica a byte variabile in cui si memorizzano 7 bit di dati per byte e si utilizza un bit per indicare che sta arrivando un altro byte, ciascuno dei singoli elementi dell'array richiederebbe 3 byte per trasmetterlo, ma la versione con codifica delta richiederebbe solo 3 byte per il primo elemento e 1 byte per gli elementi rimanenti; a seconda del tipo di dati che stai serializzando, questo può portare ad alcuni risparmi piuttosto impressionanti.

Tuttavia, si tratta più di un'ottimizzazione della serializzazione e non è ciò che generalmente si intende quando parliamo di "codifica delta" quando si tratta di trasmettere dati arbitrari come parte dello stato del gioco (o video o simili); altre risposte fanno già un lavoro adeguato per spiegarlo.


4

Vale anche la pena notare che gli algoritmi di compressione fanno meglio il loro lavoro sul diff. Come altre risposte menzionano, la maggior parte dei tuoi elementi rimane invariata tra 2 stati o i valori cambiano di una piccola frazione. In entrambi questi casi l'applicazione di un algoritmo di compressione alla differenza del tuo vettore di numeri ti dà risparmi significativi. Anche se non applichi alcuna logica aggiuntiva al tuo vettore come rimuovere gli elementi 0.

Ecco un esempio in Python:

import numpy as np
import zlib
import json
import array


state1 = np.random.random(int(1e6))

diff12 = np.r_[np.random.random(int(0.1e6)), np.zeros(int(0.9e6))]
np.random.shuffle(diff12) # shuffle so we don't cheat by having all 0s one after another
state2 = state1 + diff12

state3 = state2 + np.random.random(int(1e6)) * 1e-6
diff23 = state3 - state2

def compressed_size(nrs):
    serialized = zlib.compress(array.array("d", nrs).tostring())
    return len(serialized)/(1024**2)


print("Only 10% of elements change for state2")
print("Compressed size of diff12: {:.2f}MB".format(compressed_size(diff12)))
print("Compressed size of state2: {:.2f}MB".format(compressed_size(state2)))

print("All elements change by a small value for state3")
print("Compressed size of diff23: {:.2f}MB".format(compressed_size(diff23)))
print("Compressed size of state3: {:.2f}MB".format(compressed_size(state3)))

Il che mi dà:

Only 10% of elements change for state2
Compressed size of diff12: 0.90MB
Compressed size of state2: 7.20MB
All elements change by a small value for state3
Compressed size of diff23: 5.28MB
Compressed size of state3: 7.20MB

Bell'esempio La compressione gioca un ruolo qui.
Leopoldo Sanczyk,

0

Se la tua posizione è memorizzata in vector3 ma l'entità effettiva può spostare solo un numero intero alla volta. Quindi inviare il suo delta in byte sarebbe meglio che inviarlo in numero intero.

Posizione corrente: 23009.0, 1.0, 2342.0 (3 float)
Nuova posizione: 23010.0, 1.0, 2341.0 (3 float)
Delta: 1, 0, -1 (3 byte)

Invece di inviare sempre la posizione esatta, inviamo il delta.


0

La compressione delta è una compressione dei valori codificati delta. La codifica delta è una trasformazione che produce una diversa distribuzione statistica dei numeri. Se la distribuzione è favorevole all'algoritmo di compressione scelto, riduce la quantità di dati. Funziona molto bene in un sistema come un gioco in cui le entità si muovono solo leggermente tra due aggiornamenti.

Supponiamo che tu abbia 100 entità in 2D. Su una griglia di grandi dimensioni, 512 x 512. Considerando solo numeri interi a titolo di esempio. Sono due numeri interi per entità o 200 numeri.

Tra due aggiornamenti, tutte le nostre posizioni cambiano di 0, 1, -1, 2 o -2. Ci sono state 100 istanze di 0, 33 istanze di 1 e -1 e solo 17 istanze di 2 e -2. Questo è abbastanza comune. Scegliamo la codifica Huffman per la compressione.

L'albero di Huffman per questo sarà:

 0  0
-1  100
 1  101
 2  110
-2  1110

Tutti i tuoi 0 saranno codificati come un singolo bit. Sono solo 100 bit. 66 valori verranno codificati come 3 bit e solo 34 valori come 4 bit. Sono 434 bit o 55 byte. Inoltre un piccolo overhead per salvare il nostro albero di mappatura, in quanto l'albero è minuscolo. Si noti che per codificare 5 numeri, sono necessari 3 bit. Abbiamo scambiato qui la possibilità di usare 1 bit per '0' per la necessità di usare 4 bit per '-2'.

Ora confronta questo con l'invio di 200 numeri arbitrari. Se le tue entità non possono essere sulla stessa tessera, sei quasi sicuro di ottenere una cattiva distribuzione statistica. Il caso migliore sarebbe 100 numeri univoci (tutti sulla stessa X con Y diversa). Questo è almeno 7 bit per numero (175 byte) e molto difficile per qualsiasi algoritmo di compressione.

La compressione delta funziona nel caso speciale in cui le entità cambiano solo leggermente. Se hai molte modifiche uniche, la codifica delta non ti aiuterà.


Si noti che la codifica e la compressione delta vengono utilizzate anche in altre situazioni con altre trasformazioni.

MPEG divide l'immagine in piccoli quadrati e se parte dell'immagine si sposta, vengono salvati solo i movimenti e le modifiche. In un film a 25 fps, molti cambiamenti tra i fotogrammi sono molto piccoli. Ancora una volta, codifica delta + compressione. Funziona meglio per le scene statiche.

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.