Encoding Vector2 and Vector3 variables into single int or float and back

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 :slight_smile:

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]);
    }
}