I am starting to optimize a game for mobile pvp, and to that end I am looking to reduce my traffic as much as possible.
The maps will rarely exceed a cube of (-50, -50, -50) to (50, 50, 50) units with a required resolution of 1 decimal point. Vector3 data for each projectile fire needs even less data. That means I could nicely fit my coordinates into 10bit chunks that give me a range of values from -51.2 to 51.2 for each axis.
Integers are 32bits, and I need 30bits per vector.
So my first easiest path seems to be to strip all V3 data down to a 3 digit integer (the decimal removed by multiplying times 10) and then crush all of that into a single integer for transmission, and then decode it back to a V3 on the receiving end.
So before I go doing this, my question is - am I recreating someone else’s work I can just download and can I save myself a couple hours of learning how to bitwise encode/decode this? If not, does anyone have some code snippets they would recommend for doing this efficiently? I am sure I will not be the last person looking to do this.
I dont know anything about your game but it feels insane to even bother optimizing traffic that much
Anyway, i think c# - How do I save a floating-point number in 2 bytes? - Stack Overflow will point you in the right direction.
It is intended to be people in a small space moving at higher than should work speeds for PvP over mobile, so if I can cut my V3 data in a third - it should be of value. It might not be noticeable on its own, but as I start piling up syncvars and command/rpcs every little bit is likely going to matter. I need every millisecond I can get. I also will be relying less on player authority than games of this time for hiding the latency. Player movement is likely going to be the only item under player authority - the rest is all server - so all latency will be felt and seen.
I intend to squeeze every bit down as much as humanly possible. Thanks for the link, will add that to my reading when I attack this.
1 Like
public int encodeVector3ToInt(Vector3 v) {
//Vectors must stay within the -512 to 512 range per axis - no error handling coded here
//Add 512 to get numbers into the 0-1024 range rather than -512 to 512 range
//Multiply by 10 to save one decimal place from rounding
int xcomp = Mathf.RoundToInt((v.x * 10)) + 512;
int ycomp = Mathf.RoundToInt((v.y * 10)) + 512;
int zcomp = Mathf.RoundToInt((v.z * 10)) + 512;
return xcomp + ycomp * 1024 + zcomp * 1048576;
}
public Vector3 decodeVector3FromInt(int i) {
//Get the leftmost bits first. The fractional remains are the bits to the right.
// 1024 is 2 ^ 10 - 1048576 is 2 ^ 20 - just saving some calculation time doing that in advance
float z = Mathf.Floor(i / 1048576);
float y = Mathf.Floor ((i - z * 1048576) / 1024);
float x = (i - y * 1024 - z * 1048576);
// subtract 512 to move numbers back into the -512 to 512 range rather than 0 - 1024
return new Vector3 ((x - 512) / 10, (y - 512) / 10, (z - 512) / 10);
}
This is my dirty working code to solve the problem. If you have a world that is smaller than 102.4x102.4x102.4 units and a resolution of .1 units is good enough - this will reduce your network traffic. If anyone has faster/cleaner code methods to do this same thing - PLEASE pass them along.
There are also two unused bits on the left of the int. So they could be used to double the x & z limits to 204.8 units if your level is more wide than tall. Just replace the 1048576 and 1024 numbers with the appropriate 2^X result for the number of bits you want for each axis.
The resolution was a bit low, the alternative is to use shorts as a cheap half-float. That knocks 12 bytes per tick down to 6 (plus whatever unet overhead there is in each serialized command).
[Command(channel = Channels.DefaultUnreliable)]
private void CmdSendPositionShort(short x, short y, short z)
{
_lastPosition.Set(
(float)x / 100f,
(float)y / 100f,
(float)z / 100f);
}
CmdSendPositionShort (
(short)(transform.position.x * 100),
(short)(transform.position.y * 100),
(short)(transform.position.z * 100));
And my latest resting place for truncating vectors and encoding them into a single variable (and back). Note I am only using 3/4s of the ulongs 64bits. 16 bits per axis. The remaining unused flags I may use to pass other info later such things as animation states and other flags.
public ulong encodeVector3ToULong(Vector3 v) {
//Vectors must stay within the -320.00 to 320.00 range per axis - no error handling is coded here
//Adds 32768 to get numbers into the 0-65536 range rather than -32768 to 32768 range to allow unsigned
//Multiply by 100 to get two decimal place
ulong xcomp = (ulong)(Mathf.RoundToInt((v.x * 100f)) + 32768);
ulong ycomp = (ulong)(Mathf.RoundToInt((v.y * 100f)) + 32768);
ulong zcomp = (ulong)(Mathf.RoundToInt((v.z * 100f)) + 32768);
//Debug.Log ("comps " + xcomp + " " + ycomp + " " + zcomp);
return xcomp + ycomp * 65536 + zcomp * 4294967296;
}
public Vector3 decodeVector3FromULong(ulong i) {
//Debug.Log ("ulong " +i);
//Get the leftmost bits first. The fractional remains are the bits to the right.
// 1024 is 2 ^ 10 - 1048576 is 2 ^ 20 - just saving some calculation time doing that in advance
ulong z = (ulong)(i / 4294967296);
ulong y = (ulong)((i - z * 4294967296) / 65536);
ulong x = (ulong)(i - y * 65536 - z * 4294967296);
//Debug.Log (x + " " + y + " " + z);
// subtract 512 to move numbers back into the -512 to 512 range rather than 0 - 1024
return new Vector3 (((float)x - 32768f) / 100f, ((float)y - 32768f) / 100f, ((float)z - 32768f) / 100f);
}
Hopefully this wasn’t all for nothing - my understanding of what goes on with Syncvar behind the scenes is limited so I don’t know if regular network compression would be condensing a V3 to a comparable size. Again, there must be a more efficient way of doing this than decimal math, but right now my concern is crushing network usage more than sparing the CPU.
1 Like
@emotitron
this works for me. but I am unable to understand why are you multiplying
return xcomp + ycomp * 65536 + zcomp * 4294967296;
in case of encoding.
and in case of decoding how are you getting value of z and y
- ulong z = (ulong)(i / 4294967296);
- ulong y = (ulong)((i - z * 4294967296) / 65536);
it will be great if you can explain the logic
Thank you emotitron
I have created a dynamic class by advancing your solution.
abstract class Compressor
{
public static UInt64 Encode(float[] values, int resolution)
{
sbyte parts = (sbyte)values.Length;
sbyte k = (sbyte)(64 / parts);
UInt32 halfKVal = 1U << (k - 1);
float validAbsValue = (1 << (k - 1)) / resolution;
if (values.Any(val => Math.Abs(val) > validAbsValue))
throw new Exception(String.Format("Values({0}) must be between -{1} and {1} with resolution of {2}", string.Join(", ", values), validAbsValue, 1 / resolution));
UInt64 sum = 0;
for (int i = 0; i < parts; i++)
{
UInt64 comp = (UInt64)(Mathf.RoundToInt(values[i] * resolution) + halfKVal);
sum += comp << (i * k);
}
return sum;
}
public static float[] Decode(UInt64 sum, sbyte parts, int resolution)
{
sbyte k = (sbyte)(64 / parts);
UInt32 kVal = 1U << k;
UInt32 halfKVal = kVal >> 1;
UInt32 kValComplement = (kVal - 1);
float[] values = new float[parts];
for (int i = 0; i < parts; i++)
{
ulong comp = (sum >> (i * k)) & kValComplement;
values[i] = ((float)comp - halfKVal) / resolution;
}
return values;
}
}
sealed class Vector3Compressor1048_576 : Compressor // [-1048.576, 1048.576] (Resolution 0,001)
{
public static readonly int resolution = 1000; // 3 digit
public static readonly sbyte parts = 3;
public static readonly float max = 1048.576f;
public static readonly float min = -1048.576f;
public static UInt64 Encode(Vector3 vector)=> Encode(new float[] { vector.X, vector.Y, vector.Z }, resolution);
public static Vector3 Decode(UInt64 sum)
{
float[] values = Decode(sum, parts, resolution);
return new Vector3(values[0], values[1], values[2]);
}
}
sealed class QuaternionCompressor1_6384 : Compressor // [-1.6384, 1.6384] (Resolution 0,0001)
{
public static readonly int resolution = 10000; // 4 digit
public static readonly sbyte parts = 4;
public static readonly float max = 1.6384f;
public static readonly float min = -1.6384f;
public static UInt64 Encode(Quaternion vector) => Encode(new float[] { vector.X, vector.Y, vector.Z, vector.W }, resolution);
public static Quaternion Decode(UInt64 sum)
{
float[] values = Decode(sum, parts, resolution);
return new Quaternion(values[0], values[1], values[2], values[3]);
}
}