On 6/20/2018 at 2:04 PM, hplus0603 said:
Note that the client then needs to know which previous server state this is in reference to, so the server needs to include which last acked client tick it based its compression on. The client then needs to also have as long a queue of old client states as the server has, to be able to reconstruct the correct value.
22 hours ago, tromtrom said:
For now I'm going with the first method, which is replacing the old value by the new value, because as you said there's a need to keep track of previous states at the level of the client in order to reconstitute the new value.
In Quake 3, they use this method (copy, not XOR), but they still do need to keep track of a history of old states on the client and server. This is because Quake 3 sends the deltas via UDP, which means they can be lost or arrive out of order -- and if simply naively apply deltas that arrive to the client's current state in this situation (where some don't arrive, or arrive in the wrong order), then the client won't correctly reconstruct the server's state.
To get rid of the history-buffer concept, you need to use a reliable protocol like TCP, to ensure no deltas are lost and the order of changes is preserved. This is the same regardless of whether you do straight copies, or the XOR trick
The history buffer idea is really the key innovation of Quake 3, which allows them to use both UDP and delta encoding reliably.
Another alternative to the XOR method is to store differences. e.g. if health was 100 and now it's 80, then you send a delta value of -20 across the network, and the client ADDs this onto their value. This can also help when you've got an extra compression step, as if the changes are small, then the high bits of the detlas will probably contain a long string of zeros too.
Google's protobuf system for reading/writing bitstreams is also optimized for situations like this -- IIRC, when writing an integer, the process is something like the following:
* if it's smaller than 128, they write a 0 followed by the 7bit value
* else they write a 1, and then:
** if it's smaller than 16384, they write a 0, followed by the 14bit value
** else they write a 1, and then (...repeat the pattern...)
This causes small values to take up less space in the bit-stream, while large values pay an overhead of a few extra bits. If most of your values are small, then this can add up to a massive space saving.
Another trick I've heard of is, when sending deltas that are against some previously known state (like in Quake 3), you use the previous state as the "dictionary" in LZ-style compression. The kinds of compressors build up a list of bit patterns that they reference ("the dictionary"), so if there's a lot of common patterns in the previous state and the new one, then they will be able to compress the data really well. It's a kind of automatic delta encoding.
Normally this wouldn't be that great, because the "dictionary" has to normally be stored alongside the compressed file -- but in this situation, both parties already have the dictionary, so it doesn't need to be sent!