Hi, How would i implement a manual deltaCompression of 6bytes? Will i have to programm my own LZW?
i am just wondering if the idea i am working on would be a good approach? to sync player transforms or not:
instead of reliable-delta-compressed or unreliable-NOTdeltacompressed i am using an unreliable-delta-compressed method:
-sync the absolute transform every ~1s with a not deltacompressed packet
-sync the deltacomp. transform 15-30 times per second
so its like a video stream that has those i-frames (containing a whole picture) once every 30 frames and 29 p-frames which only contain the difference to the i frame
Scenario:
-2D Top-Down environment so i only need position.x and position.z and rotation.euler.y
-i drop some of the accuracy by converting those floats into short and ushort
–yields a positional resolution 0.1m within -3276.8m to 3276.7m of useable 2D terrain
–and a radial resolution 0.005° (360°/65535), i can drop even more accuracy here i guess
–>this sums up to 6bytes of data compared to the 36Bytes of unreliable transmitted transforms, allowing me to increase the sendRate
To implement unreliable deltas you need a way of knowing which packets got received on the other end, this is not something which unity exposes to you. So if you are using the built in networking, the short answer is that you “cant” do this.
The long answer is that you can anyway, but it requires you to build your own packet sequencing and ACK/NACK:ing on-top of the unity built in networking, which leads to a lot of overhead, so you will most likely loose any bandwidth you’d save by using deltas instead.
EDIT: Also, the model you described is a bit complicated (syncing some sort of “full” sync every second or whatever and is not really needed). Either go full delta or go full sync all the time, not something in-between.
hmm, i dont understand why i would need ACK/NACK:ing but i also didn’t check LZW-algorithm yet
my whole approach would be unreliable, i just send my 6 bytes via
if some full-sync packets get lost, the corresponding player may become somewhat out of sync.
But as soon as the next full-sync arrives we are synced again.
i just thought this could save a lot bandwidth compared to the standard unreliable approach (where everything is synced all the time) at the expense of small synchronization inaccuracies if some packets get lost
First of all, I don’t know of any networking library that uses LZW compression. This has nothing to do with delta compression. Sending the delta means that you send “what has changed since the last update the client received”.
If you don’t know what the client received, you don’t know what to calculate your delta against. You can’t calculate towards what “you sent in the previous packet” since you don’t know if this data reached the client. It does save a lot of bandwidth if implemented correctly, which requires you to ACK/NACK data received. If you don’t implement it correctly you will eventually end up with huge de-sync issues.
Edit: Yes, you could most likely mitigate some of the de-syncs by doing a “full-sync” at some specific interval, but it complicates things a fair amount in the protocol by mixing two different transfer modes (I don’t know of any game that actually does this). Also, the state data sent is usually a lot more then just pos/rot in a real game.
The simplest model that uses delta syncs is the way it’s done in Quake 3, there are several sources online that describe this networking model. It’s super robust and very simple to implement, but it puts a pretty tight upper bound on how large your entire game state can be, which limits total amount of players to usually around ~16.
Also, doing delta syncs for something which pretty much changed each frame, like position and rotation is pretty wasteful.
true, just checked where i read this, i thought it was in the Unity docs but i’ve had it from a quick gaze on http://en.wikipedia.org/wiki/Binary_delta_compression, shame on me :?
looking at that LZW it wouldn’t make sense too…
i came across this once and have a bookmark, i will take a closer look at it
aww, right. i only thought: transmitting in the whole range of -3276.8m to 3276.7m every sync while i can only move 120.0m/s max would be compressible by half a byte but obviously the yield wont be worth it for the dirty first implementation
Thank you fholm, for taking the time and leading me the way
Did you implement acks/nacks? I’d love to see. I do pretty much what fholm suggested not to (unreliable delta compressed against the last state the server sent, rather than the last state the client acknowledged receiving), but I tend to embrace the slop. In testing with 10-15% packet loss units still get to where they should because I do send a full state every 1-3 seconds.
Ohh, i think you got me wrong as i wrote “these helped me”. I am not even remotely finished with my stuff, i only program in my free time, so the progress since yesterday till now is:
-i can send my 10-bytes via 5 shorts using OnSerializeNetworkView
–my reduced-in-accuracy transform (6bytes)
–my reduced-in-accuracy velocity vector (for extrapolation in future) (2bytes)
–my triggers (activation-bools of weapons and devices) (2bytes)
-unpack them and apply them to the corresponding networkInstantiated Transform
-Yay!
but because my little game is also a shooter, i am aiming for the neat things i’ve found in the quake 3 article, mainly
-variable packet length using a bitmarker for each field
-gamestateHistory-acks/nacks for each client
what i did not grasp yet is how the client knows against which acknowledged-gamestate the delta was generated from the server… but i guess the server just has some iterator between the client or smth…
however, it is a long way for me and i want to understand all those functionalities
That’s how I do it… I’m using uLink, but all the syntax is largely the same as built-in, except I’m using unreliable RPCs in this case instead of uLink’s OnSerializeNetworkView, but not for any particular reason.
#pragma strict
function uLink_OnNetworkInstantiate(info : uLink.NetworkMessageInfo){
if(uLink.Network.isServer){
//start sending updates to clients
InvokeRepeating("SendState", 0, 1.0/15.0);
}
}
/*------------------------------------------------------------
Server logic
------------------------------------------------------------*/
//byte for denoting changes in the object' state
private enum ChangeFlags{
None = 0x0,
PosX = 0x1,
PosY = 0x2,
PosZ = 0x4,
ViewX = 0x8,
ViewY = 0x16,
ViewZ = 0x32,
Firing = 0x64
}
//save the last sent state values to compare against new onces
//to decide if they need to be sent
private var lastPosX : float;
private var lastPosY : float;
private var lastPosZ : float;
//send a full state update every 1-3 seconds in
//an attempt to compensate for packet loss
private var nextFullState : float = 0.0;
function SendState(){
var changeFlags = ChangeFlags.None;
var sendFullState : boolean = false;
if(uLink.Network.time > nextFullState){
sendFullState = true;
nextFullState = uLink.Network.time + Random.Range(1.0, 3.0);
}
if(transform.position.x != lastPosX || sendFullState)
changeFlags |= ChangeFlags.PosX;
if(transform.position.y != lastPosY || sendFullState)
changeFlags |= ChangeFlags.PosY;
if(transform.position.z != lastPosZ || sendFullState)
changeFlags |= ChangeFlags.PosZ;
//if changeflags != none, something must have changed
if(changeFlags != ChangeFlags.None){
var stream : uLink.BitStream = new uLink.BitStream(false);
//always write the changeflags and the timestamp to the stream
stream.Write.<byte>(changeFlags);
//only write the values that changed to the stream
if((changeFlags ChangeFlags.PosX) == ChangeFlags.PosX){
stream.Write.<float>(transform.position.x);
lastPosX = transform.position.x;
}
if((changeFlags ChangeFlags.PosY) == ChangeFlags.PosY){
stream.Write.<float>(transform.position.y);
lastPosY = transform.position.y;
}
if((changeFlags ChangeFlags.PosZ) == ChangeFlags.PosZ){
stream.Write.<float>(transform.position.z);
lastPosZ = transform.position.z;
}
//send the stream unreliably to the clients
uLink.NetworkView.Get(this).UnreliableRPC("UpdateState", uLink.RPCMode.Others, stream);
}
}
/*------------------------------------------------------------
Client logic
------------------------------------------------------------*/
var latestPos : Vector3;
var lastPos : Vector3;
var timeOfLastUpdate : float;
@RPC
function UpdateState(stream : uLink.BitStream){
lastPos = transform.position;
//Read in the change flags
var changeFlags : ChangeFlags = stream.Read.<byte>();
//Initialize the new state with the same values as the last state,
//since we're only reading in what has changed since then
latestPos = transform.position;
//Read in the new values
if((changeFlags ChangeFlags.PosX) == ChangeFlags.PosX)
latestPos.x = stream.Read.<float>();
if((changeFlags ChangeFlags.PosY) == ChangeFlags.PosY)
latestPos.y = stream.Read.<float>();
if((changeFlags ChangeFlags.PosZ) == ChangeFlags.PosZ)
latestPos.z = stream.Read.<float>();
timeOfLastUpdate = Time.time;
}
function Update(){
//only clients perform corrections and view interpolation
if(uLink.Network.isServer || uLink.NetworkView.Get(this).isMine)
return;
if((Time.time - timeOfLastUpdate) > 0.1){
var extrapDir = (latestPos - lastPos).normalized;
var extrapPos : Vector3 = transform.position + extrapDir * Time.deltaTime;
transform.position = Vector3.Lerp(transform.position, extrapPos, Time.deltaTime*10.0);
}
else
transform.position = Vector3.Lerp(transform.position, latestPos, Time.deltaTime*10.0);
}
@Prime: If you are using your ChangeFlags as a bit mask, you have a bug in your code. Hexadecimal notation does not work like that (0x32, 0x64, etc.). Either you use 1, 2, 4, 8, 16, 32, 64, etc. without the hex notation, your you should change it to the proper notation, which is : 0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80. Or to make it even more clear, do this: 1 << 0, 1 << 1, 1 << 2, 1 << 3, 1 << 4, 1 << 5, 1 << 6, 1 << 7. To signify which bit you are using.